Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wind Waker: Allow users to refresh, save/load, or share cutscene scenes at a specific point in time #726

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CaptureHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function captureSceneToZip(viewer: Viewer, options: CaptureOptions)
const fileEntries: ZipFileEntry[] = [];

// This is some ugliness to take over the main code... in an ideal world we'd do this offscreen...
viewer.sceneTime = 0;
viewer.sceneTime.time = 0;
viewer.rafTime = 0;
for (let i = 0; i < options.frameCount; i++) {
const t = i / (options.frameCount - 1);
Expand Down
13 changes: 13 additions & 0 deletions src/SaveManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ export class SaveManager {
this.settingListeners[i].callback(this, key);
}

public loadTime(key: string): number | null {
const timeStr = window.localStorage.getItem(`SceneTime_${key}`);
return timeStr ? parseInt(timeStr) : null;
}

public saveTime(key: string, time: number) {
window.localStorage.setItem(`SceneTime_${key}`, Math.round(time).toString());
}

public deleteTime(key: string) {
window.localStorage.removeItem(`SceneTime_${key}`);
}

public addSettingListener(key: string, callback: SettingCallback, triggerNow: boolean = true): void {
this.settingListeners.push({ callback, key });
if (triggerNow)
Expand Down
3 changes: 2 additions & 1 deletion src/SceneBase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { GfxDevice } from "./gfx/platform/GfxPlatform.js";
import { SceneGfx, ViewerRenderInput } from "./viewer.js";
import { SceneGfx, SceneTime, ViewerRenderInput } from "./viewer.js";
import { DataFetcher } from "./DataFetcher.js";
import { DataShare } from "./DataShare.js";
import { GfxRenderInstManager } from "./gfx/render/GfxRenderInstManager.js";
Expand All @@ -27,6 +27,7 @@ export interface SceneContext {
destroyablePool: Destroyable[];
inputManager: InputManager;
viewerInput: ViewerRenderInput;
sceneTime: SceneTime;
}

export interface SceneDesc {
Expand Down
40 changes: 32 additions & 8 deletions src/ZeldaWindWaker/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { dStage_dt_c_roomLoader, dStage_dt_c_roomReLoader, dStage_dt_c_stageInit
import { WoodPacket } from './d_wood.js';
import { fopAcM_create, fopAc_ac_c } from './f_op_actor.js';
import { cPhs__Status, fGlobals, fopDw_Draw, fopScn, fpcCt_Handler, fpcLy_SetCurrentLayer, fpcM_Management, fpcPf__Register, fpcSCtRq_Request, fpc_pc__ProfileList } from './framework.js';
import { IS_DEVELOPMENT } from '../BuildVersion.js';

type SymbolData = { Filename: string, SymbolName: string, Data: ArrayBufferSlice };
type SymbolMapData = { SymbolData: SymbolData[] };
Expand Down Expand Up @@ -329,11 +330,12 @@ export class WindWakerRenderer implements Viewer.SceneGfx {
public renderCache: GfxRenderCache;

public time: number; // In milliseconds, affected by pause and time scaling
public isPlaying: boolean = false;
public roomLayerMask: number = 0;

public onstatechanged!: () => void;

constructor(public device: GfxDevice, public globals: dGlobals) {
constructor(public sceneId: string, public device: GfxDevice, public globals: dGlobals) {
this.renderHelper = new GXRenderHelperGfx(device);

this.renderCache = this.renderHelper.renderInstManager.gfxRenderCache;
Expand Down Expand Up @@ -473,7 +475,6 @@ export class WindWakerRenderer implements Viewer.SceneGfx {
// 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) {
Expand Down Expand Up @@ -501,6 +502,19 @@ export class WindWakerRenderer implements Viewer.SceneGfx {
this.globals.context.inputManager.isMouseEnabled = true;
}

// Save the camera state when play/pause is toggled
if( this.globals.context.sceneTime.isPlaying != this.isPlaying ) {
this.isPlaying = this.globals.context.sceneTime.isPlaying;
this.onstatechanged();

if(IS_DEVELOPMENT) {
// If pausing, save the time and pause state. If we reload, the demo will be paused at the same spot
// If resuming, clear the time and pause state. Reloading will restart the demo.
if (this.isPlaying) { this.globals.context.sceneTime.deleteState(this.sceneId); }
else { this.globals.context.sceneTime.saveState(this.sceneId); }
}
}

this.globals.camera = viewerInput.camera;

// Not sure exactly where this is ordered...
Expand Down Expand Up @@ -629,6 +643,7 @@ export class WindWakerRenderer implements Viewer.SceneGfx {
this.extraTextures.destroy(device);
this.globals.destroy(device);
this.globals.frameworkGlobals.delete(this.globals);
if(IS_DEVELOPMENT) { this.globals.context.sceneTime.deleteState(this.sceneId); }
}
}

Expand Down Expand Up @@ -891,7 +906,7 @@ class SceneDesc {
this.globals = globals;
globals.stageName = this.stageDir;

const renderer = new WindWakerRenderer(device, globals);
const renderer = new WindWakerRenderer(this.id, device, globals);
context.destroyablePool.push(renderer);
globals.renderer = renderer;

Expand Down Expand Up @@ -977,9 +992,9 @@ class DemoDesc extends SceneDesc implements Viewer.SceneDesc {
public layer: number,
public offsetPos?:vec3,
public rotY: number = 0,
public startCode?: number,
public eventFlags?: number,
public startFrame?: number, // noclip modification for easier debugging
public startCode: number = 0,
public eventFlags: number = 0,
public startFrame: number = 0, // noclip modification for easier debugging
) {
super(stageDir, name, roomList);
assert(this.roomList.length === 1);
Expand All @@ -990,6 +1005,8 @@ class DemoDesc extends SceneDesc implements Viewer.SceneDesc {
}

public override async createScene(device: GfxDevice, context: SceneContext): Promise<Viewer.SceneGfx> {
context.sceneTime.togglePlayPause(false);

const res = await super.createScene(device, context);
this.playDemo(this.globals);
return res;
Expand Down Expand Up @@ -1027,15 +1044,22 @@ class DemoDesc extends SceneDesc implements Viewer.SceneDesc {

// @TODO: Set noclip layer visiblity based on this.layer

if(IS_DEVELOPMENT) {
// Load the current scene time and play/pause state, if saved
globals.context.sceneTime.loadState(this.id);
}

// 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); }
if( demoData ) {
globals.scnPlay.demo.setFrame(this.startFrame + globals.context.sceneTime.time / 1000.0 * 30.0);
globals.scnPlay.demo.create(demoData, this.offsetPos, this.rotY / 180.0 * Math.PI);
} else { console.warn('Failed to load demo data:', this.stbFilename); }

return this.globals.renderer;
}
Expand Down
16 changes: 7 additions & 9 deletions src/ZeldaWindWaker/d_demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,8 @@ class dDemo_system_c implements TSystem {
}

export class dDemo_manager_c {
private frame: number;
private frameNoMsg: number;
private frame: number = 0;
private frameNoMsg: number = 0;
private mode = EDemoMode.None;
private curFile: ArrayBufferSlice | null;

Expand All @@ -359,26 +359,24 @@ export class dDemo_manager_c {
private globals: dGlobals
) { }

public getFrame() { return this.frame; }
public getFrameNoMsg() { return this.frameNoMsg; }
public getFrame() { return this.frameNoMsg; }
public setFrame(frame: number) { this.frame = frame; this.frameNoMsg = frame; }
public getMode() { return this.mode; }
public getSystem() { return this.system; }

public create(data: ArrayBufferSlice, originPos?: vec3, rotY?: number, startFrame?: number): boolean {
public create(data: ArrayBufferSlice, originPos?: vec3, rotY?: 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);
this.control.forward(this.frame || 0);
if (originPos) {
this.control.transformSetOrigin(originPos, rotY || 0);
}

this.frame = 0;
this.frameNoMsg = 0;
this.curFile = data;
this.mode = EDemoMode.Playing;

Expand All @@ -397,7 +395,7 @@ export class dDemo_manager_c {
return false;
}

const dtFrames = this.globals.context.viewerInput.deltaTime / 1000.0 * 30;
let 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); }
Expand Down
18 changes: 10 additions & 8 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,9 @@ class Main {
if (inputManager.isKeyDownEventTriggered('Numpad3'))
this._exportSaveData();
if (inputManager.isKeyDownEventTriggered('Period'))
this.ui.togglePlayPause();
this.viewer.sceneTime.togglePlayPause();
if (inputManager.isKeyDown('Comma')) {
this.ui.togglePlayPause(false);
this.viewer.sceneTime.togglePlayPause(false);
this.isFrameStep = true;
}
if (inputManager.isKeyDownEventTriggered('F9'))
Expand Down Expand Up @@ -472,7 +472,7 @@ class Main {
const shouldTakeScreenshot = this.viewer.inputManager.isKeyDownEventTriggered('Numpad7') || this.viewer.inputManager.isKeyDownEventTriggered('BracketRight');

let sceneTimeScale = this.sceneTimeScale;
if (!this.ui.isPlaying) {
if (!this.viewer.sceneTime.isPlaying) {
if (this.isFrameStep) {
sceneTimeScale /= 4.0;
this.isFrameStep = false;
Expand All @@ -482,7 +482,7 @@ class Main {
}

if (!this.viewer.externalControl) {
this.viewer.sceneTimeScale = sceneTimeScale;
this.viewer.sceneTime.timeScale = sceneTimeScale;
this.viewer.update(updateInfo);
}

Expand Down Expand Up @@ -545,7 +545,7 @@ class Main {
const byteLength = atob(this._saveStateTmp, 0, state);

let byteOffs = 0;
this.viewer.sceneTime = this._saveStateView.getFloat32(byteOffs + 0x00, true);
this.viewer.sceneTime.time = this._saveStateView.getFloat32(byteOffs + 0x00, true);
byteOffs += 0x04;
byteOffs += deserializeCamera(this.viewer.camera, this._saveStateView, byteOffs);
if (this.viewer.scene !== null && this.viewer.scene.deserializeSaveState)
Expand Down Expand Up @@ -689,8 +689,6 @@ class Main {
if (scene.createPanels)
scenePanels = scene.createPanels();
this.ui.setScenePanels(scenePanels);
// Force time to play when loading a map.
this.ui.togglePlayPause(true);

const sceneDescId = this._getCurrentSceneDescId()!;
this.saveManager.setCurrentSceneDescId(sceneDescId);
Expand Down Expand Up @@ -759,6 +757,9 @@ class Main {
this.destroyablePool[i].destroy(device);
this.destroyablePool.length = 0;

// Force time to play when loading a map (but allow createScene() to override this).
this.viewer.sceneTime.togglePlayPause(true);

// Unhide any hidden scene groups upon being loaded.
if (sceneGroup.hidden)
sceneGroup.hidden = false;
Expand All @@ -778,8 +779,9 @@ class Main {
const inputManager = this.viewer.inputManager;
inputManager.reset();
const viewerInput = this.viewer.viewerRenderInput;
const sceneTime = this.viewer.sceneTime;
const context: SceneContext = {
device, dataFetcher, dataShare, uiContainer, destroyablePool, inputManager, viewerInput,
device, dataFetcher, dataShare, uiContainer, destroyablePool, inputManager, viewerInput, sceneTime
};

// The age delta on pruneOldObjects determines whether any resources will be shared at all.
Expand Down
29 changes: 7 additions & 22 deletions src/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2611,23 +2611,17 @@ class FullscreenButton extends SingleIconButton {
}

class PlayPauseButton extends SingleIconButton {
public onplaypause: ((shouldBePlaying: boolean) => void) | null = null;
public isPlaying: boolean;
public sceneTime: Viewer.SceneTime;

public override syncStyle(): void {
super.syncStyle();
setFontelloIcon(this.icon, this.isPlaying ? FontelloIcon.pause : FontelloIcon.play);
this.tooltipElem.textContent = this.isPlaying ? 'Pause' : 'Play';
}

public setIsPlaying(isPlaying: boolean): void {
this.isPlaying = isPlaying;
this.syncStyle();
setFontelloIcon(this.icon, this.sceneTime.isPlaying ? FontelloIcon.pause : FontelloIcon.play);
this.tooltipElem.textContent = this.sceneTime.isPlaying ? 'Pause' : 'Play';
}

public onClick() {
if (this.onplaypause !== null)
this.onplaypause(!this.isPlaying);
this.sceneTime.togglePlayPause();
this.syncStyle();
}
}

Expand Down Expand Up @@ -2692,8 +2686,6 @@ export class UI {
private isDragging: boolean = false;
private lastMouseActiveTime: number = -1;

public isPlaying: boolean = true;

public isEmbedMode: boolean = false;
public isVisible: boolean = true;

Expand Down Expand Up @@ -2769,10 +2761,7 @@ export class UI {
this.studioPanel = new StudioPanel(this, viewer);
this.toplevel.appendChild(this.studioPanel.elem);

this.playPauseButton.onplaypause = (shouldBePlaying) => {
this.togglePlayPause(shouldBePlaying);
};
this.playPauseButton.setIsPlaying(this.isPlaying);
this.playPauseButton.sceneTime = viewer.sceneTime;

this.about.onfaq = () => {
this.faqPanel.elem.style.display = 'block';
Expand All @@ -2788,11 +2777,6 @@ export class UI {
this.elem = this.toplevel;
}

public togglePlayPause(shouldBePlaying: boolean = !this.isPlaying): void {
this.isPlaying = shouldBePlaying;
this.playPauseButton.setIsPlaying(this.isPlaying);
}

public toggleWebXRCheckbox(shouldBeChecked: boolean = !this.xrSettings.enableXRCheckBox.checked) {
this.xrSettings.enableXRCheckBox.setChecked(shouldBeChecked);
}
Expand All @@ -2803,6 +2787,7 @@ export class UI {

public update(): void {
this.syncVisibilityState();
this.playPauseButton.syncStyle();
}

public setSaveState(saveState: string) {
Expand Down
Loading