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