From 5efd35ebab7f7ddecb5ea5f840f09cdbc3adf9bd Mon Sep 17 00:00:00 2001 From: Mike Lester <themikelester@gmail.com> Date: Wed, 11 Dec 2024 08:55:19 +0200 Subject: [PATCH] JStudio: Added JMSG, RangeOutside, and RangeAdjust support --- src/Common/JSYSTEM/JStudio.ts | 192 +++++++++++++++++++++++++++------- src/ZeldaWindWaker/d_demo.ts | 5 +- 2 files changed, 159 insertions(+), 38 deletions(-) diff --git a/src/Common/JSYSTEM/JStudio.ts b/src/Common/JSYSTEM/JStudio.ts index e5a5f477f..5d078504e 100644 --- a/src/Common/JSYSTEM/JStudio.ts +++ b/src/Common/JSYSTEM/JStudio.ts @@ -12,8 +12,15 @@ const scratchVec3a = vec3.create(); const scratchVec3b = vec3.create(); const scratchVec3c = vec3.create(); +// TODO: Setup the JMessage system in a separate file +export namespace JMessage { + export class TControl { + public setMessageCode(packed: number): boolean { return true; }; + } +} + //---------------------------------------------------------------------------------------------------------------------- -// Stage Objects +// STB Objects // These are created an managed by the game. Each Stage Object has a corresponding STB Object, connected by an Adaptor. // The STB objects are manipulated by Sequences from the STB file each frame, and update the Stage Object via Adaptor. //---------------------------------------------------------------------------------------------------------------------- @@ -203,11 +210,11 @@ abstract class TAdaptor { public enableLogging = true, ) { } - public abstract adaptor_do_prepare(obj: STBObject): void; - public abstract adaptor_do_begin(obj: STBObject): void; - public abstract adaptor_do_end(obj: STBObject): void; - public abstract adaptor_do_update(obj: STBObject, frameCount: number): void; - public abstract adaptor_do_data(obj: STBObject, id: number, data: DataView): void; + public adaptor_do_prepare(obj: STBObject): void {}; + public adaptor_do_begin(obj: STBObject): void {}; + public adaptor_do_end(obj: STBObject): void {}; + public adaptor_do_update(obj: STBObject, frameCount: number): void {}; + public adaptor_do_data(obj: STBObject, id: number, data: DataView): void {}; // Set a single VariableValue update function, with the option of using FuncVals public adaptor_setVariableValue(obj: STBObject, keyIdx: number, data: ParagraphData) { @@ -573,7 +580,7 @@ class TActorAdaptor extends TAdaptor { return frame; } - public adaptor_do_prepare(obj: STBObject): void { + public override adaptor_do_prepare(obj: STBObject): void { this.variableValues[EActorTrack.AnimTransition].setOutput(this.object.JSGSetAnimationTransition.bind(this.object)); this.variableValues[EActorTrack.AnimFrame].setOutput((frame: number, adaptor: TAdaptor) => { @@ -587,7 +594,7 @@ class TActorAdaptor extends TAdaptor { }); } - public adaptor_do_begin(obj: STBObject): void { + public override adaptor_do_begin(obj: STBObject): void { this.object.JSGFEnableFlag(1); const pos = scratchVec3a; @@ -611,11 +618,11 @@ class TActorAdaptor extends TAdaptor { this.variableValues[EActorTrack.AnimFrame].setValue_immediate(this.object.JSGGetTextureAnimationFrame()); } - public adaptor_do_end(obj: STBObject): void { + public override adaptor_do_end(obj: STBObject): void { this.object.JSGFDisableFlag(1); } - public adaptor_do_update(obj: STBObject, frameCount: number): void { + public override adaptor_do_update(obj: STBObject, frameCount: number): void { const pos = scratchVec3a; const rot = scratchVec3b; const scale = scratchVec3c; @@ -633,7 +640,7 @@ class TActorAdaptor extends TAdaptor { this.object.JSGSetScaling(scale); } - public adaptor_do_data(obj: STBObject, id: number, data: DataView): void { + public override adaptor_do_data(obj: STBObject, id: number, data: DataView): void { this.log(`SetData: ${id}`); this.object.JSGSetData(id, data); } @@ -868,14 +875,14 @@ class TCameraAdaptor extends TAdaptor { override object: TCamera ) { super(11); } - public adaptor_do_prepare(obj: STBObject): void { + public override adaptor_do_prepare(obj: STBObject): void { this.variableValues[ECameraTrack.FovY].setOutput(this.object.JSGSetProjectionFovy.bind(this.object)); this.variableValues[ECameraTrack.Roll].setOutput(this.object.JSGSetViewRoll.bind(this.object)); this.variableValues[ECameraTrack.DistNear].setOutput(this.object.JSGSetProjectionNear.bind(this.object)); this.variableValues[ECameraTrack.DistFar].setOutput(this.object.JSGSetProjectionFar.bind(this.object)); } - public adaptor_do_begin(obj: STBObject): void { + public override adaptor_do_begin(obj: STBObject): void { const camPos = scratchVec3a; const targetPos = scratchVec3b; this.object.JSGGetViewPosition(camPos); @@ -892,11 +899,11 @@ class TCameraAdaptor extends TAdaptor { this.variableValues[ECameraTrack.DistFar].setValue_immediate(this.object.JSGGetProjectionFar()); } - public adaptor_do_end(obj: STBObject): void { + public override adaptor_do_end(obj: STBObject): void { this.object.JSGFDisableFlag(1); } - public adaptor_do_update(obj: STBObject, frameCount: number): void { + public override adaptor_do_update(obj: STBObject, frameCount: number): void { const camPos = scratchVec3a; const targetPos = scratchVec3b; @@ -910,7 +917,7 @@ class TCameraAdaptor extends TAdaptor { this.object.JSGSetViewTargetPosition(targetPos); } - public adaptor_do_data(obj: STBObject, id: number, data: DataView): void { + public override adaptor_do_data(obj: STBObject, id: number, data: DataView): void { // This is not used by TWW. Untested. debugger; } @@ -983,6 +990,39 @@ class TCameraObject extends STBObject { } } +//---------------------------------------------------------------------------------------------------------------------- +// Message +//---------------------------------------------------------------------------------------------------------------------- +class TMessageAdaptor extends TAdaptor { + constructor( private messageControl: JMessage.TControl ) { super(0, []); } + + public adaptor_do_MESSAGE(data: ParagraphData): void { + if(this.enableLogging) console.debug('JMSG:', data.value); + switch (data.dataOp) { + case EDataOp.ObjectIdx: this.messageControl.setMessageCode(data.value); break; + default: assert(false); + } + } +} + +class TMessageObject extends STBObject { + override adaptor: TMessageAdaptor; + + constructor( + control: TControl, + blockObj: TBlockObject, + adaptor: TMessageAdaptor, + ) { super(control, blockObj, adaptor) } + + public override do_paragraph(file: Reader, dataSize: number, dataOffset: number, param: number): void { + const type = param >> 5; + const dataOp = param & 0x1F; + switch( type ) { + case 0x42: return this.adaptor.adaptor_do_MESSAGE(readData(dataOp, dataOffset, dataSize, file)); + default: console.error('Unexpected JMSG paragraph type:', type); + } + } +} //---------------------------------------------------------------------------------------------------------------------- // Parsing helpers @@ -1057,13 +1097,21 @@ namespace FVB { InterpSet = 0x16, }; - enum EExtrapolationType { + enum EExtrapolateType { Raw, Repeat, Turn, Clamp } + enum EAdjustType { + Raw = 0, + BiasBegin = 1, + BiasEnd = 2, + BiasAve = 3, + Remap = 4, + }; + class TBlock { size: number; type: number; @@ -1072,6 +1120,7 @@ namespace FVB { }; export abstract class TFunctionValue { + public idNo: number; protected range: Attribute.Range | null = null; protected refer: Attribute.Refer | null = null; protected interpolate: Attribute.Interpolate | null = null; @@ -1084,18 +1133,16 @@ namespace FVB { public getAttrRefer() { return this.refer; } public getAttrInterpolate() { return this.interpolate; } - public static toFunction_outside(type: EExtrapolationType): (frame: number, maxFrame: number) => number { + public setIdNo(idNo: number) { this.idNo = idNo; } + + public static toFunction_outside(type: EExtrapolateType): (frame: number, maxFrame: number) => number { switch (type) { - case EExtrapolationType.Raw: return (f, m) => f; - case EExtrapolationType.Repeat: return (f, m) => { f = f % m; return f < 0 ? f + m : f; } - case EExtrapolationType.Turn: return (f, m) => { f %= (2 * m); if (f < 0) f += m; return f > m ? 2 * m - f : f }; - case EExtrapolationType.Clamp: return (f, m) => clamp(f, 0.0, m); + case EExtrapolateType.Raw: return (f, m) => f; + case EExtrapolateType.Repeat: return (f, m) => { f = f % m; return f < 0 ? f + m : f; } + case EExtrapolateType.Turn: return (f, m) => { f %= (2 * m); if (f < 0) f += m; return f > m ? 2 * m - f : f }; + case EExtrapolateType.Clamp: return (f, m) => clamp(f, 0.0, m); } } - - // static ExtrapolateParameter toFunction(TFunctionValue::TEOutside outside) { - // return toFunction_outside(outside); - // } } export abstract class TObject { @@ -1171,10 +1218,28 @@ namespace FVB { const interpType = file.view.getUint32(para.dataOffset + 0); interp.set(interpType); break; + + case EPrepareOp.RangeOutside: { + assert(para.dataSize === 4); + const range = this.funcVal.getAttrRange(); + assert(!!range, 'FVB Paragraph assumes FuncVal has range attribute, but it does not'); + const underflow = file.view.getInt16(para.dataOffset + 0); + const overflow = file.view.getInt16(para.dataOffset + 2); + range.setExtrapolation(underflow, overflow); + break; + } + + case EPrepareOp.RangeAdjust: { + assert(para.dataSize === 4); + const range = this.funcVal.getAttrRange(); + assert(!!range, 'FVB Paragraph assumes FuncVal has range attribute, but it does not'); + const adjust = file.view.getInt32(para.dataOffset + 0) + range.setAdjust(adjust); + break; + } + case EPrepareOp.RangeProgress: - case EPrepareOp.RangeAdjust: - case EPrepareOp.RangeOutside: default: console.warn('Unhandled FVB PrepareOp: ', para.type); debugger; @@ -1235,6 +1300,7 @@ namespace FVB { if (!obj) { return false; } obj.prepare(block, this.control, file); + obj.funcVal.setIdNo(this.control.objects.length); this.control.objects.push(obj); return true; @@ -1273,7 +1339,9 @@ namespace FVB { private diff: number = 0; private progress: number = 0; - private adjust: number = 0; + private adjust: EAdjustType = 0; + private underflow: EExtrapolateType = 0; + private overflow: EExtrapolateType = 0; public prepare() { // Progress updated here @@ -1286,10 +1354,47 @@ namespace FVB { assert(this.diff >= 0); } + public setAdjust(adjust: EAdjustType) { + this.adjust = adjust; + } + + public setExtrapolation(underflow: EExtrapolateType, overflow: EExtrapolateType) { + this.underflow = underflow; + this.overflow = overflow; + } + public getParameter(time: number, startTime: number, endTime: number): number { - // @NOTE: Does not currently support, Progress, Adjust, or Outside modifications. These can only be set + // @NOTE: Does not currently support, Progress modifications. These can only be set // in an FVB paragraph, so attempt to set them will be caught in FVB.TObject.prepare(). - return time; + + const progress = time; + + if( this.adjust != 0 ) { + debugger; // Untested. Remove once confirmed working + } + + switch (this.adjust) { + case EAdjustType.Raw: return this.extrapolate(progress); + case EAdjustType.BiasBegin: return this.extrapolate(progress + this.begin); + case EAdjustType.BiasEnd: return this.extrapolate(progress) + this.end; + case EAdjustType.BiasAve: return this.extrapolate(progress) + 0.5 * (this.begin + this.end); + case EAdjustType.Remap: + const temp = this.extrapolate(progress); + return startTime + ((temp - this.begin) * (endTime - startTime)) / this.diff; + + default: + debugger; + return this.extrapolate(progress); + } + } + + private extrapolate(progress: number) { + let t = progress + t -= this.begin; + if( t < 0.0 ) { t = FVB.TFunctionValue.toFunction_outside(this.underflow)(t, this.diff); } + else if( t >= this.diff) { t = FVB.TFunctionValue.toFunction_outside(this.overflow)(t, this.diff); } + t += this.begin; + return t; } } @@ -1631,7 +1736,6 @@ namespace FVB { } public static composite_add(fvs: TFunctionValue[], dataVal: number, timeSec: number): number { - debugger; // Untested. Remove once confirmed working let val = dataVal; for (let fv of fvs) { val += fv.getValue(timeSec); } return val; @@ -1771,6 +1875,7 @@ export abstract class TBlockObject { // This combines JStudio::TControl and JStudio::stb::TControl into a single class, for simplicity. export class TControl { public system: TSystem; + public msgControl: JMessage.TControl; public fvbControl = new FVB.TControl(); public secondsPerFrame: number = 1 / 30.0; private suspendFrames: number; @@ -1786,8 +1891,9 @@ export class TControl { // A special object that the STB file can use to suspend the demo (such as while waiting for player input) private controlObject = new TControlObject(this); - constructor(system: TSystem) { + constructor(system: TSystem, msgControl: JMessage.TControl) { this.system = system; + this.msgControl = msgControl; } public isSuspended() { return this.suspendFrames > 0; } @@ -1837,8 +1943,8 @@ export class TControl { public getFunctionValueByIdx(idx: number) { return this.fvbControl.objects[idx].funcVal; } public getFunctionValueByName(name: string) { return this.fvbControl.objects.find(v => v.id === name)?.funcVal; } - // Really this is a stb::TFactory method - public createObject(blockObj: TBlockObject): STBObject | null { + // Really this is a stb::TFactory `createObject` method + public createStageObject(blockObj: TBlockObject): STBObject | null { let objConstructor; let objType: JStage.EObject; switch (blockObj.type) { @@ -1862,6 +1968,18 @@ export class TControl { return obj; } + public createMessageObject(blockObj: TBlockObject): STBObject | null { + if( blockObj.type == 'JMSG' ) { + const adaptor = new TMessageAdaptor(this.msgControl); + const obj = new TMessageObject(this, blockObj, adaptor); + + if (obj) { adaptor.adaptor_do_prepare(obj); } + this.objects.push(obj); + return obj; + } + return null; + } + public destroyObject_all() { this.objects = []; this.fvbControl.destroyObject_all(); @@ -1899,7 +2017,9 @@ export class TParse { return true; } - const obj = this.control.createObject(blockObj); + let obj = this.control.createStageObject(blockObj); + if(!obj) { obj = this.control.createMessageObject(blockObj); } + if (!obj) { if (flags & 0x40) { console.debug('Unhandled flag during parseBlockObject: 0x40'); diff --git a/src/ZeldaWindWaker/d_demo.ts b/src/ZeldaWindWaker/d_demo.ts index 67a3ba121..ff552c305 100644 --- a/src/ZeldaWindWaker/d_demo.ts +++ b/src/ZeldaWindWaker/d_demo.ts @@ -3,7 +3,7 @@ import { mat4, ReadonlyVec3, vec3 } from "gl-matrix"; import ArrayBufferSlice from "../ArrayBufferSlice.js"; import { J3DModelInstance } from "../Common/JSYSTEM/J3D/J3DGraphBase.js"; import { LoopMode, TPT1, TTK1 } from "../Common/JSYSTEM/J3D/J3DLoader.js"; -import { JStage, TActor, TCamera, TControl, TParse, TSystem } from "../Common/JSYSTEM/JStudio.js"; +import { JMessage, JStage, TActor, TCamera, TControl, TParse, TSystem } from "../Common/JSYSTEM/JStudio.js"; import { getMatrixAxisY } from "../MathHelpers.js"; import { assert } from "../util.js"; import { ResType } from "./d_resorce.js"; @@ -427,7 +427,8 @@ export class dDemo_manager_c { private parser: TParse; private system = new dDemo_system_c(this.globals); - private control: TControl = new TControl(this.system); + private messageControl = new JMessage.TControl(); // TODO + private control: TControl = new TControl(this.system, this.messageControl); constructor( private globals: dGlobals