diff --git a/src/Common/JSYSTEM/JStudio.ts b/src/Common/JSYSTEM/JStudio.ts new file mode 100644 index 000000000..e591c1634 --- /dev/null +++ b/src/Common/JSYSTEM/JStudio.ts @@ -0,0 +1,1939 @@ +// Nintendo's cutscene framework. Seems very over-engineered. Data is stored in a STB (Studio Binary) file. + +import { mat4, ReadonlyVec3, vec3 } from "gl-matrix"; +import ArrayBufferSlice from "../../ArrayBufferSlice"; +import { align, assert, nArray, readString } from "../../util.js"; +import { JSystemFileReaderHelper } from "./J3D/J3DLoader.js"; +import { GfxColor } from "../../gfx/platform/GfxPlatform"; +import { clamp } from "../../MathHelpers.js"; +import { Endianness } from "../../endian.js"; + +const scratchVec3a = vec3.create(); +const scratchVec3b = vec3.create(); +const scratchVec3c = vec3.create(); + +//---------------------------------------------------------------------------------------------------------------------- +// Stage 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. +//---------------------------------------------------------------------------------------------------------------------- +export namespace JStage { + export enum EObject { + PreExistingActor = 0x0, + Unk1 = 0x1, + Actor = 0x2, + Camera = 0x3, + Ambient = 0x4, + Light = 0x5, + Fog = 0x6, + }; + + export abstract class TObject { + public JSGFDisableFlag(flag: number): void { this.JSGSetFlag(this.JSGGetFlag() & ~flag); } + public JSGFEnableFlag(flag: number): void { this.JSGSetFlag(this.JSGGetFlag() | flag); } + + public abstract JSGFGetType(): number; + public JSGGetName(): string | null { return null; } + public JSGGetFlag(): number { return 0; } + public JSGSetFlag(flag: number): void { } + public JSGGetData(unk0: number, data: Object, unk1: number): boolean { return false; } + public JSGSetData(id: number, data: DataView): void { } + public JSGGetParent(parentDst: JStage.TObject, unk: { x: number }): void { } + public JSGSetParent(parent: JStage.TObject | null, unk: number): void { } + public JSGSetRelation(related: boolean, obj: JStage.TObject, unk: number): void { } + public JSGFindNodeID(id: string): number { return -1; } + public JSGGetNodeTransformation(nodeId: number, mtx: mat4): number { + mat4.identity(mtx); + return 0; + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// System +// The main interface between a game and JStudio. Provides a method of finding or creating objects that will then be +// modified by a cutscene. Each game should override JSGFindObject() to supply or create objects for manipulation. +//---------------------------------------------------------------------------------------------------------------------- +export interface TSystem { + JSGFindObject(objId: string, objType: JStage.EObject): JStage.TObject | null; +} + +//---------------------------------------------------------------------------------------------------------------------- +// TVariableValue +// Manages a single float, which will be updated each frame. This float can be updated using a variety of operations: +// - Immediate(x): Set to single value. On Update(y), set the value to a single number then do nothing on future frames. +// - Time(x): Increase over time, mValue is the velocity. On Update(y), set the value to mValue * dt * mAge. +// - FuncVal(x): Set to the output of a functor. See FVB for details. +// +// Normally, after update() the value can be retrieved from mValue(). Alternatively, if setOutput() is called that +// functor will be called during update(). +//---------------------------------------------------------------------------------------------------------------------- +class TVariableValue { + private value: number; + private age: number; // In frames + private updateFunc: ((varval: TVariableValue, x: number) => void) | null = null; + private updateParam: number | FVB.TFunctionValue | null; + private outputFunc: ((val: number, adaptor: TAdaptor) => void) | null = null; + + public getValue() { return this.value; } + public getValueU8() { return clamp(this.value, 0, 255); } + + public forward(frameCount: number) { + if (Number.MAX_VALUE - this.age <= frameCount) { + this.age = Number.MAX_VALUE; + } else { + this.age += frameCount; + } + } + + public update(secondsPerFrame: number, adaptor: TAdaptor): void { + if (this.updateFunc) { + this.updateFunc(this, secondsPerFrame); + if (this.outputFunc) this.outputFunc(this.value, adaptor); + } + } + + //-------------------- + // Update functions + // Each frame, one of these (or nothing) will be called to update the value of each TVariableValue. + //-------------------- + private static update_immediate(varval: TVariableValue, secondsPerFrame: number): void { + varval.value = (varval.updateParam as number); + varval.updateFunc = null; + } + + private static update_time(varval: TVariableValue, secondsPerFrame: number): void { + varval.value = (varval.updateParam as number) * (varval.age * secondsPerFrame); + } + + private static update_functionValue(varval: TVariableValue, secondsPerFrame: number): void { + const t = varval.age * secondsPerFrame; + varval.value = (varval.updateParam as FVB.TFunctionValue).getValue(t); + } + + //-------------------- + // Set Update functions + // Modify the function that will be called each Update() + //-------------------- + public setValue_none() { + this.updateFunc = null; + } + + // Value will be set only on next update + public setValue_immediate(v: number): void { + assert(v !== undefined); + this.updateFunc = TVariableValue.update_immediate; + this.age = 0; + this.updateParam = v; + } + + // Value will be set to (mAge * v * x) each frame + public setValue_time(v: number): void { + assert(v !== undefined); + this.updateFunc = TVariableValue.update_time; + this.age = 0; + this.updateParam = v; + } + + // Value will be the result of a Function Value each frame + public setValue_functionValue(v: FVB.TFunctionValue | null = null): void { + assert(v !== undefined); + this.updateFunc = TVariableValue.update_functionValue; + this.age = 0; + this.updateParam = v; + } + + //-------------------- + // Set Output + //-------------------- + public setOutput(outputFunc: ((val: number, adaptor: TAdaptor) => void) | null = null) { + this.outputFunc = outputFunc; + } +} + + +//---------------------------------------------------------------------------------------------------------------------- +// TAdaptor +// Connects the STBObject to a Game Object. Manages tracks of TVariableValues, updates their values on the Game object. +//---------------------------------------------------------------------------------------------------------------------- +enum EDataOp { + None = 0, + Void = 1, // Disable updates for this track. + Immediate = 2, // Set the value on this track to an immediate value. + Time = 3, // The value increases each frame by a given velocity, starting at 0. + FuncValName = 0x10, // Evaluate a FunctionValue each frame and use the result + FuncValIdx = 0x12, // Same as FuncValName but by FunctionValue index + ObjectName = 0x18, // Set the value directly on the JStage object (e.g. an actor), don't store in the adaptor + ObjectIdx = 0x19, // Same as ObjectName, but by object index +}; + +type ParagraphData = ( + { dataOp: EDataOp.Void | EDataOp.None; value: null; } | + { dataOp: EDataOp.FuncValName | EDataOp.ObjectName; value: string; } | + { dataOp: EDataOp.FuncValIdx | EDataOp.ObjectIdx; value: number; } | + { dataOp: EDataOp.Immediate | EDataOp.Time; value: number; valueInt: number } +); + +// Parse data from a DataView as either a number or a string, based on the dataOp +function readData(dataOp: EDataOp, dataOffset: number, dataSize: number, file: Reader): ParagraphData { + switch (dataOp) { + case EDataOp.Immediate: + case EDataOp.Time: + return { dataOp, value: file.view.getFloat32(dataOffset), valueInt: file.view.getUint32(dataOffset) }; + + case EDataOp.FuncValIdx: + case EDataOp.ObjectIdx: + return { dataOp, value: file.view.getUint32(dataOffset) }; + + case EDataOp.FuncValName: + case EDataOp.ObjectName: + return { dataOp, value: readString(file.buffer, dataOffset, dataSize) }; + + default: + assert(false, 'Unsupported data operation'); + } +} + +abstract class TAdaptor { + public object: JStage.TObject; + + constructor( + public count: number, + public variableValues = nArray(count, i => new TVariableValue()), + 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; + + // Set a single VariableValue update function, with the option of using FuncVals + public adaptor_setVariableValue(obj: STBObject, keyIdx: number, data: ParagraphData) { + const varval = this.variableValues[keyIdx]; + const control = obj.control; + + switch (data.dataOp) { + case EDataOp.Void: varval.setValue_none(); break; + case EDataOp.Immediate: varval.setValue_immediate(data.value); break; + case EDataOp.Time: varval.setValue_time(data.value); break; + case EDataOp.FuncValName: varval.setValue_functionValue(control.getFunctionValueByName(data.value)); break; + case EDataOp.FuncValIdx: varval.setValue_functionValue(control.getFunctionValueByIdx(data.value)); break; + default: + console.debug('Unsupported dataOp: ', data.dataOp); + debugger; + return; + } + } + + // Immediately set 3 consecutive VariableValue update functions from a single vec3 + public adaptor_setVariableValue_Vec(startKeyIdx: number, data: vec3) { + this.variableValues[startKeyIdx + 0].setValue_immediate(data[0]); + this.variableValues[startKeyIdx + 1].setValue_immediate(data[1]); + this.variableValues[startKeyIdx + 2].setValue_immediate(data[2]); + } + + // Get the current value of 3 consecutive VariableValues, as a vector. E.g. Camera position. + public adaptor_getVariableValue_Vec(dst: vec3, startKeyIdx: number) { + dst[0] = this.variableValues[startKeyIdx + 0].getValue(); + dst[1] = this.variableValues[startKeyIdx + 1].getValue(); + dst[2] = this.variableValues[startKeyIdx + 2].getValue(); + } + + // Immediately set 4 consecutive VariableValue update functions from a single GXColor (4 bytes) + public adaptor_setVariableValue_GXColor(startKeyIdx: number, data: GfxColor) { + debugger; // @TODO: Confirm that all uses of this always have consecutive keyIdxs. JStudio remaps them. + this.variableValues[startKeyIdx + 0].setValue_immediate(data.r); + this.variableValues[startKeyIdx + 1].setValue_immediate(data.g); + this.variableValues[startKeyIdx + 2].setValue_immediate(data.b); + this.variableValues[startKeyIdx + 4].setValue_immediate(data.a); + } + + // Get the current value of 4 consecutive VariableValues, as a GXColor. E.g. Fog color. + public adaptor_getVariableValue_GXColor(dst: GfxColor, startKeyIdx: number) { + dst.r = this.variableValues[startKeyIdx + 0].getValue(); + dst.g = this.variableValues[startKeyIdx + 1].getValue(); + dst.b = this.variableValues[startKeyIdx + 2].getValue(); + dst.a = this.variableValues[startKeyIdx + 2].getValue(); + } + + public adaptor_updateVariableValue(obj: STBObject, frameCount: number) { + const control = obj.control; + for (let vv of this.variableValues) { + vv.forward(frameCount); + vv.update(control.secondsPerFrame, this); + } + } + + public log(msg: string) { + if (this.enableLogging) { console.debug(`[${this.object.JSGGetName()}] ${msg}`); } + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// STB Objects +// Created at parse time, and controlled by Sequences from the STB file. Connects to Game objects via an Adaptor. +// Each frame the STB data is marched (see do_paragraph) to update one or more properties of the Object via its Adaptor. +//---------------------------------------------------------------------------------------------------------------------- +abstract class STBObject { + public control: TControl; + public adaptor: TAdaptor; + + private id: string; + private type: string; + private flags: number; + private status: EStatus = EStatus.Still; + private isSequence: boolean = false; + private suspendFrames: number = 0; + private data: Reader; + private sequence: number; + private sequenceNext: number; + private wait: number = 0; + + constructor(control: TControl, blockObj: TBlockObject | null = null, adaptor: TAdaptor | null = null) { + this.control = control; + + if (blockObj && adaptor) { + this.adaptor = adaptor; + + this.id = blockObj.id; + this.type = blockObj.type; + this.flags = blockObj.flag; + this.data = blockObj.data; + this.sequence = 0; + this.sequenceNext = 0xC + align(blockObj.id.length + 1, 4); + } + } + + // These are intended to be overridden by subclasses + public abstract do_paragraph(file: Reader, dataSize: number, dataOffset: number, param: number): void; + public do_begin() { if (this.adaptor) this.adaptor.adaptor_do_begin(this); } + public do_end() { if (this.adaptor) this.adaptor.adaptor_do_end(this); } + + // Done updating this frame. Compute our variable data (i.e. interpolate) and send to the game object. + public do_wait(frameCount: number) { + if (this.adaptor) this.adaptor.adaptor_updateVariableValue(this, frameCount); + if (this.adaptor) this.adaptor.adaptor_do_update(this, frameCount); + } + public do_data(id: number, data: DataView) { if (this.adaptor) this.adaptor.adaptor_do_data(this, id, data); } + + public getStatus() { return this.status; } + public getSuspendFrames(): number { return this.suspendFrames; } + public isSuspended(): boolean { return this.suspendFrames > 0; } + public setSuspend(frameCount: number) { this.suspendFrames = frameCount; } + + public reset(blockObj: TBlockObject) { + this.sequence = 0; + this.status = EStatus.Still; + this.sequenceNext = 0xC + align(blockObj.id.length + 1, 4); + this.data = blockObj.data; + this.wait = 0; + } + + public forward(frameCount: number): boolean { + let hasWaited = false; + while (true) { + // Top bit of mFlags makes this object immediately inactive, restarting any existing sequence + if (this.flags & 0x8000) { + if (this.status != EStatus.Inactive) { + this.status = EStatus.Inactive; + if (this.isSequence) { + this.do_end(); + } + } + return true; + } + + if (this.status == EStatus.Inactive) { + assert(this.isSequence); + this.do_begin(); + this.status = EStatus.Wait; + } + + if ((this.control && this.control.isSuspended()) || this.isSuspended()) { + if (this.isSequence) { + assert((this.status == EStatus.Wait) || (this.status == EStatus.Suspend)); + this.status = EStatus.Suspend; + this.do_wait(frameCount); + } + return true; + } + + while (true) { + this.sequence = this.sequenceNext; + + // If there is nothing left in the sequence, end it + if (!this.sequence) { + if (this.isSequence) { + assert(this.status != EStatus.Still); + if (!hasWaited) { + this.do_wait(0); + } + this.isSequence = false; + this.status = EStatus.End; + this.do_end(); + } + return false; + } + + // If we're not currently running a sequence, start it + if (!this.isSequence) { + assert(this.status == EStatus.Still); + this.isSequence = true; + this.do_begin(); + } + + this.status = EStatus.Wait; + + if (this.wait == 0) { + this.process_sequence(); + if (this.wait == 0) { + break; + } + } + assert(this.wait > 0); + + hasWaited = true; + if (frameCount >= this.wait) { + const wait = this.wait; + frameCount -= this.wait; + this.wait = 0; + this.do_wait(wait); + } else { + this.wait -= frameCount; + this.do_wait(frameCount); + return true; + } + } + } + } + + private process_sequence() { + const view = this.data.view; + let byteIdx = this.sequence; + + let cmd = view.getUint8(byteIdx); + let param = view.getUint32(byteIdx) & 0xFFFFFF; + + let next = 0; + if (cmd != 0) { + if (cmd <= 0x7f) { + next = byteIdx + 4; + } else { + next = byteIdx + 4 + param; + } + } + + this.sequenceNext = next; + + switch (cmd) { + case ESequenceCmd.End: + break; + + case ESequenceCmd.SetFlag: + debugger; // Untested. Remove after confirmed working. + break; + + case ESequenceCmd.Wait: + this.wait = param; + break; + + case ESequenceCmd.Skip: + debugger; // Untested. Remove after confirmed working. + break; + + case ESequenceCmd.Suspend: + this.suspendFrames += param; + break; + + case ESequenceCmd.Paragraph: + byteIdx += 4; + while (byteIdx < this.sequenceNext) { + const para = TParagraph.parse(view, byteIdx); + if (para.type <= 0xff) { + this.process_paragraph_reserved_(this.data, para.dataSize, para.dataOffset, para.type); + } else { + this.do_paragraph(this.data, para.dataSize, para.dataOffset, para.type); + } + byteIdx = para.nextOffset; + } + + break; + + default: + console.debug('Unsupported sequence cmd: ', cmd); + debugger; + byteIdx += 4; + break; + } + } + + private process_paragraph_reserved_(file: Reader, dataSize: number, dataOffset: number, param: number): void { + switch (param) { + case 0x1: debugger; break; + case 0x2: debugger; break; + case 0x3: debugger; break; + case 0x80: debugger; break; + case 0x81: + const idSize = file.view.getUint16(dataOffset + 2); + assert(idSize == 4); + const id = file.view.getUint32(dataOffset + 4); + const contentOffset = dataOffset + 4 + align(idSize, 4); + const contentSize = dataSize - (contentOffset - dataOffset); + const content = file.buffer.createDataView(contentOffset, contentSize); + this.do_data(id, content); + break; + + case 0x82: + break; + } + } +} + +class TControlObject extends STBObject { + constructor(control: TControl) { + super(control) + } + + public override do_paragraph(file: Reader, dataSize: number, dataOffset: number, param: number): void { } +} + + +//---------------------------------------------------------------------------------------------------------------------- +// Actor +//---------------------------------------------------------------------------------------------------------------------- +enum EActorTrack { + AnimFrame = 0, + AnimTransition = 1, + TexAnimFrame = 2, + + PosX = 3, + PosY = 4, + PosZ = 5, + RotX = 6, + RotY = 7, + RotZ = 8, + ScaleX = 9, + ScaleY = 10, + ScaleZ = 11, + + Parent = 12, + Relation = 13, +} + +export abstract class TActor extends JStage.TObject { + public JSGFGetType() { return JStage.EObject.Actor; } + public JSGGetTranslation(dst: vec3) { } + public JSGSetTranslation(src: ReadonlyVec3) { } + public JSGGetScaling(dst: vec3) { } + public JSGSetScaling(src: ReadonlyVec3) { } + public JSGGetRotation(dst: vec3) { } + public JSGSetRotation(src: ReadonlyVec3) { } + public JSGGetShape(): number { return -1; } + public JSGSetShape(x: number): void { } + public JSGGetAnimation(): number { return -1; } + public JSGSetAnimation(x: number): void { } + public JSGGetAnimationFrame(): number { return 0.0; } + public JSGSetAnimationFrame(x: number): void { } + public JSGGetAnimationFrameMax(): number { return 0.0; } + public JSGGetAnimationTransition(): number { return 0.0; } + public JSGSetAnimationTransition(x: number): void { } + public JSGGetTextureAnimation(): number { return -1; } + public JSGSetTextureAnimation(x: number): void { } + public JSGGetTextureAnimationFrame(): number { return 0.0; } + public JSGSetTextureAnimationFrame(x: number): void { } + public JSGGetTextureAnimationFrameMax(): number { return 0.0; } +} + +class TActorAdaptor extends TAdaptor { + public parent: JStage.TObject | null = null; + public parentNodeID: number; + public relation: JStage.TObject | null = null; + public relationNodeID: number; + public animMode: number = 0; // See computeAnimFrame() + public animTexMode: number = 0; // See computeAnimFrame() + + constructor( + private system: TSystem, + public override object: TActor, + ) { super(14); } + + private static computeAnimFrame(animMode: number, maxFrame: number, frame: number) { + const outsideType = animMode & 0xFF; + const reverse = animMode >> 8; + + if (reverse) { frame = maxFrame - frame; } + if (maxFrame > 0.0) { + const func = FVB.TFunctionValue.toFunction_outside(outsideType); + frame = func(frame, maxFrame); + } + return frame; + } + + public 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) => { + frame = TActorAdaptor.computeAnimFrame(this.animMode, this.object.JSGGetAnimationFrameMax(), frame); + this.object.JSGSetAnimationFrame(frame); + }); + + this.variableValues[EActorTrack.TexAnimFrame].setOutput((frame: number, adaptor: TAdaptor) => { + frame = TActorAdaptor.computeAnimFrame(this.animTexMode, this.object.JSGGetTextureAnimationFrameMax(), frame); + this.object.JSGSetTextureAnimationFrame(frame); + }); + } + + public adaptor_do_begin(obj: STBObject): void { + this.object.JSGFEnableFlag(1); + + const pos = scratchVec3a; + const rot = scratchVec3b; + const scale = scratchVec3c; + this.object.JSGGetTranslation(pos); + this.object.JSGGetRotation(rot); + this.object.JSGGetScaling(scale); + + if (obj.control.isTransformEnabled()) { + vec3.transformMat4(pos, pos, obj.control.getTransformOnGet()); + rot[1] -= obj.control.transformRotY!; + } + + this.adaptor_setVariableValue_Vec(EActorTrack.PosX, pos); + this.adaptor_setVariableValue_Vec(EActorTrack.RotX, rot); + this.adaptor_setVariableValue_Vec(EActorTrack.ScaleX, scale); + + this.variableValues[EActorTrack.AnimTransition].setValue_immediate(this.object.JSGGetAnimationTransition()); + this.variableValues[EActorTrack.AnimFrame].setValue_immediate(this.object.JSGGetAnimationFrame()); + this.variableValues[EActorTrack.AnimFrame].setValue_immediate(this.object.JSGGetTextureAnimationFrame()); + } + + public adaptor_do_end(obj: STBObject): void { + this.object.JSGFDisableFlag(1); + } + + public adaptor_do_update(obj: STBObject, frameCount: number): void { + const pos = scratchVec3a; + const rot = scratchVec3b; + const scale = scratchVec3c; + this.adaptor_getVariableValue_Vec(pos, EActorTrack.PosX); + this.adaptor_getVariableValue_Vec(rot, EActorTrack.RotX); + this.adaptor_getVariableValue_Vec(scale, EActorTrack.ScaleX); + + if (obj.control.isTransformEnabled()) { + vec3.transformMat4(pos, pos, obj.control.getTransformOnSet()); + rot[1] += obj.control.transformRotY!; + } + + this.object.JSGSetTranslation(pos); + this.object.JSGSetRotation(rot); + this.object.JSGSetScaling(scale); + } + + public adaptor_do_data(obj: STBObject, id: number, data: DataView): void { + this.log(`SetData: ${id}`); + this.object.JSGSetData(id, data); + } + + public adaptor_do_PARENT(data: ParagraphData): void { + assert(data.dataOp == EDataOp.ObjectName); + this.log(`SetParent: ${data.value}`); + this.parent = this.system.JSGFindObject(data.value, JStage.EObject.PreExistingActor); + } + + public adaptor_do_PARENT_NODE(data: ParagraphData): void { + debugger; + this.log(`SetParentNode: ${data.value}`); + switch (data.dataOp) { + case EDataOp.ObjectName: + if (this.parent) + this.parentNodeID = this.parent.JSGFindNodeID(data.value); + break; + case EDataOp.ObjectIdx: + this.parentNodeID = data.value; + break; + default: assert(false); + } + } + + public adaptor_do_PARENT_ENABLE(data: ParagraphData): void { + assert(data.dataOp == EDataOp.Immediate); + this.log(`SetParentEnable: ${data.valueInt}`); + if (data.valueInt) { this.object.JSGSetParent(this.parent!, this.parentNodeID); } + else { this.object.JSGSetParent(null, 0xFFFFFFFF); } + } + + public adaptor_do_RELATION(data: ParagraphData): void { + assert(data.dataOp == EDataOp.ObjectName); + this.log(`SetRelation: ${data.value}`); + this.relation = this.system.JSGFindObject(data.value, JStage.EObject.PreExistingActor); + } + + public adaptor_do_RELATION_NODE(data: ParagraphData): void { + debugger; + this.log(`SetRelationNode: ${data.value}`); + switch (data.dataOp) { + case EDataOp.ObjectName: + if (this.relation) + this.relationNodeID = this.relation.JSGFindNodeID(data.value); + break; + case EDataOp.ObjectIdx: + this.relationNodeID = data.value; + break; + default: assert(false); + } + } + + public adaptor_do_RELATION_ENABLE(data: ParagraphData): void { + assert(data.dataOp == EDataOp.Immediate); + this.log(`SetRelationEnable: ${data.valueInt}`); + this.object.JSGSetRelation(!!data.valueInt, this.relation!, this.relationNodeID); + } + + public adaptor_do_SHAPE(data: ParagraphData): void { + assert(data.dataOp == EDataOp.ObjectIdx); + this.log(`SetShape: ${data.value}`); + this.object.JSGSetShape(data.value); + } + + public adaptor_do_ANIMATION(data: ParagraphData): void { + assert(data.dataOp == EDataOp.ObjectIdx); + this.log(`SetAnimation: ${(data.value) & 0xFFFF} (${(data.value) >> 4 & 0x01})`); + this.object.JSGSetAnimation(data.value); + } + + public adaptor_do_ANIMATION_MODE(data: ParagraphData): void { + assert(data.dataOp == EDataOp.Immediate); + this.log(`SetAnimationMode: ${data.valueInt}`); + this.animMode = data.valueInt; + } + + public adaptor_do_TEXTURE_ANIMATION(data: ParagraphData): void { + assert(data.dataOp == EDataOp.ObjectIdx); + this.log(`SetTexAnim: ${data.value}`); + this.object.JSGSetTextureAnimation(data.value); + } + + public adaptor_do_TEXTURE_ANIMATION_MODE(data: ParagraphData): void { + assert(data.dataOp == EDataOp.Immediate); + this.log(`SetTexAnimMode: ${data.valueInt}`); + this.animTexMode = data.valueInt; + } +} + +class TActorObject extends STBObject { + override adaptor: TActorAdaptor; + + constructor( + control: TControl, + blockObj: TBlockObject, + stageObj: JStage.TObject, + ) { super(control, blockObj, new TActorAdaptor(control.system, stageObj as TActor)) } + + public override do_paragraph(file: Reader, dataSize: number, dataOffset: number, param: number): void { + const dataOp = (param & 0x1F) as EDataOp; + const cmdType = param >> 5; + + let keyCount = 1; + let keyIdx; + let data = readData(dataOp, dataOffset, dataSize, file); + + switch (cmdType) { + // Pos + case 0x09: keyIdx = EActorTrack.PosX; break; + case 0x0a: keyIdx = EActorTrack.PosY; break; + case 0x0b: keyIdx = EActorTrack.PosZ; break; + case 0x0c: keyCount = 3; keyIdx = EActorTrack.PosX; break; + + // Rot + case 0x0d: keyIdx = EActorTrack.RotX; break; + case 0x0e: keyIdx = EActorTrack.RotY; break; + case 0x0f: keyIdx = EActorTrack.RotZ; break; + case 0x10: keyCount = 3; keyIdx = EActorTrack.RotX; break; + + // Scale + case 0x11: keyIdx = EActorTrack.ScaleX; break; + case 0x12: keyIdx = EActorTrack.ScaleY; break; + case 0x13: keyIdx = EActorTrack.ScaleZ; break; + case 0x14: keyCount = 3; keyIdx = EActorTrack.ScaleX; break; + + case 0x3b: keyIdx = EActorTrack.AnimFrame; break; + case 0x4b: keyIdx = EActorTrack.AnimTransition; break; + + case 0x39: this.adaptor.adaptor_do_SHAPE(data); return; + case 0x3a: this.adaptor.adaptor_do_ANIMATION(data); return; + case 0x43: this.adaptor.adaptor_do_ANIMATION_MODE(data); return; + case 0x4c: debugger; this.adaptor.adaptor_do_TEXTURE_ANIMATION(data); return; + case 0x4e: debugger; this.adaptor.adaptor_do_TEXTURE_ANIMATION_MODE(data); return; + + case 0x30: debugger; this.adaptor.adaptor_do_PARENT(data); return; + case 0x31: debugger; this.adaptor.adaptor_do_PARENT_NODE(data); return; + case 0x32: + debugger; + keyIdx = EActorTrack.Parent; + if (dataOp == EDataOp.FuncValIdx || dataOp == EDataOp.FuncValName) { + debugger; + this.adaptor.adaptor_setVariableValue(this, keyIdx, data); + this.adaptor.variableValues[keyIdx].setOutput((enabled, adaptor) => { + (adaptor as TActorAdaptor).adaptor_do_PARENT_ENABLE({ dataOp:EDataOp.Immediate, value: enabled, valueInt: enabled }); + }); + } else { + this.adaptor.adaptor_do_PARENT_ENABLE(data); + } + break; + + case 0x33: debugger; this.adaptor.adaptor_do_RELATION(data); return; + case 0x34: debugger; this.adaptor.adaptor_do_RELATION_NODE(data); return; + case 0x35: + debugger; + keyIdx = EActorTrack.Relation; + if ((dataOp < 0x13) && (dataOp > 0x0F)) { + debugger; + this.adaptor.adaptor_setVariableValue(this, keyIdx, data); + this.adaptor.variableValues[keyIdx].setOutput((enabled, adaptor) => { + (adaptor as TActorAdaptor).adaptor_do_RELATION_ENABLE({ dataOp:EDataOp.Immediate, value: enabled, valueInt: enabled }); + }); + } + this.adaptor.adaptor_do_RELATION_ENABLE(data); + break; + + default: + console.debug('Unsupported TActor update: ', cmdType, ' ', dataOp); + debugger; + return; + } + + let keyData = []; + for (let i = 0; i < keyCount; i++) { + keyData[i] = readData(dataOp, dataOffset + i * 4, dataSize, file); + this.adaptor.adaptor_setVariableValue(this, keyIdx + i, keyData[i]); + } + + const keyName = EActorTrack[keyIdx].slice(0, keyCount > 0 ? -1 : undefined); + this.adaptor.log(`Set${keyName}: ${EDataOp[dataOp]} [${keyData.map(k => k.value)}]`); + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// Camera +//---------------------------------------------------------------------------------------------------------------------- +enum ECameraTrack { + PosX = 0x00, + PosY = 0x01, + PosZ = 0x02, + TargetX = 0x03, + TargetY = 0x04, + TargetZ = 0x05, + FovY = 0x06, + Roll = 0x07, + DistNear = 0x08, + DistFar = 0x09, +} + +export abstract class TCamera extends JStage.TObject { + public JSGFGetType() { return JStage.EObject.Camera; } + public JSGGetProjectionType() { return true; } + public JSGSetProjectionType(type: number) { } + public JSGGetProjectionNear() { return 0.0; } + public JSGSetProjectionNear(near: number) { } + public JSGGetProjectionFar() { return Number.MAX_VALUE; } + public JSGSetProjectionFar(far: number) { } + public JSGGetProjectionFovy() { return 0.0 }; + public JSGSetProjectionFovy(fovy: number) { }; + public JSGGetProjectionAspect() { return 0.0 }; + public JSGSetProjectionAspect(aspect: number) { }; + public JSGGetProjectionField() { return 0.0 }; + public JSGSetProjectionField(field: number) { }; + public JSGGetViewType() { return true; }; + public JSGSetViewType(type: number) { } + public JSGGetViewPosition(dst: vec3) { vec3.zero(dst); } + public JSGSetViewPosition(v: ReadonlyVec3) { } + public JSGGetViewUpVector(dst: vec3) { vec3.zero(dst); } + public JSGSetViewUpVector(v: ReadonlyVec3) { } + public JSGGetViewTargetPosition(dst: vec3) { vec3.zero(dst); } + public JSGSetViewTargetPosition(v: ReadonlyVec3) { } + public JSGGetViewRoll() { return 0.0 }; + public JSGSetViewRoll(roll: number) { }; +} + +class TCameraAdaptor extends TAdaptor { + constructor( + override object: TCamera + ) { super(11); } + + public 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 { + const camPos = scratchVec3a; + const targetPos = scratchVec3b; + this.object.JSGGetViewPosition(camPos); + this.object.JSGGetViewTargetPosition(targetPos); + + vec3.transformMat4(camPos, camPos, obj.control.getTransformOnGet()); + vec3.transformMat4(targetPos, targetPos, obj.control.getTransformOnGet()); + + this.adaptor_setVariableValue_Vec(ECameraTrack.PosX, camPos); + this.adaptor_setVariableValue_Vec(ECameraTrack.TargetX, targetPos); + this.variableValues[ECameraTrack.FovY].setValue_immediate(this.object.JSGGetProjectionFovy()); + this.variableValues[ECameraTrack.Roll].setValue_immediate(this.object.JSGGetViewRoll()); + this.variableValues[ECameraTrack.DistNear].setValue_immediate(this.object.JSGGetProjectionNear()); + this.variableValues[ECameraTrack.DistFar].setValue_immediate(this.object.JSGGetProjectionFar()); + } + + public adaptor_do_end(obj: STBObject): void { + this.object.JSGFDisableFlag(1); + } + + public adaptor_do_update(obj: STBObject, frameCount: number): void { + const camPos = scratchVec3a; + const targetPos = scratchVec3b; + + this.adaptor_getVariableValue_Vec(camPos, ECameraTrack.PosX); + this.adaptor_getVariableValue_Vec(targetPos, ECameraTrack.TargetX); + + vec3.transformMat4(camPos, camPos, obj.control.getTransformOnSet()); + vec3.transformMat4(targetPos, targetPos, obj.control.getTransformOnSet()); + + this.object.JSGSetViewPosition(camPos); + this.object.JSGSetViewTargetPosition(targetPos); + } + + public adaptor_do_data(obj: STBObject, id: number, data: DataView): void { + // This is not used by TWW. Untested. + debugger; + } + + // Custom adaptor functions. These can be called from within TCameraObject::do_paragraph() + public adaptor_do_PARENT(dataOp: EDataOp, data: number | string, unk0: number): void { + debugger; + } + + public adaptor_do_PARENT_NODE(dataOp: EDataOp, data: number | string, unk0: number): void { + debugger; + } + + public adaptor_do_PARENT_ENABLE(dataOp: EDataOp, data: number | string, unk0: number): void { + debugger; + } +} + +class TCameraObject extends STBObject { + constructor( + control: TControl, + blockObj: TBlockObject, + stageObj: JStage.TObject, + ) { super(control, blockObj, new TCameraAdaptor(stageObj as TCamera)) } + + public override do_paragraph(file: Reader, dataSize: number, dataOffset: number, param: number): void { + const dataOp = (param & 0x1F) as EDataOp; + const cmdType = param >> 5; + + let keyCount = 1; + let keyIdx; + + switch (cmdType) { + // Eye position + case 0x15: keyIdx = ECameraTrack.PosX; break; + case 0x16: keyIdx = ECameraTrack.PosY; break; + case 0x17: keyIdx = ECameraTrack.PosZ; break; + case 0x18: keyCount = 3; keyIdx = ECameraTrack.PosX; break; + break; + + // Target position + case 0x19: keyIdx = ECameraTrack.TargetX; break; + case 0x1A: keyIdx = ECameraTrack.TargetY; break; + case 0x1B: keyIdx = ECameraTrack.TargetZ; break; + case 0x1C: keyCount = 3; keyIdx = ECameraTrack.TargetX; break; + + // Camera params + case 0x26: keyIdx = ECameraTrack.Roll; break; + case 0x27: keyIdx = ECameraTrack.FovY; break; + + // Near/far distance + case 0x28: keyIdx = ECameraTrack.DistNear; break; + case 0x29: keyIdx = ECameraTrack.DistFar; break; + case 0x2A: keyCount = 2; keyIdx = ECameraTrack.DistNear; break; + + default: + console.debug('Unsupported TCamera update: ', cmdType, ' ', dataOp); + debugger; + return; + } + + let keyData = [] + for (let i = 0; i < keyCount; i++) { + keyData[i] = readData(dataOp, dataOffset + i * 4, dataSize, file); + this.adaptor.adaptor_setVariableValue(this, keyIdx + i, keyData[i]); + } + + const keyName = ECameraTrack[keyIdx].slice(0, keyCount > 0 ? -1 : undefined); + this.adaptor.log(`Set${keyName}: ${EDataOp[dataOp]} [${keyData.map(k => k.value)}]`); + } +} + + +//---------------------------------------------------------------------------------------------------------------------- +// Parsing helpers +//---------------------------------------------------------------------------------------------------------------------- +class Reader { + buffer: ArrayBufferSlice; + view: DataView; + offset: number; + + constructor(buffer: ArrayBufferSlice, offset: number) { + this.buffer = buffer.subarray(offset); + this.view = this.buffer.createDataView(); + this.offset = 0; + } +} + +class TParagraph { + type: number; + dataSize: number; + dataOffset: number; + nextOffset: number; + + public static parse(view: DataView, byteIdx: number): TParagraph { + // The top bit of the paragraph determines if the type and size are 16 bit (if set), or 32 (if not set) + let dataSize = view.getUint16(byteIdx); + let type; + let offset; + + if ((dataSize & 0x8000) == 0) { + // 16 bit data + type = view.getUint16(byteIdx + 2); + offset = 4; + } else { + // 32 bit data + dataSize = view.getUint32(byteIdx + 0) & ~0x80000000; + type = view.getUint32(byteIdx + 4); + offset = 8; + } + + if (dataSize == 0) { + return { dataSize, type, dataOffset: 0, nextOffset: byteIdx + offset }; + } else { + return { dataSize, type, dataOffset: byteIdx + offset, nextOffset: byteIdx + offset + align(dataSize, 4) }; + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// FVB (Function Value Binary) Parsing +// Although embedded in the STB file, the FVB section is treated and parsed like a separate file +//---------------------------------------------------------------------------------------------------------------------- +namespace FVB { + enum EFuncValType { + None = 0, + Composite = 1, + Constant = 2, + Transition = 3, + List = 4, + ListParameter = 5, + Hermite = 6, + }; + + enum EPrepareOp { + None = 0x00, + Data = 0x01, + ObjSetByName = 0x10, + ObjSetByIdx = 0x11, + RangeSet = 0x12, + RangeProgress = 0x13, + RangeAdjust = 0x14, + RangeOutside = 0x15, + InterpSet = 0x16, + }; + + enum EExtrapolationType { + Raw, + Repeat, + Turn, + Clamp + } + + class TBlock { + size: number; + type: number; + id: string; + dataOffset: number; + }; + + export abstract class TFunctionValue { + protected range: Attribute.Range | null = null; + protected refer: Attribute.Refer | null = null; + protected interpolate: Attribute.Interpolate | null = null; + + public abstract getType(): EFuncValType; + public abstract prepare(): void; + public abstract getValue(arg: number): number; + + public getAttrRange() { return this.range; } + public getAttrRefer() { return this.refer; } + public getAttrInterpolate() { return this.interpolate; } + + public static toFunction_outside(type: EExtrapolationType): (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); + } + } + + // static ExtrapolateParameter toFunction(TFunctionValue::TEOutside outside) { + // return toFunction_outside(outside); + // } + } + + export abstract class TObject { + public funcVal: TFunctionValue; + public id: string; + + constructor(block: TBlock) { + this.id = block.id; + } + + public abstract prepare_data(para: TParagraph, control: TControl, file: Reader): void; + + public prepare(block: TBlock, pControl: TControl, file: Reader) { + const blockNext = file.offset + block.size; + file.offset = blockNext; + + let pOffset = block.dataOffset; + while (pOffset < blockNext) { + const para = TParagraph.parse(file.view, pOffset); + switch (para.type) { + case EPrepareOp.None: + this.funcVal.prepare(); + assert(para.nextOffset == blockNext); + return; + + case EPrepareOp.Data: + this.prepare_data(para, pControl, file); + break; + + case EPrepareOp.RangeSet: + assert(para.dataSize == 8); + const range = this.funcVal.getAttrRange(); + assert(!!range, 'FVB Paragraph assumes FuncVal has range attribute, but it does not'); + const begin = file.view.getFloat32(para.dataOffset + 0); + const end = file.view.getFloat32(para.dataOffset + 4); + range.set(begin, end); + break; + + case EPrepareOp.ObjSetByName: { + debugger; // Untested. Remove after confirmed working. + assert(para.dataSize >= 4); + const refer = this.funcVal.getAttrRefer(); + assert(!!refer, 'FVB Paragraph assumes FuncVal has refer attribute, but it does not'); + const objCount = file.view.getUint32(para.dataOffset + 0); + for (let i = 0; i < objCount; i++) { + const idSize = file.view.getUint32(para.dataOffset + 4 + i * 8 + 0); + const id = readString(file.buffer, para.dataOffset + 4 + i * 8 + 4, idSize); + const obj = pControl.objects.find(o => o.id == id); + assert(!!obj); + refer.fvs.push(obj.funcVal); + } + break; + } + + case EPrepareOp.ObjSetByIdx: { + assert(para.dataSize >= 4); + const refer = this.funcVal.getAttrRefer(); + assert(!!refer, 'FVB Paragraph assumes FuncVal has refer attribute, but it does not'); + const objCount = file.view.getUint32(para.dataOffset + 0); + for (let i = 0; i < objCount; i++) { + const idx = file.view.getUint32(para.dataOffset + 4 + i * 4); + const obj = pControl.objects[idx]; + assert(!!obj); + refer.fvs.push(obj.funcVal); + } + break; + } + + case EPrepareOp.InterpSet: + assert(para.dataSize == 4); + const interp = this.funcVal.getAttrInterpolate(); + assert(!!interp, 'FVB Paragraph assumes FuncVal has interpolate attribute, but it does not'); + const interpType = file.view.getUint32(para.dataOffset + 0); + interp.set(interpType); + break; + + case EPrepareOp.RangeProgress: + case EPrepareOp.RangeAdjust: + case EPrepareOp.RangeOutside: + default: + console.warn('Unhandled FVB PrepareOp: ', para.type); + debugger; + } + pOffset = para.nextOffset; + } + + assert(pOffset == blockNext); + this.funcVal.prepare(); + } + } + + export class TControl { + public objects: TObject[] = []; + + // Really this is a fvb::TFactory method + public createObject(block: TBlock): TObject | null { + switch (block.type) { + case EFuncValType.Composite: + return new TObject_Composite(block); + case EFuncValType.Constant: + return new TObject_Constant(block); + // case EFuncValType.Transition: + // return new TObject_transition(block); + // case EFuncValType.List: + // return new TObject_list(block); + case EFuncValType.ListParameter: + return new TObject_ListParameter(block); + case EFuncValType.Hermite: + return new TObject_Hermite(block); + default: + console.warn('Unknown FVB type: ', block.type); + debugger; + return null; + } + } + + public destroyObject_all() { + this.objects = []; + } + } + + export class TParse { + constructor( + private control: TControl + ) { } + + private parseBlock(file: Reader, flags: number): boolean { + const idLen = file.view.getUint16(file.offset + 6); + const block: TBlock = { + size: file.view.getUint32(file.offset + 0), + type: file.view.getUint16(file.offset + 4), + id: readString(file.buffer, file.offset + 8, idLen), + dataOffset: file.offset + align(8 + idLen, 4), + } + + const obj = this.control.createObject(block); + if (!obj) { return false; } + + obj.prepare(block, this.control, file); + this.control.objects.push(obj); + + return true; + } + + public parse(data: ArrayBufferSlice, flags: number) { + const view = data.createDataView(); + let fourCC = readString(data, 0, 4); + let byteOrder = view.getUint16(0x04); + let version = view.getUint16(0x06); + let blockCount = view.getUint32(0x0C); + assert(fourCC === 'FVB'); + assert(byteOrder == 0xFEFF); + assert(version >= 2 && version <= 256); // As of Wind Waker + + const blockReader = new Reader(data, 16); + for (let i = 0; i < blockCount; i++) { + this.parseBlock(blockReader, flags); + } + } + } + + //---------------------------------------------------------------------------------------------------------------------- + // FV Attributes + //---------------------------------------------------------------------------------------------------------------------- + enum EInterpolateType { + None = 0, + Linear = 1, + Plateau = 2, + BSpline = 3 + } + namespace Attribute { + export class Range { + private begin: number = 0; + private end: number = 0; + private diff: number = 0; + + private progress: number = 0; + private adjust: number = 0; + + public prepare() { + // Progress updated here + } + + public set(begin: number, end: number) { + this.begin = begin; + this.end = end; + this.diff = end - begin; + assert(this.diff >= 0); + } + + public getParameter(time: number, startTime: number, endTime: number): number { + // @NOTE: Does not currently support, Progress, Adjust, or Outside modifications. These can only be set + // in an FVB paragraph, so attempt to set them will be caught in FVB.TObject.prepare(). + return time; + } + } + + export class Refer { + public fvs: TFunctionValue[] = []; + } + + export class Interpolate { + private type = EInterpolateType.None; + public prepare() { } + public set(type: EInterpolateType) { this.type = type; } + public get() { return this.type; } + + public static Linear(t: number, t0: number, v0: number, t1: number, v1: number) { + return v0 + ((v1 - v0) * (t - t0)) / (t1 - t0); + } + + public static BSpline_Nonuniform(t: number, controlPoints: Float64Array, knotVector: Float64Array) { + const knot0 = knotVector[0]; + const knot1 = knotVector[1]; + const knot2 = knotVector[2]; + const knot3 = knotVector[3]; + const knot4 = knotVector[4]; + const knot5 = knotVector[5]; + const diff0 = t - knot0; + const diff1 = t - knot1; + const diff2 = t - knot2; + const diff3 = knot3 - t; + const diff4 = knot4 - t; + const diff5 = knot5 - t; + const inverseDeltaKnot32 = 1 / (knot3 - knot2); + const blendFactor3 = (diff3 * inverseDeltaKnot32) / (knot3 - knot1); + const blendFactor2 = (diff2 * inverseDeltaKnot32) / (knot4 - knot2); + const blendFactor1 = (diff3 * blendFactor3) / (knot3 - knot0); + const blendFactor4 = ((diff1 * blendFactor3) + (diff4 * blendFactor2)) / (knot4 - knot1); + const blendFactor5 = (diff2 * blendFactor2) / (knot5 - knot2); + const term1 = diff3 * blendFactor1; + const term2 = (diff0 * blendFactor1) + (diff4 * blendFactor4); + const term3 = (diff1 * blendFactor4) + (diff5 * blendFactor5); + const term4 = diff2 * blendFactor5; + + return (term1 * controlPoints[0]) + (term2 * controlPoints[1]) + (term3 * controlPoints[2]) + (term4 * controlPoints[3]); + } + + public static Hermite(c0: number, c1: number, x: number, c2: number, x2: number, c3: number, x3: number) { + let a: number; + let b: number; + let c: number; + let d: number; + + a = c0 - c1; + b = a * (1.0 / (x2 - c1)); // (a - b) * 1.0 / (c - d) + c = b - 1.0; // 1.0 + d = (3.0 + -2.0 * b) * (b * b); // 3.0 - 2.0 * b + const cab = (c * a * b); + const coeffx3 = cab * x3; + const cca = (c * c * a); + const coeffc2 = cca * c2; + return ((1.0 - d) * x + (d * c3)) + coeffc2 + coeffx3; + } + } + } + + //---------------------------------------------------------------------------------------------------------------------- + // FunctionValue: Constant + // Simply return a constant value every frame + //---------------------------------------------------------------------------------------------------------------------- + class TObject_Constant extends FVB.TObject { + override funcVal = new FunctionValue_Constant; + + public override prepare_data(para: TParagraph, control: TControl, file: Reader): void { + assert(para.dataSize == 4); + const value = file.view.getFloat32(para.dataOffset); + this.funcVal.setData(value); + } + } + + class FunctionValue_Constant extends TFunctionValue { + private value: number = 0; + + public getType() { return EFuncValType.Constant; } + public prepare() { } + public setData(value: number) { this.value = value; } + public getValue(timeSec: number) { + return this.value; + } + } + + //---------------------------------------------------------------------------------------------------------------------- + // FunctionValue: ListParameter + // Interpolate between a list of values using a specific interpolation function [None, Linear, Plateau, BSpline] + //---------------------------------------------------------------------------------------------------------------------- + class TObject_ListParameter extends FVB.TObject { + override funcVal = new FunctionValue_ListParameter; + + public override prepare_data(para: TParagraph, control: TControl, file: Reader): void { + assert(para.dataSize >= 8); + // Each Key contains 2 floats, a time and value + const keyCount = file.view.getUint32(para.dataOffset + 0); + const keys = file.buffer.createTypedArray(Float32Array, para.dataOffset + 4, keyCount * 2, Endianness.BIG_ENDIAN); + this.funcVal.setData(keys); + } + } + class FunctionValue_ListParameter extends TFunctionValue { + protected override range = new Attribute.Range(); + protected override interpolate = new Attribute.Interpolate(); + + // Each key contains 2 floats, a time and value + private keyCount: number = 0; + private keys: Float32Array; + private curKeyIdx: number; + private interpFunc: (t: number) => number; + + public prepare(): void { + this.range.prepare(); + this.interpolate.prepare(); + + const interp = this.interpolate.get(); + switch (interp) { + case EInterpolateType.None: this.interpFunc = this.interpolateNone; + case EInterpolateType.Linear: this.interpFunc = this.interpolateLinear; + case EInterpolateType.Plateau: this.interpFunc = this.interpolatePlateau; + case EInterpolateType.BSpline: + if (this.keyCount > 2) { this.interpFunc = this.interpolateBSpline; } + else { this.interpFunc = this.interpolateLinear; } + break; + + default: + console.warn('Invalid EInterp value', interp); + debugger; + } + } + + public setData(values: Float32Array) { + this.keys = values; + this.keyCount = values.length / 2; + this.curKeyIdx = 0; + } + + public getType() { return EFuncValType.ListParameter; } + public getStartTime() { return this.keys[0]; } + public getEndTime(): number { return this.keys[this.keys.length - 2]; } + + // Interpolate between our keyframes, given the current time + public getValue(timeSec: number): number { + // Remap (if requested) the time to our range + const t = this.range.getParameter(timeSec, this.getStartTime(), this.getEndTime()); + + // Update our current key. If the current time is between keys, select the later one. + this.curKeyIdx = this.keys.findIndex((k, i) => (i % 2) == 0 && k >= t) / 2; + + if (this.curKeyIdx == 0) { // Time is at or before the start, return the first key + return this.keys[this.curKeyIdx * 2 + 1]; + } else if (this.curKeyIdx < 0) { // Time is at or after the end, return the last key + this.curKeyIdx = this.keyCount - 1; + return this.keys[this.curKeyIdx * 2 + 1]; + } + + const value = this.interpFunc(t); + if (isNaN(value)) { + console.warn('NaN generated by FunctionValue'); + debugger; + } + + return value; + } + + public interpolateBSpline(t: number): number { + const c = this.curKeyIdx * 2; + + const controlPoints = new Float64Array(4); + const knotVector = new Float64Array(6); + controlPoints[1] = this.keys[c - 1]; + controlPoints[2] = this.keys[c + 1]; + knotVector[2] = this.keys[c + -2]; + knotVector[3] = this.keys[c + 0]; + + const keysBefore = this.curKeyIdx; + const keysAfter = this.keyCount - this.curKeyIdx; + + switch (keysBefore) { + case 1: + controlPoints[0] = 2.0 * controlPoints[1] - controlPoints[2]; + controlPoints[3] = this.keys[c + 3]; + knotVector[4] = this.keys[c + 2]; + knotVector[1] = 2.0 * knotVector[2] - knotVector[3]; + knotVector[0] = 2.0 * knotVector[2] - knotVector[4]; + switch (keysAfter) { + case 1: + case 2: + knotVector[5] = 2.0 * knotVector[4] - knotVector[3]; + break; + default: + knotVector[5] = this.keys[c + 4]; + break; + } + break; + case 2: + controlPoints[0] = this.keys[c + -3]; + knotVector[1] = this.keys[c + -4]; + knotVector[0] = 2.0 * knotVector[1] - knotVector[2]; + switch (keysAfter) { + case 1: + controlPoints[3] = 2.0 * controlPoints[2] - controlPoints[1]; + knotVector[4] = 2.0 * knotVector[3] - knotVector[2]; + knotVector[5] = 2.0 * knotVector[3] - knotVector[1]; + break; + case 2: + controlPoints[3] = this.keys[c + 3]; + knotVector[4] = this.keys[c + 2]; + knotVector[5] = 2.0 * knotVector[4] - knotVector[3]; + break; + default: + controlPoints[3] = this.keys[c + 3]; + knotVector[4] = this.keys[c + 2]; + knotVector[5] = this.keys[c + 4]; + } + break; + default: + controlPoints[0] = this.keys[c + -3]; + knotVector[1] = this.keys[c + -4]; + knotVector[0] = this.keys[c + -6]; + switch (keysAfter) { + case 1: + controlPoints[3] = 2.0 * controlPoints[2] - controlPoints[1]; + knotVector[4] = 2.0 * knotVector[3] - knotVector[2]; + knotVector[5] = 2.0 * knotVector[3] - knotVector[1]; + break; + case 2: + controlPoints[3] = this.keys[c + 3]; + knotVector[4] = this.keys[c + 2]; + knotVector[5] = 2.0 * knotVector[4] - knotVector[3]; + break; + default: + controlPoints[3] = this.keys[c + 3]; + knotVector[4] = this.keys[c + 2]; + knotVector[5] = this.keys[c + 4]; + break; + } + break; + } + + return Attribute.Interpolate.BSpline_Nonuniform(t, controlPoints, knotVector); + } + + public interpolateNone(t: number) { + debugger; // Untested. Remove after confirmed working. + return this.keys[this.curKeyIdx]; + } + + public interpolateLinear(t: number) { + const ks = this.keys; + const c = this.curKeyIdx * 2; + return Attribute.Interpolate.Linear(t, ks[c - 2], ks[c - 1], ks[c + 0], ks[c + 1]); + } + + public interpolatePlateau(t: number) { + console.error('Plateau interpolation not yet implemented') + debugger; // Untested. Remove after confirmed working. + return this.interpolateNone(t); + } + } + + //---------------------------------------------------------------------------------------------------------------------- + // FunctionValue: Composite + // Perform a simple operation to combine some number of other FunctionValues, returning the result. + // For example, we can using the ADD ECompositeOp we can return the sum of two ListParameter FunctionValues. + //---------------------------------------------------------------------------------------------------------------------- + enum ECompositeOp { + None, + Raw, + Idx, + Parm, + Add, + Sub, + Mul, + Div, + } + + class TObject_Composite extends FVB.TObject { + override funcVal = new FunctionValue_Composite; + + public override prepare_data(para: TParagraph, control: TControl, file: Reader): void { + assert(para.dataSize >= 8); + + const compositeOp = file.view.getUint32(para.dataOffset + 0); + const floatData = file.view.getFloat32(para.dataOffset + 4); + const uintData = file.view.getUint32(para.dataOffset + 4); + + let fvData: number; + let fvFunc: (ref: TFunctionValue[], data: number, t: number) => number; + switch (compositeOp) { + case ECompositeOp.Raw: fvData = uintData; fvFunc = FunctionValue_Composite.composite_raw; break; + case ECompositeOp.Idx: fvData = uintData; fvFunc = FunctionValue_Composite.composite_index; break; + case ECompositeOp.Parm: fvData = floatData; fvFunc = FunctionValue_Composite.composite_parameter; break; + case ECompositeOp.Add: fvData = floatData; fvFunc = FunctionValue_Composite.composite_add; break; + case ECompositeOp.Sub: fvData = floatData; fvFunc = FunctionValue_Composite.composite_subtract; break; + case ECompositeOp.Mul: fvData = floatData; fvFunc = FunctionValue_Composite.composite_multiply; break; + case ECompositeOp.Div: fvData = floatData; fvFunc = FunctionValue_Composite.composite_divide; break; + default: + console.warn('Unsupported CompositeOp:', compositeOp); + return; + } + + this.funcVal.setData(fvFunc, fvData) + } + } + + class FunctionValue_Composite extends TFunctionValue { + protected override refer = new Attribute.Refer(); + + public override prepare(): void { } + public override getType(): EFuncValType { return EFuncValType.Composite; } + public setData(func: (ref: TFunctionValue[], dataVal: number, t: number) => number, dataVal: number) { + this.func = func; + this.dataVal = dataVal; + } + + public getValue(timeSec: number): number { + return this.func(this.refer.fvs, this.dataVal, timeSec); + } + + public static composite_raw(fvs: TFunctionValue[], dataVal: number, timeSec: number): number { + debugger; // Untested. Remove once confirmed working + if (fvs.length == 0) { return 0.0; } + return fvs[dataVal].getValue(timeSec); + } + + public static composite_index(fvs: TFunctionValue[], dataVal: number, timeSec: number): number { + debugger; // Untested. Remove once confirmed working + return 0.0; + } + + public static composite_parameter(fvs: TFunctionValue[], dataVal: number, timeSec: number): number { + debugger; // Untested. Remove once confirmed working + let val = timeSec - dataVal; + for (let fv of fvs) { val = fv.getValue(timeSec); } + return val; + } + + 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; + } + + public static composite_subtract(fvs: TFunctionValue[], dataVal: number, timeSec: number): number { + debugger; // Untested. Remove once confirmed working + if (fvs.length == 0) { return 0.0; } + let val = fvs[0].getValue(timeSec); + for (let fv of fvs.slice(1)) { val -= fv.getValue(timeSec); } + return val - dataVal; + } + + public static composite_multiply(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; + } + + public static composite_divide(fvs: TFunctionValue[], dataVal: number, timeSec: number): number { + debugger; // Untested. Remove once confirmed working + if (fvs.length == 0) { return 0.0; } + let val = fvs[0].getValue(timeSec); + for (let fv of fvs.slice(1)) { val /= fv.getValue(timeSec); } + return val / dataVal; + } + + private func: (ref: TFunctionValue[], dataVal: number, t: number) => number; + private dataVal: number; + } + + //---------------------------------------------------------------------------------------------------------------------- + // FunctionValue: Hermite + // Use hermite interpolation to compute a value from a list + //---------------------------------------------------------------------------------------------------------------------- + class TObject_Hermite extends FVB.TObject { + public override funcVal = new FunctionValue_Hermite; + + public override prepare_data(para: TParagraph, control: TControl, file: Reader): void { + assert(para.dataSize >= 8); + + const keyCount = file.view.getUint32(para.dataOffset + 0) & 0xFFFFFFF; + const stride = file.view.getUint32(para.dataOffset + 0) >> 0x1C; + + // Each Key contains `stride` floats, a time and value + const keys = file.buffer.createTypedArray(Float32Array, para.dataOffset + 4, keyCount * stride, Endianness.BIG_ENDIAN); + this.funcVal.setData(keys, stride); + } + } + + class FunctionValue_Hermite extends TFunctionValue { + protected override range = new Attribute.Range(); + + // Each key contains `stride` floats, a time and values + private keyCount: number = 0; + private keys: Float32Array; + private curKeyIdx: number; + private stride: number; + + public prepare(): void { this.range.prepare(); } + + public setData(values: Float32Array, stride: number) { + assert(stride == 3 || stride == 4); + this.stride = stride + this.keys = values; + this.keyCount = values.length / stride; + this.curKeyIdx = 0; + } + + public getType() { return EFuncValType.ListParameter; } + public getStartTime() { return this.keys[0]; } + public getEndTime(): number { return this.keys[(this.keyCount - 1) * this.stride]; } + + public getValue(timeSec: number): number { + // @TODO: Support range parameters like Outside + + // Remap (if requested) the time to our range + const t = this.range.getParameter(timeSec, this.getStartTime(), this.getEndTime()); + + // Update our current key. If the current time is between keys, select the later one. + this.curKeyIdx = this.keys.findIndex((k, i) => (i % this.stride) == 0 && k >= t) / this.stride; + + if (this.curKeyIdx == 0) { // Time is at or before the start, return the first key + return this.keys[this.curKeyIdx * this.stride + 1]; + } else if (this.curKeyIdx < 0) { // Time is at or after the end, return the last key + this.curKeyIdx = this.keyCount - 1; + return this.keys[this.curKeyIdx * this.stride + 1]; + } + + const ks = this.keys; + const c = this.curKeyIdx * this.stride; + const l = c - this.stride; + const value = Attribute.Interpolate.Hermite( + t, ks[l + 0], ks[l + 1], ks[l + this.stride - 1], ks[c + 0], ks[c + 1], ks[c + 2]); + + if (isNaN(value)) { + console.warn('NaN generated by FunctionValue'); + debugger; + } + + return value; + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// STB Parsing +//---------------------------------------------------------------------------------------------------------------------- +const BLOCK_TYPE_CONTROL = "ÿÿÿÿ"; // -1 represented as a fourcc + +enum ESequenceCmd { + End = 0, + SetFlag = 1, + Wait = 2, + Skip = 3, + Suspend = 4, + Paragraph = 0x80, +} + +enum EStatus { + Still = 0, + End = 1 << 0, + Wait = 1 << 1, + Suspend = 1 << 2, + Inactive = 1 << 3, +} + +export abstract class TBlockObject { + size: number; + type: string; // char[4] JMSG, JSND, JACT, ... + flag: number; + id: string; + data: Reader; +} + +// This combines JStudio::TControl and JStudio::stb::TControl into a single class, for simplicity. +export class TControl { + public system: TSystem; + public fvbControl = new FVB.TControl(); + public secondsPerFrame: number = 1 / 30.0; + private suspendFrames: number; + + public transformOrigin: vec3 | null = null; + public transformRotY: number | null = null; + private transformOnGetMtx = mat4.create(); + private transformOnSetMtx = mat4.create(); + + private status: EStatus = EStatus.Still; + private objects: STBObject[] = []; + + // 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) { + this.system = system; + } + + public isSuspended() { return this.suspendFrames > 0; } + public setSuspend(frameCount: number) { return this.controlObject.setSuspend(frameCount); } + + public isTransformEnabled() { return !!this.transformOrigin; } + public getTransformOnSet() { return this.transformOnSetMtx; } + public getTransformOnGet() { return this.transformOnGetMtx; } + public transformSetOrigin(originPos: vec3, rotY: number) { + this.transformOrigin = originPos; + this.transformRotY = rotY; + + // The "OnGet" matrix transforms from world space into demo space + mat4.fromYRotation(this.transformOnGetMtx, -rotY); + mat4.translate(this.transformOnGetMtx, this.transformOnGetMtx, vec3.negate(scratchVec3a, originPos)); + + // The "OnSet" matrix is the inverse + mat4.fromTranslation(this.transformOnSetMtx, originPos); + mat4.rotateY(this.transformOnSetMtx, this.transformOnSetMtx, rotY); + } + + public setControlObject(obj: TBlockObject) { + this.controlObject.reset(obj); + } + + public forward(frameCount: number): boolean { + ; + let andStatus = 0xFF; + let orStatus = 0; + + this.suspendFrames = this.controlObject.getSuspendFrames(); + let shouldContinue = this.controlObject.forward(frameCount); + + for (let obj of this.objects) { + const res = obj.forward(frameCount); + shouldContinue ||= res; + + const objStatus = obj.getStatus(); + andStatus &= objStatus; + orStatus |= objStatus; + } + + this.status = (andStatus | (orStatus << 0x10)); + return shouldContinue; + } + + 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 { + let objConstructor; + let objType: JStage.EObject; + switch (blockObj.type) { + case 'JCMR': objConstructor = TCameraObject; objType = JStage.EObject.Camera; break; + case 'JACT': objConstructor = TActorObject; objType = JStage.EObject.Actor; break; + case 'JABL': + case 'JLIT': + case 'JFOG': + default: + return null; + } + + const stageObj = this.system.JSGFindObject(blockObj.id, objType); + if (!stageObj) { + return null; + } + + const obj = new objConstructor(this, blockObj, stageObj); + obj.adaptor.adaptor_do_prepare(obj); + this.objects.push(obj); + return obj; + } + + public destroyObject_all() { + this.objects = []; + this.fvbControl.destroyObject_all(); + } +} + +export class TParse { + constructor( + private control: TControl, + private fvbParse = new FVB.TParse(control.fvbControl) + ) { } + + // Parse an entire scene's worth of object sequences at once + private parseBlockObject(file: Reader, flags: number) { + const blockObj: TBlockObject = { + size: file.view.getUint32(0), + type: readString(file.buffer, file.offset + 4, 4), + flag: file.view.getUint16(8), + id: readString(file.buffer, 12, file.view.getUint16(10)), + data: file + } + + if (blockObj.type == BLOCK_TYPE_CONTROL) { + this.control.setControlObject(blockObj); + return true; + } + + if (flags & 0x10) { + console.debug('Unhandled flag during parseBlockObject: 0x10'); + return true; + } + + if (flags & 0x20) { + console.debug('Unhandled flag during parseBlockObject: 0x20'); + return true; + } + + const obj = this.control.createObject(blockObj); + if (!obj) { + if (flags & 0x40) { + console.debug('Unhandled flag during parseBlockObject: 0x40'); + return true; + } + console.debug('Unhandled STB block type: ', blockObj.type); + return false; + } + + return true; + } + + // Parse all the TBlocks from an STB file. Blocks can either contain STBObjects, or FVB (function value) data. + // All objects will be created, they can be modified by using TControl. + public parse(data: ArrayBufferSlice, flags: number) { + const file = new JSystemFileReaderHelper(data); + + // Parse the THeader + let byteOrder = file.view.getUint16(0x04); + let version = file.view.getUint16(0x06); + let targetVersion = file.view.getUint16(0x1E); + assert(file.magic === 'STB'); + assert(version >= 1 && version <= 3); // As of Wind Waker, only versions 1-3 supported. TP seems to support <7, but untested. + assert(targetVersion >= 2 && targetVersion <= 3); // As of Wind Waker, only version 2-3 is supported + assert(byteOrder == 0xFEFF); + + let byteIdx = file.offs; + for (let i = 0; i < file.numChunks; i++) { + const blockSize = file.view.getUint32(byteIdx + 0); + const blockType = readString(file.buffer, byteIdx + 4, 4); + + if (blockType == 'JFVB') { + this.fvbParse.parse(file.buffer.subarray(byteIdx + 8, blockSize - 8), flags) + } else { + this.parseBlockObject(new Reader(file.buffer, byteIdx), flags); + } + + byteIdx += blockSize; + } + + return true; + } +} \ No newline at end of file diff --git a/src/ZeldaWindWaker/Grass.ts b/src/ZeldaWindWaker/Grass.ts index 4b4d9f02a..ce55e61f4 100644 --- a/src/ZeldaWindWaker/Grass.ts +++ b/src/ZeldaWindWaker/Grass.ts @@ -123,8 +123,8 @@ function checkGroundY(globals: dGlobals, roomIdx: number, pos: vec3) { } function setColorFromRoomNo(globals: dGlobals, materialParams: MaterialParams, roomNo: number): void { - colorCopy(materialParams.u_Color[ColorKind.C0], globals.roomStatus[roomNo].tevStr.colorC0); - colorCopy(materialParams.u_Color[ColorKind.C1], globals.roomStatus[roomNo].tevStr.colorK0); + colorCopy(materialParams.u_Color[ColorKind.C0], globals.roomCtrl.status[roomNo].tevStr.colorC0); + colorCopy(materialParams.u_Color[ColorKind.C1], globals.roomCtrl.status[roomNo].tevStr.colorK0); } function distanceCull(camPos: ReadonlyVec3, objPos: ReadonlyVec3, maxDist = 20000) { @@ -397,7 +397,7 @@ export class FlowerPacket { } private drawRoom(globals: dGlobals, roomIdx: number, renderInstManager: GfxRenderInstManager, viewerInput: ViewerRenderInput): void { - if (!globals.roomStatus[roomIdx].visible) + if (!globals.roomCtrl.status[roomIdx].visible) return; if (this.rooms[roomIdx].length === 0) @@ -719,7 +719,7 @@ export class TreePacket { } private drawRoom(globals: dGlobals, roomIdx: number, renderInstManager: GfxRenderInstManager, viewerInput: ViewerRenderInput, device: GfxDevice) { - if (!globals.roomStatus[roomIdx].visible) + if (!globals.roomCtrl.status[roomIdx].visible) return; const room = this.rooms[roomIdx]; @@ -1016,7 +1016,7 @@ export class GrassPacket { } private drawRoom(globals: dGlobals, roomIdx: number, renderInstManager: GfxRenderInstManager, viewerInput: ViewerRenderInput): void { - if (!globals.roomStatus[roomIdx].visible) + if (!globals.roomCtrl.status[roomIdx].visible) return; const room = this.rooms[roomIdx]; diff --git a/src/ZeldaWindWaker/Main.ts b/src/ZeldaWindWaker/Main.ts index 738258759..182597c24 100644 --- a/src/ZeldaWindWaker/Main.ts +++ b/src/ZeldaWindWaker/Main.ts @@ -17,7 +17,7 @@ import { J3DModelInstance } from '../Common/JSYSTEM/J3D/J3DGraphBase.js'; import * as JPA from '../Common/JSYSTEM/JPA.js'; import { BTIData } from '../Common/JSYSTEM/JUTTexture.js'; import { dfRange } from '../DebugFloaters.js'; -import { getMatrixAxisZ, range } from '../MathHelpers.js'; +import { getMatrixAxisY, getMatrixAxisZ, MathConstants, range } from '../MathHelpers.js'; import { SceneContext } from '../SceneBase.js'; import { TextureMapping } from '../TextureHolder.js'; import { setBackbufferDescSimple, standardFullClearRenderPassDescriptor } from '../gfx/helpers/RenderGraphHelpers.js'; @@ -33,12 +33,13 @@ import { dDlst_2DStatic_c, d_a__RegisterConstructors } from './d_a.js'; import { d_a_sea } from './d_a_sea.js'; import { dBgS } from './d_bg.js'; import { dDlst_list_Set, dDlst_list_c } from './d_drawlist.js'; -import { dKankyo_create, dKy__RegisterConstructors, dKy_setLight, dKy_tevstr_init, dScnKy_env_light_c } from './d_kankyo.js'; +import { dKankyo_create, dKy__RegisterConstructors, dKy_setLight, dScnKy_env_light_c } from './d_kankyo.js'; import { dKyw__RegisterConstructors } from './d_kankyo_wether.js'; import { dPa_control_c } from './d_particle.js'; import { ResType, dRes_control_c } from './d_resorce.js'; -import { dStage_dt_c_roomLoader, dStage_dt_c_roomReLoader, dStage_dt_c_stageInitLoader, dStage_dt_c_stageLoader, dStage_roomStatus_c, dStage_stageDt_c } from './d_stage.js'; +import { dStage_dt_c_roomLoader, dStage_dt_c_roomReLoader, dStage_dt_c_stageInitLoader, dStage_dt_c_stageLoader, dStage_roomControl_c, dStage_roomStatus_c, dStage_stageDt_c } from './d_stage.js'; import { cPhs__Status, fGlobals, fopAcM_create, fopAc_ac_c, fopDw_Draw, fopScn, fpcCt_Handler, fpcLy_SetCurrentLayer, fpcM_Management, fpcPf__Register, fpcSCtRq_Request, fpc__ProcessName, fpc_pc__ProfileList } from './framework.js'; +import { dDemo_manager_c, EDemoCamFlags, EDemoMode } from './d_demo.js'; type SymbolData = { Filename: string, SymbolName: string, Data: ArrayBufferSlice }; type SymbolMapData = { SymbolData: SymbolData[] }; @@ -125,7 +126,7 @@ export class dGlobals { // This is tucked away somewhere in dComInfoPlay public stageName: string; public dStage_dt = new dStage_stageDt_c(); - public roomStatus: dStage_roomStatus_c[] = nArray(64, () => new dStage_roomStatus_c()); + public roomCtrl = new dStage_roomControl_c(); public particleCtrl: dPa_control_c; public scnPlay: d_s_play; @@ -159,11 +160,6 @@ export class dGlobals { this.relNameTable = createRelNameTable(extraSymbolData); this.objectNameTable = createActorTable(extraSymbolData); - for (let i = 0; i < this.roomStatus.length; i++) { - this.roomStatus[i].roomNo = i; - dKy_tevstr_init(this.roomStatus[i].tevStr, i); - } - this.quadStatic = new dDlst_2DStatic_c(modelCache.device, modelCache.cache); this.dlst = new dDlst_list_c(modelCache.device, modelCache.cache, extraSymbolData); @@ -317,6 +313,8 @@ const enum EffectDrawGroup { } const scratchMatrix = mat4.create(); +const scratchVec3a = vec3.create(); +const scratchVec3b = vec3.create(); export class WindWakerRenderer implements Viewer.SceneGfx { private mainColorDesc = new GfxrRenderTargetDescription(GfxFormat.U8_RGBA_RT); private mainDepthDesc = new GfxrRenderTargetDescription(GfxFormat.D32F); @@ -408,7 +406,7 @@ export class WindWakerRenderer implements Viewer.SceneGfx { if (ac.roomNo === -1) return null; - return this.globals.roomStatus[ac.roomNo]; + return this.globals.roomCtrl.status[ac.roomNo]; } private getSingleRoomVisible(): number { @@ -470,6 +468,37 @@ export class WindWakerRenderer implements Viewer.SceneGfx { viewerInput.camera.setClipPlanes(nearPlane, farPlane); + // noclip modification: if we're paused, allow noclip camera control during demos + const isPaused = viewerInput.deltaTime === 0; + + // TODO: Determine the correct place for this + // dCamera_c::Store() sets the camera params if the demo camera is active + const demoCam = this.globals.scnPlay.demo.getSystem().getCamera(); + if (demoCam && !isPaused) { + let viewPos = this.globals.cameraPosition; + let targetPos = vec3.add(scratchVec3a, this.globals.cameraPosition, this.globals.cameraFwd); + let upVec = vec3.set(scratchVec3b, 0, 1, 0); + let roll = 0.0; + + if(demoCam.flags & EDemoCamFlags.HasTargetPos) { targetPos = demoCam.targetPosition; } + if(demoCam.flags & EDemoCamFlags.HasEyePos) { viewPos = demoCam.viewPosition; } + if(demoCam.flags & EDemoCamFlags.HasUpVec) { upVec = demoCam.upVector; } + if(demoCam.flags & EDemoCamFlags.HasFovY) { viewerInput.camera.fovY = demoCam.fovY * MathConstants.DEG_TO_RAD; } + if(demoCam.flags & EDemoCamFlags.HasRoll) { roll = demoCam.roll * MathConstants.DEG_TO_RAD; } + if(demoCam.flags & EDemoCamFlags.HasAspect) { debugger; /* Untested. Remove once confirmed working */ } + if(demoCam.flags & EDemoCamFlags.HasNearZ) { viewerInput.camera.near = demoCam.projNear; } + if(demoCam.flags & EDemoCamFlags.HasFarZ) { viewerInput.camera.far = demoCam.projFar; } + + mat4.targetTo(viewerInput.camera.worldMatrix, viewPos, targetPos, upVec); + mat4.rotateZ(viewerInput.camera.worldMatrix, viewerInput.camera.worldMatrix, roll); + viewerInput.camera.setClipPlanes(viewerInput.camera.near, viewerInput.camera.far); + viewerInput.camera.worldMatrixUpdated(); + + this.globals.context.inputManager.isMouseEnabled = false; + } else { + this.globals.context.inputManager.isMouseEnabled = true; + } + this.globals.camera = viewerInput.camera; // Not sure exactly where this is ordered... @@ -736,6 +765,7 @@ export const pathBase = `ZeldaWindWaker`; class d_s_play extends fopScn { public bgS = new dBgS(); + public demo: dDemo_manager_c; public flowerPacket: FlowerPacket; public treePacket: TreePacket; @@ -747,6 +777,8 @@ class d_s_play extends fopScn { public override load(globals: dGlobals, userData: any): cPhs__Status { super.load(globals, userData); + this.demo = new dDemo_manager_c(globals); + this.treePacket = new TreePacket(globals); this.flowerPacket = new FlowerPacket(globals); this.grassPacket = new GrassPacket(globals); @@ -757,6 +789,15 @@ class d_s_play extends fopScn { return cPhs__Status.Complete; } + public override execute(globals: dGlobals, deltaTimeFrames: number): void { + this.demo.update(); + + // From executeEvtManager() -> SpecialProcPackage() + if (this.demo.getMode() == EDemoMode.Ended) { + this.demo.remove(); + } + } + public override draw(globals: dGlobals, renderInstManager: GfxRenderInstManager, viewerInput: Viewer.ViewerRenderInput): void { super.draw(globals, renderInstManager, viewerInput); @@ -794,6 +835,7 @@ class d_s_play extends fopScn { class SceneDesc { public id: string; + protected globals: dGlobals; public constructor(public stageDir: string, public name: string, public roomList: number[] = [0]) { this.id = stageDir; @@ -844,6 +886,7 @@ class SceneDesc { const symbolMap = new SymbolMap(modelCache.getFileData(`extra.crg1_arc`)); const globals = new dGlobals(context, modelCache, symbolMap, framework); + this.globals = globals; globals.stageName = this.stageDir; const renderer = new WindWakerRenderer(device, globals); @@ -891,6 +934,8 @@ class SceneDesc { // dStage_dt_c_stageLoader() // dMap_c::create() + globals.roomCtrl.demoArcName = null; + const vrbox = resCtrl.getStageResByName(ResType.Model, `Stage`, `vr_sky.bdl`); if (vrbox !== null) { fpcSCtRq_Request(framework, null, fpc__ProcessName.d_a_vrbox, null); @@ -901,7 +946,7 @@ class SceneDesc { const roomNo = Math.abs(this.roomList[i]); const visible = this.roomList[i] >= 0; - const roomStatus = globals.roomStatus[i]; + const roomStatus = globals.roomCtrl.status[i]; roomStatus.visible = visible; renderer.rooms.push(new WindWakerRoom(roomNo, roomStatus)); @@ -911,14 +956,147 @@ class SceneDesc { fopAcM_create(framework, fpc__ProcessName.d_a_bg, roomNo, null, roomNo, null, null, 0xFF, -1); const dzr = assertExists(resCtrl.getStageResByName(ResType.Dzs, `Room${roomNo}`, `room.dzr`)); - dStage_dt_c_roomLoader(globals, globals.roomStatus[roomNo], dzr); - dStage_dt_c_roomReLoader(globals, globals.roomStatus[roomNo], dzr); + dStage_dt_c_roomLoader(globals, globals.roomCtrl.status[roomNo].data, dzr); + dStage_dt_c_roomReLoader(globals, globals.roomCtrl.status[roomNo].data, dzr); } return renderer; } } +class DemoDesc extends SceneDesc implements Viewer.SceneDesc { + private scene: SceneDesc; + + public constructor( + public override stageDir: string, + public override name: string, + public override roomList: number[], + public stbFilename: string, + public layer: number, + public offsetPos?:vec3, + public rotY: number = 0, + public startCode?: number, + public eventFlags?: number, + public startFrame?: number, // noclip modification for easier debugging + ) { + super(stageDir, name, roomList); + assert(this.roomList.length === 1); + } + + public override async createScene(device: GfxDevice, context: SceneContext): Promise { + const res = await super.createScene(device, context); + this.playDemo(this.globals); + return res; + } + + async playDemo(globals: dGlobals) { + globals.scnPlay.demo.remove(); + + // TODO: Don't render until the camera has been placed for this demo. The cuts are jarring. + + // noclip modification: This normally happens on room load. Do it here instead so that we don't waste time + // loading .arcs for cutscenes that aren't going to be played + const lbnk = globals.roomCtrl.status[this.roomList[0]].data.lbnk; + if (lbnk) { + const bank = lbnk[this.layer]; + if (bank != 0xFF) { + assert(bank >= 0 && bank < 100); + globals.roomCtrl.demoArcName = `Demo${bank.toString().padStart(2, '0')}`; + console.debug(`Loading stage demo file: ${globals.roomCtrl.demoArcName}`); + + globals.modelCache.fetchObjectData(globals.roomCtrl.demoArcName).catch(e => { + // @TODO: Better error handling. This does not prevent a debugger break. + console.log(`Failed to load stage demo file: ${globals.roomCtrl.demoArcName}`, e); + }) + + await globals.modelCache.waitForLoad(); + } + } + + // noclip modification: ensure all the actors are created before we load the cutscene + await new Promise(resolve => { (function waitForActors(){ + if (globals.frameworkGlobals.ctQueue.length == 0) return resolve(null); + setTimeout(waitForActors, 30); + })(); }); + + // @TODO: Set noclip layer visiblity based on this.layer + + // From dEvDtStaff_c::specialProcPackage() + let demoData; + if(globals.roomCtrl.demoArcName) + demoData = globals.modelCache.resCtrl.getObjectResByName(ResType.Stb, globals.roomCtrl.demoArcName, this.stbFilename); + if (!demoData) + demoData = globals.modelCache.resCtrl.getStageResByName(ResType.Stb, "Stage", this.stbFilename); + + if( demoData ) { globals.scnPlay.demo.create(demoData, this.offsetPos, this.rotY / 180.0 * Math.PI, this.startFrame); } + else { console.warn('Failed to load demo data:', this.stbFilename); } + + return this.globals.renderer; + } +} + +// Move these into sceneDescs once they are complete enough to be worth viewing. +// Extracted from the game using a modified version of the excellent wwrando/wwlib/stage_searcher.py script from LagoLunatic +// Most of this data comes from the PLAY action of the PACKAGE actor in an event from a Stage's event_list.dat. This +// action has properties for the room and layer that these cutscenes are designed for. HOWEVER, this data is often missing. +// It has been reconstructed by cross-referencing each Room's lbnk section (which points to a Demo*.arc file for each layer), +// the .stb files contained in each of those Objects/Demo*.arc files, and the FileName attribute from the event action. +const demoDescs = [ + new DemoDesc("sea", "Stolen Sister", [44], "stolensister.stb", 9, [0.0, 0.0, 20000.0], 0, 0, 0), + new DemoDesc("sea", "Departure", [44], "departure.stb", 10, [-200000.0, 0.0, 320000.0], 0.0, 204, 0), + new DemoDesc("sea", "Pirate Zelda Fly", [44], "kaizoku_zelda_fly.stb", 0, [-200000.0, 0.0, 320000.0], 180.0, 0, 0), + new DemoDesc("sea", "Zola Awakens", [13], "awake_zola.stb", 8, [200000.0, 0.0, -200000.0], 0, 227, 0), + new DemoDesc("sea", "Get Komori Pearl", [13], "getperl_komori.stb", 9, [200000.0, 0.0, -200000.0], 0, 0, 0), + new DemoDesc("sea", "Meet the King of Red Lions", [11], "meetshishioh.stb", 8, [0.0, 0.0, -200000.0], 0, 128, 0), + new DemoDesc("sea", "Fairy", [9], "fairy.stb", 8, [-180000.0, 740.0, -199937.0], 25.0, 2, 0), + new DemoDesc("sea", "fairy_flag_on.stb", [9], "fairy_flag_on.stb", 8, [-180000.0, 740.0, -199937.0], 25.0, 2, 0), + new DemoDesc("ADMumi", "warp_in.stb", [0], "warp_in.stb", 9, [0.0, 0.0, 0.0], 0.0, 200, 0), + new DemoDesc("ADMumi", "warphole.stb", [0], "warphole.stb", 10, [0.0, 0.0, 0.0], 0.0, 219, 0), + new DemoDesc("ADMumi", "runaway_majuto.stb", [0], "runaway_majuto.stb", 11, [0, 0, 0], 0, 0, 0), + new DemoDesc("ADMumi", "towerd.stb", [0], "towerd.stb", 8, [-50179.0, -1000.0, 7070.0], 90.0, 0, 0), + new DemoDesc("ADMumi", "towerf.stb", [0], "towerf.stb", 8, [-50179.0, -1000.0, 7070.0], 90.0, 0, 0), + new DemoDesc("ADMumi", "towern.stb", [0], "towern.stb", 8, [-50179.0, -1000.0, 7070.0], 90.0, 0, 0), + new DemoDesc("A_mori", "meet_tetra.stb", [0], "meet_tetra.stb", 0, [0, 0, 0], 0, 0, 0), + new DemoDesc("Adanmae", "howling.stb", [0], "howling.stb", 8, [0.0, 0.0, 0.0], 0.0, 0, 0), + new DemoDesc("Atorizk", "dragontale.stb", [0], "dragontale.stb", 0, [0, 0, 0], 0, 0, 0), + new DemoDesc("ENDumi", "ending.stb", [0], "ending.stb", 8, [0.0, 0.0, 0.0], 0.0, 0, 0), + new DemoDesc("Edaichi", "dance_zola.stb", [0], "dance_zola.stb", 8, [0.0, 0.0, 0.0], 0, 226, 0), + new DemoDesc("Ekaze", "dance_kokiri.stb", [0], "dance_kokiri.stb", 8, [0.0, 0.0, 0.0], 0, 229, 0), + new DemoDesc("GTower", "g2before.stb", [0], "g2before.stb", 8, [0.0, 0.0, 0.0], 0.0, 0, 0), + new DemoDesc("GTower", "endhr.stb", [0], "endhr.stb", 9, [0.0, 0.0, 0.0], 0.0, 0, 0), + new DemoDesc("GanonK", "kugutu_ganon.stb", [0], "kugutu_ganon.stb", 8, [0.0, 0.0, 0.0], 0.0, 0, 0), + new DemoDesc("GanonK", "to_roof.stb", [0], "to_roof.stb", 9, [0.0, 0.0, 0.0], 0.0, 4, 0), + new DemoDesc("Hyroom", "rebirth_hyral.stb", [0], "rebirth_hyral.stb", 8, [0.0, -2100.0, 3910.0], 0.0, 249, 0), + new DemoDesc("Hyrule", "warp_out.stb", [0], "warp_out.stb", 8, [0, 0, 0], 0, 201, 0), + new DemoDesc("Hyrule", "seal.stb", [0], "seal.stb", 4, [0.0, 0.0, 0.0], 0, 0, 0), + new DemoDesc("LinkRM", "tale.stb", [0], "tale.stb", 8, [0, 0, 0], 0, 0, 0), + new DemoDesc("LinkRM", "tale_2.stb", [0], "tale_2.stb", 8, [0, 0, 0], 0, 0, 0), + new DemoDesc("LinkRM", "get_shield.stb", [0], "get_shield.stb", 9, [0, 0, 0], 0, 201, 0), + new DemoDesc("LinkRM", "tale_2.stb", [0], "tale_2.stb", 8, [0, 0, 0], 0, 0, 0), + new DemoDesc("M2ganon", "attack_ganon.stb", [0], "attack_ganon.stb", 1, [2000.0, 11780.0, -8000.0], 0.0, 244, 0), + new DemoDesc("M2tower", "rescue.stb", [0], "rescue.stb", 1, [3214.0, 3939.0, -3011.0], 57.5, 20, 0), + new DemoDesc("M_DaiB", "pray_zola.stb", [0], "pray_zola.stb", 8, [0, 0, 0], 0, 229, 0), + new DemoDesc("MajyuE", "maju_shinnyu.stb", [0], "maju_shinnyu.stb", 0, [0, 0, 0], 0, 0, 0), + new DemoDesc("Mjtower", "find_sister.stb", [0], "find_sister.stb", 0, [4889.0, 0.0, -2635.0], 57.5, 0, 0), + new DemoDesc("Obombh", "bombshop.stb", [0], "bombshop.stb", 0, [0.0, 0.0, 0.0], 0.0, 200, 0), + new DemoDesc("Omori", "getperl_deku.stb", [0], "getperl_deku.stb", 9, [0, 0, 0], 0, 214, 0), + new DemoDesc("Omori", "meet_deku.stb", [0], "meet_deku.stb", 8, [0, 0, 0], 0, 213, 0), + new DemoDesc("Otkura", "awake_kokiri.stb", [0], "awake_kokiri.stb", 8, [0, 0, 0], 0, 0, 0), + new DemoDesc("Pjavdou", "getperl_jab.stb", [0], "getperl_jab.stb", 8, [0.0, 0.0, 0.0], 0.0, 0, 0), + new DemoDesc("kazeB", "pray_kokiri.stb", [0], "pray_kokiri.stb", 8, [0.0, 300.0, 0.0], 0.0, 232, 0), + new DemoDesc("kenroom", "awake_zelda.stb", [0], "awake_zelda.stb", 9, [0.0, 0.0, 0.0], 0.0, 2, 0), + new DemoDesc("kenroom", "master_sword.stb", [0], "master_sword.stb", 0, [-124.0, -3223.0, -7823.0], 180.0, 248, 0), + new DemoDesc("kenroom", "swing_sword.stb", [0], "swing_sword.stb", 10, [-124.0, -3223.0, -7823.0], 180.0, 249, 0), + + // These are present in the sea_T event_list.dat, but not in the room's lbnk. They are only playable from "sea". + new DemoDesc("sea_T", "Awaken", [0xFF], "awake.stb", 0, [-220000.0, 0.0, 320000.0], 0.0, 0, 0), + new DemoDesc("sea_T", "Departure", [0xFF], "departure.stb", 0, [-200000.0, 0.0, 320000.0], 0.0, 0, 0), + new DemoDesc("sea_T", "PirateZeldaFly", [0xFF], "kaizoku_zelda_fly.stb", 0, [-200000.0, 0.0, 320000.0], 180.0, 0, 0), + + // The game expects this STB file to be in Stage/Ocean/Stage.arc, but it is not. Must be a leftover. + new DemoDesc("Ocean", "counter.stb", [-1], "counter.stb", 0, [0, 0, 0], 0, 0, 0), +] + // Location names taken from CryZe's Debug Menu. // https://github.com/CryZe/WindWakerDebugMenu/blob/master/src/warp_menu/consts.rs const sceneDescs = [ @@ -941,7 +1119,12 @@ const sceneDescs = [ new SceneDesc("PShip", "Ghost Ship"), new SceneDesc("Obshop", "Beedle's Shop", [1]), + "Cutscenes", + new DemoDesc("sea_T", "Title Screen", [44], "title.stb", 0, [-220000.0, 0.0, 320000.0], 180.0, 0, 0), + new DemoDesc("sea", "Awaken", [44], "awake.stb", 0, [-220000.0, 0.0, 320000.0], 0.0, 0, 0), + "Outset Island", + new SceneDesc("sea_T", "Title Screen", [44]), new SceneDesc("sea", "Outset Island", [44]), new SceneDesc("LinkRM", "Link's House"), new SceneDesc("LinkUG", "Under Link's House"), diff --git a/src/ZeldaWindWaker/d_a.ts b/src/ZeldaWindWaker/d_a.ts index 2493bb69d..553109dda 100644 --- a/src/ZeldaWindWaker/d_a.ts +++ b/src/ZeldaWindWaker/d_a.ts @@ -1,5 +1,5 @@ -import { ReadonlyVec3, mat4, quat, vec2, vec3 } from "gl-matrix"; +import { ReadonlyMat4, ReadonlyVec3, mat4, quat, vec2, vec3 } from "gl-matrix"; import { TransparentBlack, colorCopy, colorFromRGBA8, colorNewCopy, colorNewFromRGBA8 } from "../Color.js"; import { J3DModelData, J3DModelInstance, buildEnvMtx } from "../Common/JSYSTEM/J3D/J3DGraphBase.js"; import { LoopMode, TRK1, TTK1 } from "../Common/JSYSTEM/J3D/J3DLoader.js"; @@ -29,10 +29,11 @@ import { dPa_splashEcallBack, dPa_trackEcallBack, dPa_waveEcallBack } from "./d_ import { ResType, dComIfG_resLoad } from "./d_resorce.js"; import { dPath, dPath_GetRoomPath, dPath__Point, dStage_Multi_c, dStage_stagInfo_GetSTType } from "./d_stage.js"; import { cPhs__Status, fGlobals, fopAcIt_JudgeByID, fopAcM_create, fopAcM_prm_class, fopAc_ac_c, fpcPf__Register, fpcSCtRq_Request, fpc__ProcessName, fpc_bs__Constructor } from "./framework.js"; -import { mDoExt_McaMorf, mDoExt_bckAnm, mDoExt_brkAnm, mDoExt_btkAnm, mDoExt_modelEntryDL, mDoExt_modelUpdateDL, mDoLib_project } from "./m_do_ext.js"; +import { mDoExt_McaMorf, mDoExt_bckAnm, mDoExt_brkAnm, mDoExt_btkAnm, mDoExt_btpAnm, mDoExt_modelEntryDL, mDoExt_modelUpdateDL, mDoLib_project } from "./m_do_ext.js"; import { MtxPosition, MtxTrans, calc_mtx, mDoMtx_XYZrotM, mDoMtx_XrotM, mDoMtx_YrotM, mDoMtx_YrotS, mDoMtx_ZXYrotM, mDoMtx_ZrotM, mDoMtx_ZrotS, quatM } from "./m_do_mtx.js"; import { dGlobals } from "./Main.js"; import { dDlst_alphaModel__Type } from "./d_drawlist.js"; +import { dDemo_setDemoData } from "./d_demo.js"; // Framework'd actors @@ -54,7 +55,7 @@ class d_a_grass extends fopAc_ac_c { { group: 5, count: 7 }, { group: 6, count: 5 }, ]; - + static kSpawnOffsets: vec3[][] = [ [ [0, 0, 0], @@ -163,7 +164,7 @@ class d_a_grass extends fopAc_ac_c { const pos = vec3.add(scratchVec3a, offset, this.pos); globals.scnPlay.grassPacket.newData(pos, this.roomNo, itemIdx); } - break; + break; case FoliageType.Tree: const rotation = mat4.fromYRotation(scratchMat4a, this.rot[1] / 0x7FFF * Math.PI); @@ -173,7 +174,7 @@ class d_a_grass extends fopAc_ac_c { const pos = vec3.add(scratchVec3b, offset, this.pos); globals.scnPlay.treePacket.newData(pos, 0, this.roomNo); } - break; + break; case FoliageType.WhiteFlower: case FoliageType.PinkFlower: @@ -185,7 +186,7 @@ class d_a_grass extends fopAc_ac_c { const pos = vec3.add(scratchVec3a, offset, this.pos); globals.scnPlay.flowerPacket.newData(globals, pos, isPink, this.roomNo, itemIdx); } - break; + break; default: console.warn('Unknown grass actor type'); } @@ -426,10 +427,10 @@ class d_a_bg extends fopAc_ac_c { const roomNo = this.parameters; const arcName = `Room` + roomNo; - const modelName = ['model.bmd', 'model1.bmd', 'model2.bmd', 'model3.bmd']; + const modelName = ['model.bmd', 'model1.bmd', 'model2.bmd', 'model3.bmd']; const modelName2 = ['model.bdl', 'model1.bdl', 'model2.bdl', 'model3.bdl']; - const btkName = ['model.btk', 'model1.btk', 'model2.btk', 'model3.btk']; - const brkName = ['model.brk', 'model1.brk', 'model2.brk', 'model3.brk']; + const btkName = ['model.btk', 'model1.btk', 'model2.btk', 'model3.btk']; + const brkName = ['model.brk', 'model1.brk', 'model2.brk', 'model3.brk']; // createHeap for (let i = 0; i < this.numBg; i++) { @@ -475,7 +476,7 @@ class d_a_bg extends fopAc_ac_c { mat4.copy(this.bgModel[i]!.modelMatrix, calc_mtx); } - dKy_tevstr_init(globals.roomStatus[roomNo].tevStr, roomNo, -1); + dKy_tevstr_init(globals.roomCtrl.status[roomNo].tevStr, roomNo, -1); return cPhs__Status.Next; } @@ -507,7 +508,7 @@ class d_a_bg extends fopAc_ac_c { } const roomNo = this.parameters; - settingTevStruct(globals, LightType.BG0, null, globals.roomStatus[roomNo].tevStr); + settingTevStruct(globals, LightType.BG0, null, globals.roomCtrl.status[roomNo].tevStr); } public override delete(globals: dGlobals): void { @@ -608,7 +609,7 @@ class d_a_vrbox extends fopAc_ac_c { return; let skyboxOffsY = 0; - const fili = globals.roomStatus[globals.mStayNo].fili; + const fili = globals.roomCtrl.status[globals.mStayNo].data.fili; if (fili !== null) skyboxOffsY = fili.skyboxY; @@ -725,7 +726,7 @@ class d_a_vrbox2 extends fopAc_ac_c { return; let skyboxOffsY = 0; - const fili = globals.roomStatus[globals.mStayNo].fili; + const fili = globals.roomCtrl.status[globals.mStayNo].data.fili; if (fili !== null) skyboxOffsY = fili.skyboxY; @@ -2443,7 +2444,7 @@ class d_a_tori_flag extends fopAc_ac_c { const toonTex = resCtrl.getObjectRes(ResType.Bti, d_a_tori_flag.arcNameCloth, 0x03); const flagTex = resCtrl.getObjectRes(ResType.Bti, d_a_tori_flag.arcName, 0x07); this.cloth = new dCloth_packet_c(toonTex, flagTex, 5, 5, 210.0, 105.0, this.clothTevStr); - + vec3.copy(this.windvec, dKyw_get_wind_vec(globals.g_env_light)); this.set_mtx(); @@ -2593,9 +2594,9 @@ class d_a_majuu_flag extends fopAc_ac_c { for (let i = 0; i < this.pointCount; i++) { const dst = this.posArr[0][i]; - const x = posData[i*3+0]; - const y = posData[i*3+1]; - const z = posData[i*3+2]; + const x = posData[i * 3 + 0]; + const y = posData[i * 3 + 1]; + const z = posData[i * 3 + 2]; vec3.set(dst, x, y, z); if (!this.isPointFixed(i)) { @@ -2873,7 +2874,6 @@ class d_a_majuu_flag extends fopAc_ac_c { private majuu_flag_move(globals: dGlobals, deltaTimeFrames: number): void { this.wave += this.waveSpeed * deltaTimeFrames; const windSpeed = lerp(this.windSpeed1, this.windSpeed2, Math.sin(cM_s2rad(this.wave)) * 0.5 + 0.5); - const windpow = dKyw_get_wind_pow(globals.g_env_light); vec3.set(scratchVec3a, 0, 0, windSpeed * windpow * 2.0); mDoMtx_ZrotS(calc_mtx, -this.rot[2]); @@ -2990,9 +2990,9 @@ class d_a_kamome extends fopAc_ac_c { if (status !== cPhs__Status.Complete) return status; - this.type = (this.parameters >>> 0x00) & 0xFF; - this.ko_count = (this.parameters >>> 0x08) & 0xFF; - this.path_arg = (this.parameters >>> 0x10) & 0xFF; + this.type = (this.parameters >>> 0x00) & 0xFF; + this.ko_count = (this.parameters >>> 0x08) & 0xFF; + this.path_arg = (this.parameters >>> 0x10) & 0xFF; this.switch_arg = (this.parameters >>> 0x18) & 0xFF; // createHeap @@ -4195,7 +4195,7 @@ class d_a_obj_flame extends fopAc_ac_c { this.extraScaleY = 1.0; const scale_xz = [1.0, 1.0, 1.0, 0.5][this.type]; - this.scaleY = [1.0, 0.815, 1.0, 0.5][this.type]; + this.scaleY = [1.0, 0.815, 1.0, 0.5][this.type]; this.scale[0] *= scale_xz; this.scale[1] *= this.extraScaleY * this.scaleY; this.scale[2] *= scale_xz; @@ -4204,17 +4204,17 @@ class d_a_obj_flame extends fopAc_ac_c { this.useSimpleEm = [true, false, false, false][this.type]; if (!this.useSimpleEm) { - const em01ScaleXZ = [-1, 13.0/3.0, 7.5, 0.5][this.type]; - const em01ScaleY = [-1, 2.716, 7.5, 0.5][this.type]; + const em01ScaleXZ = [-1, 13.0 / 3.0, 7.5, 0.5][this.type]; + const em01ScaleY = [-1, 2.716, 7.5, 0.5][this.type]; assert(em01ScaleXZ >= 0.0 && em01ScaleY >= 0.0); this.em01Scale = vec3.fromValues(em01ScaleXZ, this.extraScaleY * em01ScaleY, em01ScaleXZ); - const em2Scale = [-1, 0.866666, 1.0, 0.5][this.type]; + const em2Scale = [-1, 0.866666, 1.0, 0.5][this.type]; assert(em2Scale >= 0.0); this.em2Scale = vec3.fromValues(em2Scale, em2Scale, em2Scale); } - this.eyePosY = [1.0, 10.0/3.0, 7.5, 0.5][this.type]; + this.eyePosY = [1.0, 10.0 / 3.0, 7.5, 0.5][this.type]; this.bubblesParticleID = [0x805C, 0x808A, 0x808A, 0x805C][this.type]; // create_mode_init @@ -4711,6 +4711,248 @@ export class d_a_ff extends fopAc_ac_c { } } +class fopNpc_npc_c extends fopAc_ac_c { + protected morf: mDoExt_McaMorf; +}; +interface anm_prm_c { + anmIdx: number; + nextPrmIdx: number; + morf: number; + playSpeed: number; + loopMode: number; +}; + +function dNpc_setAnmIDRes(globals: dGlobals, pMorf: mDoExt_McaMorf, loopMode: number, morf: number, speed: number, animResId: number, arcName: string): boolean { + if (pMorf) { + const pAnimRes = globals.resCtrl.getObjectIDRes(ResType.Bck, arcName, animResId); + pMorf.setAnm(pAnimRes, loopMode, morf, speed, 0.0, -1.0); + return true; + } + return false; +} + +class d_a_npc_ls1 extends fopNpc_npc_c { + public static PROCESS_NAME = fpc__ProcessName.d_a_npc_ls1; + private type: number; + private state = 0; + private animIdx = -1; + private animStopped: boolean; + private animTime: number; + private idleCountdown: number; + private arcName = 'Ls'; + + private itemPosType: number = 1; + private itemScale: number = 1.0; + private itemModel: J3DModelInstance; + private handModel: J3DModelInstance; + private jointMtxHandL: ReadonlyMat4; + private jointMtxHandR: ReadonlyMat4; + + private btkAnim = new mDoExt_btkAnm(); + private btpAnim = new mDoExt_btpAnm(); + private btkFrame: number; + private btpFrame: number; + + private static bckIdxTable = [5, 6, 7, 8, 9, 10, 11, 2, 4, 3, 1, 1, 1, 0] + private static animParamsTable: anm_prm_c[] = [ + { anmIdx: 0xFF, nextPrmIdx: 0xFF, morf: 0.0, playSpeed: 0.0, loopMode: 0xFFFFFFFF, }, + { anmIdx: 0, nextPrmIdx: 1, morf: 8.0, playSpeed: 1.0, loopMode: 2, }, + { anmIdx: 5, nextPrmIdx: 1, morf: 8.0, playSpeed: 1.0, loopMode: 2, }, + { anmIdx: 10, nextPrmIdx: 2, morf: 8.0, playSpeed: 1.0, loopMode: 2, }, + { anmIdx: 5, nextPrmIdx: 1, morf: 8.0, playSpeed: 1.0, loopMode: 2, }]; + + public override subload(globals: dGlobals): cPhs__Status { + const success = this.decideType(this.parameters); + if (!success) { return cPhs__Status.Error; } + + let status = dComIfG_resLoad(globals, this.arcName); + if (status !== cPhs__Status.Complete) + return status; + + // noclip modification: Aryll's telescope comes from the Link arc. Load it now + status = dComIfG_resLoad(globals, "Link"); + if (status !== cPhs__Status.Complete) + return status; + + this.createModels(globals); + + this.cullMtx = this.morf.model.modelMatrix; + this.setCullSizeBox(-50.0, -20.0, -50.0, 50.0, 140.0, 50.0); + + this.createInit(globals); + + return cPhs__Status.Next; + } + + public override draw(globals: dGlobals, renderInstManager: GfxRenderInstManager, viewerInput: ViewerRenderInput): void { + super.draw(globals, renderInstManager, viewerInput); + + settingTevStruct(globals, LightType.Actor, this.pos, this.tevStr); + setLightTevColorType(globals, this.morf.model, this.tevStr, viewerInput.camera); + setLightTevColorType(globals, this.handModel, this.tevStr, viewerInput.camera); + + // this.btkAnim.entry(this.morf.model, this.btkFrame); + // this.btpAnim.entry(this.morf.model, this.btpFrame); + + this.morf.entryDL(globals, renderInstManager, viewerInput); + + mDoExt_modelEntryDL(globals, this.handModel, renderInstManager, viewerInput); + + if (this.itemModel) { + setLightTevColorType(globals, this.itemModel, this.tevStr, viewerInput.camera); + mDoExt_modelEntryDL(globals, this.itemModel, renderInstManager, viewerInput); + } + + this.drawShadow(); + } + + public override execute(globals: dGlobals, deltaTimeFrames: number): void { + if (true || this.demoActorID >= 0) { + const isDemo = this.demo(globals, deltaTimeFrames); + if (!isDemo) { + this.play_animation(deltaTimeFrames); + } + } + + // TODO: Shadowing based on the triangle that the NPC is standing on + // this.tevStr.roomNo = this.roomNo; + // bVar2 = dBgS::GetPolyColor(&d_com_inf_game::g_dComIfG_gameInfo.play.mBgS, + // &(this->parent).mObjAcch.parent.mGndChk.parent.mPolyInfo); + // (this->parent).parent.tevStr.mEnvrIdxOverride = bVar2; + + this.setMtx(false); + } + + private decideType(pcParam: number) { + const isEventBit0x2A80Set = false; + + this.type = 0xFF; + + // Outset Island has two Ls1 actors in layer 0. One is type 0, the other is type 3. Based on the status of + // dSv_event_c::isEventBit(&d_com_inf_game::g_dComIfG_gameInfo.info.mSavedata.mEvent, 0x2a80), only one is created. + switch (pcParam) { + case 0: if (isEventBit0x2A80Set) this.type = 0; break; + case 1: this.type = 1; break; + case 2: this.type = 2; break; + case 3: if (!isEventBit0x2A80Set) this.type = 3; break; + case 4: this.type = 4; break; + } + + return this.type != 0xFF; + } + + private createInit(globals: dGlobals) { + this.setAnim(globals, 2, false); + this.play_animation(1.0 / 30.0); + + this.tevStr.roomNo = this.roomNo + + this.morf.setMorf(0.0); + this.setMtx(true); + } + + private createModels(globals: dGlobals) { + this.createBody(globals); + this.createHand(globals); + this.createItem(globals); + } + + private createBody(globals: dGlobals) { + const modelData = globals.resCtrl.getObjectIDRes(ResType.Model, this.arcName, 0xd); + for (let i = 0; i < modelData.modelMaterialData.materialData!.length; i++) { + // Material anim setup + } + + this.morf = new mDoExt_McaMorf(modelData, null, null, null, LoopMode.Once, 1.0, 0, -1); + + const jointIdxHandL = modelData.bmd.jnt1.joints.findIndex(j => j.name == 'handL'); + const jointIdxHandR = modelData.bmd.jnt1.joints.findIndex(j => j.name == 'handR'); + this.jointMtxHandL = this.morf.model.shapeInstanceState.jointToWorldMatrixArray[jointIdxHandL]; + this.jointMtxHandR = this.morf.model.shapeInstanceState.jointToWorldMatrixArray[jointIdxHandR]; + } + + private createHand(globals: dGlobals) { + const modelData = globals.resCtrl.getObjectIDRes(ResType.Model, this.arcName, 0xc); + this.handModel = new J3DModelInstance(modelData); + + const handJointIdxL = modelData.bmd.jnt1.joints.findIndex(j => j.name == 'ls_handL'); + const handJointIdxR = modelData.bmd.jnt1.joints.findIndex(j => j.name == 'ls_handR'); + + this.handModel.jointMatrixCalcCallback = (dst: mat4, modelData: J3DModelData, i: number): void => { + if (i == handJointIdxL) { mat4.copy(dst, this.jointMtxHandL); } + else if (i == handJointIdxR) { mat4.copy(dst, this.jointMtxHandR); } + } + } + + private createItem(globals: dGlobals) { + const modelData = globals.resCtrl.getObjectIDRes(ResType.Model, "Link", 0x2f); + this.itemModel = new J3DModelInstance(modelData); + } + + private setAnim(globals: dGlobals, animIdx: number, hasTexAnim: boolean) { + if (hasTexAnim) { + // TODO: Facial texture animations + } + + const params = d_a_npc_ls1.animParamsTable[animIdx]; + + if (params.anmIdx > -1 && this.animIdx != params.anmIdx) { + const bckID = d_a_npc_ls1.bckIdxTable[params.anmIdx]; + dNpc_setAnmIDRes(globals, this.morf, params.loopMode, params.morf, params.playSpeed, bckID, this.arcName); + this.animIdx = params.anmIdx; + this.animStopped = false; + this.animTime = 0; + } + } + + private play_animation(deltaTimeFrames: number) { + // play_btp_anm(this); + // play_btk_anm(this); + + this.animStopped = this.morf.play(deltaTimeFrames); + if (this.morf.frameCtrl.currentTimeInFrames < this.animTime) { + this.animStopped = true; + } + this.animTime = this.morf.frameCtrl.currentTimeInFrames; + } + + private setMtx(param: boolean) { + vec3.copy(this.morf.model.baseScale, this.scale); + MtxTrans(this.pos, false, calc_mtx); + mDoMtx_ZXYrotM(calc_mtx, this.rot); + mat4.copy(this.morf.model.modelMatrix, calc_mtx); + this.morf.calc(); + + this.handModel.calcAnim(); + + if (this.itemModel) { + mat4.copy(calc_mtx, this.jointMtxHandR); + if (this.itemPosType == 0) { + MtxTrans([5.5, -3.0, -2.0], true); + } + else { + MtxTrans([5.7, -17.5, -1.0], true); + } + scaleMatrix(calc_mtx, calc_mtx, this.itemScale, this.itemScale, this.itemScale); + mDoMtx_XYZrotM(calc_mtx, [-0x1d27, 0x3b05, -0x5c71]); + mat4.copy(this.itemModel.modelMatrix, calc_mtx); + this.itemModel.calcAnim(); + } + } + + private drawShadow() { + // TODO + } + + private demo(globals: dGlobals, deltaTimeFrames: number) { + if (this.demoActorID < 0) { return false; } + + // TODO: Lots happening here + dDemo_setDemoData(globals, deltaTimeFrames, this, 0x6a, this.morf, this.arcName); + return true; + } +} + interface constructor extends fpc_bs__Constructor { PROCESS_NAME: fpc__ProcessName; } @@ -4742,4 +4984,5 @@ export function d_a__RegisterConstructors(globals: fGlobals): void { R(d_a_oship); R(d_a_obj_flame); R(d_a_ff); + R(d_a_npc_ls1); } diff --git a/src/ZeldaWindWaker/d_demo.ts b/src/ZeldaWindWaker/d_demo.ts new file mode 100644 index 000000000..f5c07aed2 --- /dev/null +++ b/src/ZeldaWindWaker/d_demo.ts @@ -0,0 +1,472 @@ +import { mat4, ReadonlyVec3, vec3 } from "gl-matrix"; +import ArrayBufferSlice from "../ArrayBufferSlice"; +import { TParse, JStage, TSystem, TControl, TCamera, TActor } from "../Common/JSYSTEM/JStudio.js"; +import { getMatrixAxisY, MathConstants } from "../MathHelpers.js"; +import { dGlobals } from "./Main"; +import { fopAc_ac_c, fopAcM_searchFromName } from "./framework.js"; +import { J3DModelInstance } from "../Common/JSYSTEM/J3D/J3DGraphBase"; +import { mDoExt_McaMorf } from "./m_do_ext"; +import { assert } from "../util.js"; +import { ResType } from "./d_resorce.js"; +import { LoopMode } from "../Common/JSYSTEM/J3D/J3DLoader"; +import { cM_deg2s, cM_sht2d } from "./SComponent.js"; + +export enum EDemoMode { + None, + Playing, + Ended +} + +export enum EDemoCamFlags { + HasNearZ = 1 << 0, + HasFarZ = 1 << 1, + HasFovY = 1 << 2, + HasAspect = 1 << 3, + HasEyePos = 1 << 4, + HasUpVec = 1 << 5, + HasTargetPos = 1 << 6, + HasRoll = 1 << 7, +} + +class dDemo_camera_c extends TCamera { + public flags = 0; + public projNear = 0; + public projFar = 0; + public fovY = 0; + public aspect = 0; + public viewPosition = vec3.create(); + public upVector = vec3.create(); + public targetPosition = vec3.create(); + public roll = 0; + + constructor( + private globals: dGlobals + ) { super() } + + public override JSGGetName() { return 'Cam'; } + + public override JSGGetProjectionNear(): number { + const camera = this.globals.camera; + if (!camera) + return 0.0; + return camera.near; + } + + public override JSGSetProjectionNear(v: number) { + this.projNear = v; + this.flags |= EDemoCamFlags.HasNearZ; + } + + public override JSGGetProjectionFar(): number { + const camera = this.globals.camera; + if (!camera) + return 1.0; + return camera.far; + } + + + public override JSGSetProjectionFar(v: number): void { + this.projFar = v; + this.flags |= EDemoCamFlags.HasFarZ; + } + + + public override JSGGetProjectionFovy(): number { + const camera = this.globals.camera; + if (!camera) + return 60.0; + return camera.fovY; + } + + + public override JSGSetProjectionFovy(v: number): void { + this.fovY = v; + this.flags |= EDemoCamFlags.HasFovY; + } + + + public override JSGGetProjectionAspect() { + const camera = this.globals.camera; + if (!camera) + return 1.3333; + return camera.aspect; + } + + + public override JSGSetProjectionAspect(v: number) { + this.aspect = v; + this.flags |= EDemoCamFlags.HasAspect; + } + + + public override JSGGetViewPosition(dst: vec3) { + vec3.copy(dst, this.globals.cameraPosition); + } + + + public override JSGSetViewPosition(v: ReadonlyVec3) { + vec3.copy(this.viewPosition, v); + this.flags |= EDemoCamFlags.HasEyePos; + } + + + public override JSGGetViewUpVector(dst: vec3) { + const camera = this.globals.camera; + if (!camera) + vec3.set(dst, 0, 1, 0); + getMatrixAxisY(dst, camera.viewMatrix); // @TODO: Double check that this is correct + } + + + public override JSGSetViewUpVector(v: ReadonlyVec3) { + vec3.copy(this.upVector, v); + this.flags |= EDemoCamFlags.HasUpVec; + } + + + public override JSGGetViewTargetPosition(dst: vec3) { + const camera = this.globals.camera; + if (!camera) + vec3.zero(dst); + vec3.add(dst, this.globals.cameraPosition, this.globals.cameraFwd); + } + + + public override JSGSetViewTargetPosition(v: ReadonlyVec3) { + vec3.copy(this.targetPosition, v); + this.flags |= EDemoCamFlags.HasTargetPos; + } + + + public override JSGGetViewRoll() { + const camera = this.globals.camera; + if (!camera) + return 0.0; + return this.roll; // HACK: Instead of actually computing roll (complicated), just assume no one else is modifying it + } + + + public override JSGSetViewRoll(v: number) { + this.roll = v; + this.flags |= EDemoCamFlags.HasRoll; + } +} + +export const enum EDemoActorFlags { + HasData = 1 << 0, + HasPos = 1 << 1, + HasScale = 1 << 2, + HasRot = 1 << 3, + HasShape = 1 << 4, + HasAnim = 1 << 5, + HasFrame = 1 << 6, + HasTexAnim = 1 << 7, + HasTexFrame = 1 << 8, +} + +export class dDemo_actor_c extends TActor { + public name: string; + public flags: number; + public translation = vec3.create(); + public scaling = vec3.create(); + public rotation = vec3.create(); + public shapeId: number; + public nextBckId: number; + public animFrame: number; + public animTransition: number; + public animFrameMax: number; + public texAnim: number; + public texAnimFrame: number; + public textAnimFrameMax: number; + public model: J3DModelInstance; + public stbDataId: number; + public stbData: DataView; + public bckId: number; + public btpIed: number; + public btkId: number; + public brkId: number; + + constructor(public actor: fopAc_ac_c) { super(); } + + public checkEnable(mask: number) { + return this.flags & mask; + } + + public getMorfParam() { + // Doesn't have anim properties + if ((this.flags & 0x40) == 0) { + // Has STB data + if ((this.flags & 1) == 0) { + return 0.0; + } else { + switch (this.stbDataId) { + // @TODO: Double check this, somehow + case 6: return this.stbData.getInt8(15); + case 5: return this.stbData.getInt8(11); + case 4: return this.stbData.getInt8(6); + case 2: return this.stbData.getInt8(7); + case 1: return this.stbData.getInt8(2); + default: return 0.0; + } + } + } else { + return this.animTransition; + } + } + + public override JSGGetName() { return this.name; } + + public override JSGGetNodeTransformation(nodeId: number, mtx: mat4): number { + debugger; // I think this may be one of the shapeInstanceState matrices instead + mat4.copy(mtx, this.model.modelMatrix); + return 1; + } + + public override JSGGetAnimationFrameMax() { return this.animFrameMax; } + public override JSGGetTextureAnimationFrameMax() { return this.textAnimFrameMax; } + + public override JSGGetTranslation(dst: vec3) { vec3.copy(dst, this.translation); } + public override JSGGetScaling(dst: vec3) { vec3.copy(dst, this.scaling); } + public override JSGGetRotation(dst: vec3) { + dst[0] = cM_sht2d(this.rotation[0]); + dst[1] = cM_sht2d(this.rotation[1]); + dst[2] = cM_sht2d(this.rotation[2]); + } + + public override JSGSetData(id: number, data: DataView): void { + this.stbDataId = id; + this.stbData = data; // @TODO: Check that data makes sense + this.flags |= EDemoActorFlags.HasData; + } + + public override JSGSetTranslation(src: ReadonlyVec3) { + vec3.copy(this.translation, src); + this.flags |= EDemoActorFlags.HasPos; + } + + public override JSGSetScaling(src: ReadonlyVec3) { + vec3.copy(this.scaling, src); + this.flags |= EDemoActorFlags.HasScale; + } + + public override JSGSetRotation(src: ReadonlyVec3) { + this.rotation[0] = cM_deg2s(src[0]); + this.rotation[1] = cM_deg2s(src[1]); + this.rotation[2] = cM_deg2s(src[2]); + this.flags |= EDemoActorFlags.HasRot; + } + + public override JSGSetShape(id: number): void { + this.shapeId = id; + this.flags |= EDemoActorFlags.HasShape + } + + public override JSGSetAnimation(id: number): void { + this.nextBckId = id; + this.animFrameMax = 3.402823e+38; + this.flags |= EDemoActorFlags.HasAnim; + } + + public override JSGSetAnimationFrame(x: number): void { + this.animFrame = x; + this.flags |= EDemoActorFlags.HasFrame; + } + + public override JSGSetAnimationTransition(x: number): void { + this.animTransition = x; + this.flags |= EDemoActorFlags.HasFrame; + } + + public override JSGSetTextureAnimation(id: number): void { + this.texAnim = id; + this.flags |= EDemoActorFlags.HasTexAnim; + } + + public override JSGSetTextureAnimationFrame(x: number): void { + this.texAnimFrame = x; + this.flags |= EDemoActorFlags.HasTexFrame; + } +} + +class dDemo_system_c implements TSystem { + private activeCamera: dDemo_camera_c | null; + private actors: dDemo_actor_c[] = []; + // private ambient: dDemo_ambient_c; + // private lights: dDemo_light_c[]; + // private fog: dDemo_fog_c; + + constructor( + private globals: dGlobals + ) { } + + public JSGFindObject(objName: string, objType: JStage.EObject): JStage.TObject | null { + switch (objType) { + case JStage.EObject.Camera: + if (this.activeCamera) return this.activeCamera; + else return this.activeCamera = new dDemo_camera_c(this.globals); + + case JStage.EObject.Actor: + case JStage.EObject.PreExistingActor: + let actor = fopAcM_searchFromName(this.globals, objName, 0, 0); + if (!actor) { + if (objType == JStage.EObject.Actor && objName == "d_act") { + debugger; // Untested. Unimplemented + actor = {} as fopAc_ac_c; + } else { + console.warn('Demo failed to find actor', objName); + return null; + } + } + if (!this.actors[actor.demoActorID]) { + actor.demoActorID = this.actors.length; + this.actors[actor.demoActorID] = new dDemo_actor_c(actor); + this.actors[actor.demoActorID].name = objName; + }; + return this.actors[actor.demoActorID]; + + case JStage.EObject.Ambient: + case JStage.EObject.Light: + case JStage.EObject.Fog: + default: + console.debug('[JSGFindObject] Unhandled type: ', objType); + return null; + } + } + + public getCamera() { return this.activeCamera; } + public getActor(actorID: number) { return this.actors[actorID]; } + + public remove() { + this.activeCamera = null; + + for (let demoActor of this.actors) { demoActor.actor.demoActorID = -1; } + this.actors = []; + } +} + +export class dDemo_manager_c { + private frame: number; + private frameNoMsg: number; + private mode = EDemoMode.None; + private curFile: ArrayBufferSlice | null; + + private parser: TParse; + private system = new dDemo_system_c(this.globals); + private control: TControl = new TControl(this.system); + + constructor( + private globals: dGlobals + ) { } + + public getFrame() { return this.frame; } + public getFrameNoMsg() { return this.frameNoMsg; } + public getMode() { return this.mode; } + public getSystem() { return this.system; } + + public create(data: ArrayBufferSlice, originPos?: vec3, rotY?: number, startFrame?: number): boolean { + this.parser = new TParse(this.control); + + if (!this.parser.parse(data, 0)) { + console.error('Failed to parse demo data'); + return false; + } + + this.control.forward(startFrame || 0); + if (originPos) { + this.control.transformSetOrigin(originPos, rotY || 0); + } + + this.frame = 0; + this.frameNoMsg = 0; + this.curFile = data; + this.mode = EDemoMode.Playing; + + return true; + } + + public remove() { + this.control.destroyObject_all(); + this.system.remove(); + this.curFile = null; + this.mode = 0; + } + + public update(): boolean { + if (!this.curFile) { + return false; + } + + const dtFrames = this.globals.context.viewerInput.deltaTime / 1000.0 * 30; + + // noclip modification: If a demo is suspended (waiting for the user to interact with a message), just resume + if (this.control.isSuspended()) { this.control.setSuspend(0); } + + if (this.control.forward(dtFrames)) { + this.frame += dtFrames; + if (!this.control.isSuspended()) { + this.frameNoMsg += dtFrames; + } + } else { + this.mode = EDemoMode.Ended; + } + return true; + } +} + +/** + * Called by Actor update functions to update their data from the demo version of the actor. + */ +export function dDemo_setDemoData(globals: dGlobals, dtFrames: number, actor: fopAc_ac_c, flagMask: number, + morf: mDoExt_McaMorf | null = null, arcName: string | null = null) { + const demoActor = globals.scnPlay.demo.getSystem().getActor(actor.demoActorID); + if (!demoActor) + return false; + + const enable = demoActor.checkEnable(flagMask); + if (enable & 2) { + // actor.current.pos = demoActor.mTranslation; + // actor.old.pos = actor.current.pos; + vec3.copy(actor.pos, demoActor.translation); + } + if (enable & 8) { + // actor.shape_angle = demoActor.mRotation; + // actor.current.angle = actor.shape_angle; + vec3.copy(actor.rot, demoActor.rotation); + } + if (enable & 4) { + actor.scale = demoActor.scaling; + } + + if (!morf) + return true; + + demoActor.model = morf.model; + + if ((enable & 0x20) && (demoActor.nextBckId != demoActor.bckId)) { + const bckID = demoActor.nextBckId; + if (bckID & 0x10000) + arcName = globals.roomCtrl.demoArcName; + assert(!!arcName); + demoActor.bckId = bckID; + + const i_key = globals.resCtrl.getObjectIDRes(ResType.Bck, arcName, bckID); + assert(!!i_key); + + // void* i_sound = dDemo_getJaiPointer(a_name, bck, soundCount, soundIdxs); + morf.setAnm(i_key, -1 as LoopMode, demoActor.getMorfParam(), 1.0, 0.0, -1.0); + demoActor.animFrameMax = morf.frameCtrl.endFrame; + } + + if (enable & 0x40) { + if (demoActor.animFrame > dtFrames) { + morf.frameCtrl.setFrame(demoActor.animFrame - dtFrames); + morf.play(dtFrames); + } else { + morf.frameCtrl.setFrame(demoActor.animFrame); + } + } else { + morf.play(dtFrames); + } + + return true; +} \ No newline at end of file diff --git a/src/ZeldaWindWaker/d_kankyo.ts b/src/ZeldaWindWaker/d_kankyo.ts index 9ab640388..08fad9e05 100644 --- a/src/ZeldaWindWaker/d_kankyo.ts +++ b/src/ZeldaWindWaker/d_kankyo.ts @@ -650,7 +650,7 @@ export function setLightTevColorType(globals: dGlobals, modelInstance: J3DModelI function SetBaseLight(globals: dGlobals): void { const envLight = globals.g_env_light; - const lgtv = globals.roomStatus[globals.mStayNo].lgtv; + const lgtv = globals.roomCtrl.status[globals.mStayNo].data.lgtv; if (lgtv !== null) { vec3.copy(envLight.baseLight.pos, lgtv.pos); colorFromRGBA(envLight.baseLight.color, 0.0, 0.0, 0.0, 0.0); diff --git a/src/ZeldaWindWaker/d_kankyo_wether.ts b/src/ZeldaWindWaker/d_kankyo_wether.ts index 6e5555b50..a0534d9ac 100644 --- a/src/ZeldaWindWaker/d_kankyo_wether.ts +++ b/src/ZeldaWindWaker/d_kankyo_wether.ts @@ -2001,7 +2001,7 @@ function wether_move_windline(globals: dGlobals, deltaTimeFrames: number): void const envLight = globals.g_env_light; let windlineCount = 0; - const fili = globals.roomStatus[globals.mStayNo].fili; + const fili = globals.roomCtrl.status[globals.mStayNo].data.fili; if (fili !== null && !!(fili.param & 0x100000) && globals.stageName !== 'GTower') { windlineCount = (10.0 * dKyw_get_wind_pow(envLight)) | 0; } @@ -2397,7 +2397,7 @@ function wether_move_wave(globals: dGlobals, deltaTimeFrames: number): void { // TODO(jstpierre): #TACT_WIND. Overwrite with tact wind. LinkRM / Orichh / Ojhous2 / Omasao / Onobuta } - const fili = globals.roomStatus[globals.mStayNo].fili; + const fili = globals.roomCtrl.status[globals.mStayNo].data.fili; let skyboxY = 0.0; if (fili !== null) skyboxY = fili.skyboxY; @@ -2524,7 +2524,7 @@ function vrkumo_move(globals: dGlobals, deltaTimeFrames: number): void { } { - const fili = globals.roomStatus[globals.mStayNo].fili; + const fili = globals.roomCtrl.status[globals.mStayNo].data.fili; let skyboxY = 0.0; if (fili !== null) skyboxY = fili.skyboxY; @@ -2697,7 +2697,7 @@ export function dKyw_wind_set(globals: dGlobals): void { targetWindPower = envLight.customWindPower; } else { let windPowerFlag = 0; - const fili = globals.roomStatus[globals.mStayNo].fili; + const fili = globals.roomCtrl.status[globals.mStayNo].data.fili; if (fili !== null) windPowerFlag = dStage_FileList_dt_GlobalWindLevel(fili); diff --git a/src/ZeldaWindWaker/d_resorce.ts b/src/ZeldaWindWaker/d_resorce.ts index 89b74ae0f..b5e79059b 100644 --- a/src/ZeldaWindWaker/d_resorce.ts +++ b/src/ZeldaWindWaker/d_resorce.ts @@ -43,7 +43,7 @@ function parseDZSHeaders(buffer: ArrayBufferSlice): DZS { } export const enum ResType { - Model, Bmt, Bck, Bpk, Brk, Btp, Btk, Bti, Dzb, Dzs, Bva, Raw, + Model, Bmt, Bck, Bpk, Brk, Btp, Btk, Bti, Dzb, Dzs, Bva, Stb, Raw, } export type ResAssetType = @@ -58,6 +58,7 @@ export type ResAssetType = T extends ResType.Dzb ? cBgD_t : T extends ResType.Dzs ? DZS : T extends ResType.Bva ? VAF1 : + T extends ResType.Stb ? NamedArrayBufferSlice : T extends ResType.Raw ? NamedArrayBufferSlice : unknown; @@ -88,6 +89,15 @@ export class dRes_control_c { return this.getResByIndex(resType, arcName, resIndex, this.resObj); } + public getObjectResByName(resType: T, arcName: string, resName: string): ResAssetType | null { + return this.getResByName(resType, arcName, resName, this.resObj); + } + + public getObjectIDRes(resType: T, arcName: string, resID: number): ResAssetType { + resID &= 0x0000FFFF; // Consider resID as a short + return this.getResByID(resType, arcName, resID, this.resObj); + } + public getResByName(resType: T, arcName: string, resName: string, resList: dRes_info_c[]): ResAssetType | null { const resInfo = assertExists(this.findResInfo(arcName, resList)); return resInfo.getResByName(resType, resName); @@ -98,6 +108,11 @@ export class dRes_control_c { return resInfo.getResByIndex(resType, resIndex); } + public getResByID(resType: T, arcName: string, resID: number, resList: dRes_info_c[]): ResAssetType { + const resInfo = assertExists(this.findResInfo(arcName, resList)); + return resInfo.getResByID(resType, resID); + } + public mountRes(device: GfxDevice, cache: GfxRenderCache, arcName: string, archive: JKRArchive, resList: dRes_info_c[]): void { if (this.findResInfo(arcName, resList) !== null) return; @@ -149,7 +164,7 @@ export class dRes_info_c { resEntry.res = BVA.parse(file.buffer) as ResAssetType; } else if (resType === ResType.Dzs) { resEntry.res = parseDZSHeaders(file.buffer) as ResAssetType; - } else if (resType === ResType.Raw) { + } else if (resType === ResType.Raw || resType === ResType.Stb) { resEntry.res = file.buffer as ResAssetType; } else { throw "whoops"; diff --git a/src/ZeldaWindWaker/d_stage.ts b/src/ZeldaWindWaker/d_stage.ts index 1b5ea3c67..8cf12f436 100644 --- a/src/ZeldaWindWaker/d_stage.ts +++ b/src/ZeldaWindWaker/d_stage.ts @@ -3,7 +3,7 @@ import { Color, White, colorNewCopy, colorFromRGBA8, colorNewFromRGBA8 } from ". import { DZS } from "./d_resorce.js"; import ArrayBufferSlice from "../ArrayBufferSlice.js"; import { nArray, assert, readString } from "../util.js"; -import { dKy_tevstr_c } from "./d_kankyo.js"; +import { dKy_tevstr_c, dKy_tevstr_init } from "./d_kankyo.js"; import { vec3 } from "gl-matrix"; import { Endianness } from "../endian.js"; import { dGlobals } from "./Main.js"; @@ -518,13 +518,27 @@ export function dStage_dt_c_stageLoader(globals: dGlobals, dt: dStage_stageDt_c, export class dStage_roomDt_c extends dStage_dt { public fili: dStage_FileList_dt_c | null = null; public lgtv: stage_lightvec_info_class | null = null; + public lbnk: Uint8Array | null = null; } -export class dStage_roomStatus_c extends dStage_roomDt_c { +export class dStage_roomStatus_c { + public data = new dStage_roomDt_c(); public tevStr = new dKy_tevstr_c(); public visible = true; } +export class dStage_roomControl_c { + public status: dStage_roomStatus_c[] = nArray(64, () => new dStage_roomStatus_c()); + public demoArcName: string | null = null; + + constructor() { + for (let i = 0; i < this.status.length; i++) { + this.status[i].data.roomNo = i; + dKy_tevstr_init(this.status[i].tevStr, i); + } + } +} + function dStage_filiInfoInit(globals: dGlobals, dt: dStage_roomDt_c, buffer: ArrayBufferSlice, count: number): void { if (count !== 0) { assert(count === 1); @@ -546,6 +560,10 @@ function dStage_lgtvInfoInit(globals: dGlobals, dt: dStage_roomDt_c, buffer: Arr } } +function dStage_lbnkInfoInit(globals: dGlobals, dt: dStage_roomDt_c, buffer: ArrayBufferSlice, count: number ): void { + dt.lbnk = buffer.createTypedArray(Uint8Array, 0, count); +} + function dStage_roomTresureInit(globals: dGlobals, dt: dStage_roomDt_c, buffer: ArrayBufferSlice, count: number, fileData: ArrayBufferSlice, layer: number): void { // dt.tres = ...; dStage_actorInit(globals, dt, buffer, count, fileData, layer); @@ -562,6 +580,7 @@ export function dStage_dt_c_roomLoader(globals: dGlobals, dt: dStage_roomDt_c, d 'LGTV': dStage_lgtvInfoInit, 'RPPN': dStage_rppnInfoInit, 'RPAT': dStage_rpatInfoInit, + 'LBNK': dStage_lbnkInfoInit, }); } @@ -581,5 +600,5 @@ export function dStage_dt_c_roomReLoader(globals: dGlobals, dt: dStage_roomDt_c, //#endregion export function dPath_GetRoomPath(globals: dGlobals, idx: number, roomNo: number): dPath { - return globals.roomStatus[roomNo].rpat[idx]; + return globals.roomCtrl.status[roomNo].data.rpat[idx]; } diff --git a/src/ZeldaWindWaker/d_wood.ts b/src/ZeldaWindWaker/d_wood.ts index bcb76a8dd..f71fb13b5 100644 --- a/src/ZeldaWindWaker/d_wood.ts +++ b/src/ZeldaWindWaker/d_wood.ts @@ -758,8 +758,8 @@ export class WoodPacket implements J3DPacket { for (let r = 0; r < kRoomCount; r++) { // Set the room color and fog params - colorCopy(materialParams.u_Color[ColorKind.C0], globals.roomStatus[r].tevStr.colorC0); - colorCopy(materialParams.u_Color[ColorKind.C1], globals.roomStatus[r].tevStr.colorK0); + colorCopy(materialParams.u_Color[ColorKind.C0], globals.roomCtrl.status[r].tevStr.colorC0); + colorCopy(materialParams.u_Color[ColorKind.C1], globals.roomCtrl.status[r].tevStr.colorK0); dKy_GxFog_set(globals.g_env_light, materialParams.u_FogBlock, viewerInput.camera); for (let unit of this.unit[r]) { diff --git a/src/ZeldaWindWaker/framework.ts b/src/ZeldaWindWaker/framework.ts index 05a227733..832bc05ae 100644 --- a/src/ZeldaWindWaker/framework.ts +++ b/src/ZeldaWindWaker/framework.ts @@ -8,6 +8,7 @@ import { dKy_tevstr_c, dKy_tevstr_init } from "./d_kankyo.js"; import { AABB } from "../Geometry.js"; import { Camera, computeScreenSpaceProjectionFromWorldSpaceAABB, computeScreenSpaceProjectionFromWorldSpaceSphere, ScreenSpaceProjection } from "../Camera.js"; import { transformVec3Mat4w1 } from "../MathHelpers.js"; +import { dGlobals } from "./Main.js"; export const enum fpc__ProcessName { d_s_play = 0x0007, @@ -29,6 +30,7 @@ export const enum fpc__ProcessName { d_a_obj_wood = 0x010C, d_a_obj_flame = 0x010D, d_a_tbox = 0x0126, + d_a_npc_ls1 = 0x0143, d_a_kytag00 = 0x0181, d_a_kytag01 = 0x0182, d_a_obj_zouK = 0x018F, @@ -155,7 +157,7 @@ function fpcDt_Delete(globals: fGlobals, pc: base_process_class): void { //#region cPhs, fpcCt, fpcSCtRq export interface fpc_bs__Constructor { - new(globalUserData: fGlobals, pcId: number, profile: DataView): base_process_class; + new(globalUserData: fGlobals, pcName: number, pcId: number, profile: DataView): base_process_class; } class standard_create_request_class { @@ -167,7 +169,7 @@ class standard_create_request_class { ]); public process: base_process_class | null = null; - constructor(public layer: layer_class, public pcId: number, public konstructor: fpc_bs__Constructor, public profileBinary: ArrayBufferSlice, public userData: T) { + constructor(public layer: layer_class, public pcName: number, public pcId: number, public konstructor: fpc_bs__Constructor, public profileBinary: ArrayBufferSlice, public userData: T) { } // fpcSCtRq_Handler @@ -181,7 +183,7 @@ class standard_create_request_class { private CreateProcess(globals: fGlobals, globalUserData: GlobalUserData, userData: this): cPhs__Status { const self = userData; - self.process = new self.konstructor(globals, self.pcId, self.profileBinary.createDataView()); + self.process = new self.konstructor(globals, self.pcName, self.pcId, self.profileBinary.createDataView()); return cPhs__Status.Next; } @@ -251,7 +253,7 @@ export function fpcSCtRq_Request(globals: fGlobals, ly: layer_class | null, p const binary = fpcPf_Get__ProfileBinary(globals, pcName); const pcId = fpcBs_MakeOfId(globals); - const rq = new standard_create_request_class(ly, pcId, constructor, binary, userData); + const rq = new standard_create_request_class(ly, pcName, pcId, constructor, binary, userData); fpcCtRq_ToCreateQ(globals, rq); return pcId; } @@ -338,13 +340,16 @@ export class base_process_class { public ly: layer_class; public pi = new process_priority_class(); - constructor(globals: fGlobals, public processId: number, profile: DataView) { + constructor(globals: fGlobals, public profName: number, public processId: number, profile: DataView) { // fpcBs_Create this.pi.layerID = profile.getUint32(0x00); this.pi.listID = profile.getUint16(0x04); this.pi.listIndex = profile.getUint16(0x06); this.processName = profile.getUint16(0x08); this.parameters = profile.getUint32(0x18); + + // The game stores these separately, and frequently accesses both. Lets see if they ever differ. + assert(profName == this.processName); } // In the original game, construction is inside "create". Here, we split it into construction and "load". @@ -392,8 +397,8 @@ class process_node_class extends base_process_class { public visible: boolean = true; public roomVisible: boolean = true; - constructor(globals: fGlobals, pcId: number, profile: DataView) { - super(globals, pcId, profile); + constructor(globals: fGlobals, pcName: number, pcId: number, profile: DataView) { + super(globals, pcName, pcId, profile); this.layer = new layer_class(globals.lyNextID++); } @@ -409,8 +414,8 @@ class leafdraw_class extends base_process_class { public visible: boolean = true; public roomVisible: boolean = true; - constructor(globals: fGlobals, pcId: number, profile: DataView) { - super(globals, pcId, profile); + constructor(globals: fGlobals, pcName: number, pcId: number, profile: DataView) { + super(globals, pcName, pcId, profile); this.drawPriority = profile.getUint16(0x20); } @@ -485,6 +490,7 @@ export class fopAc_ac_c extends leafdraw_class { public subtype: number = 0xFF; public roomNo: number = -1; public tevStr = new dKy_tevstr_c(); + public demoActorID: number = -1; protected cullSizeBox: AABB | null = null; protected cullSizeSphere: vec4 | null = null; protected cullMtx: mat4 | null = null; @@ -494,8 +500,8 @@ export class fopAc_ac_c extends leafdraw_class { private loadInit: boolean = false; - constructor(globals: fGlobals, pcId: number, profile: DataView) { - super(globals, pcId, profile); + constructor(globals: fGlobals, pcName: number, pcId: number, profile: DataView) { + super(globals, pcName, pcId, profile); // Initialize our culling information from the profile... const cullType = profile.getUint8(0x2D); @@ -648,6 +654,22 @@ export function fopAcIt_JudgeByID(globals: fGlobal } return null; } + +export function fopAcM_searchFromName(globals: dGlobals, procName: string, paramMask: number, param: number): fopAc_ac_c | null { + const objName = globals.dStage_searchName(procName); + if (!objName) { return null; } + + for (let i = 0; i < globals.frameworkGlobals.lnQueue.length; i++) { + const act = globals.frameworkGlobals.lnQueue[i] as fopAc_ac_c; + if (act.profName == objName.pcName + && objName.subtype == act.subtype + && (paramMask == 0 || param == (act.parameters & paramMask)) + ) + return act; + } + + return null; +} //#endregion //#region fopKy diff --git a/src/ZeldaWindWaker/m_do_ext.ts b/src/ZeldaWindWaker/m_do_ext.ts index 53d790f66..19e930792 100644 --- a/src/ZeldaWindWaker/m_do_ext.ts +++ b/src/ZeldaWindWaker/m_do_ext.ts @@ -188,7 +188,7 @@ export class mDoExt_McaMorf implements JointMatrixCalc { } public setMorf(morfFrames: number): void { - if (this.prevMorf < 0.0 || morfFrames < 0.0) { + if (this.prevMorf < 0.0 || morfFrames <= 0.0) { this.curMorf = 1.0; } else { this.curMorf = 0.0; diff --git a/src/ui.ts b/src/ui.ts index 7ebf3cf15..18e5fe5d6 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -55,6 +55,7 @@ export const TIME_OF_DAY_ICON = ``; export const SAND_CLOCK_ICON = ''; export const VR_ICON = ``; +export const CUTSCENE_ICON = ` ` export function setChildren(parent: Element, children: Element[]): void { // We want to swap children around without removing them, since removing them will cause