From 11c242250dbc06e28ac7dcf9cf0b3a31088490d7 Mon Sep 17 00:00:00 2001 From: EpicYoshiMaster Date: Fri, 20 Jan 2023 18:13:10 -0600 Subject: [PATCH 1/2] Implement customized reset functionality --- .../assets/data/lang/sc/gui.en_US.json.patch | 40 ++++++++++ timer/eventManager.js | 73 +++++++++++++------ timer/plugin.js | 14 ++++ timer/utils.js | 28 ++++++- 4 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 timer/assets/data/lang/sc/gui.en_US.json.patch diff --git a/timer/assets/data/lang/sc/gui.en_US.json.patch b/timer/assets/data/lang/sc/gui.en_US.json.patch new file mode 100644 index 0000000..8dd33a5 --- /dev/null +++ b/timer/assets/data/lang/sc/gui.en_US.json.patch @@ -0,0 +1,40 @@ +{ + "labels": { + "options": { + "headers": { + "ccTimer": "CCTimer" + }, + + "dontResetTimerOnDeath": { + "name": "Don't reset timer on death", + "description": "Don't reset timer on death, \\c[1]WARNING: This will affect the actual IGT!" + }, + + "printEvents": { + "name": "Print all events", + "description": "Print all possible events that can be split on. Use \"Log level: Default\"" + }, + + "roomTimer": { + "name": "Display room timer", + "description": "Displays a room timer." + }, + + "resetOnNewGame": { + "name": "Reset splits on new game", + "description": "Will check for split start conditions upon starting a new file." + }, + + "resetOnPreset": { + "name": "Reset splits on preset", + "description": "Will check for split start conditions upon starting a preset." + }, + + "controls": { + "keys": { + "reset-splits": "Reset Splits" + } + } + } + } +} \ No newline at end of file diff --git a/timer/eventManager.js b/timer/eventManager.js index 5091f08..42c8f1a 100644 --- a/timer/eventManager.js +++ b/timer/eventManager.js @@ -18,35 +18,70 @@ export class EventManager { this._activeConfig = JSON.parse(localStorage.getItem(activeConfigKey)); this._awaitingStart = false; - Hook.onTitlescreen(() => this._cancelAwaitStart()); - Hook.newGameButton(() => this._onStart('newGame')); - Hook.startPresetButton((preset, slotIndex) => this._onStart('preset', preset.slots[slotIndex].title.value)); - Hook.enemyHP((name, hp) => { this._awaitingStart ? this._checkStart('damage', { type: 'damage', name, hp }) : this._check({ type: 'damage', name, hp }) }); - Hook.teleport((mapName) => { this._awaitingStart ? this._checkStart('teleport', { type: 'teleport', mapName }) : this._check({ type: 'teleport', mapName }) }); - Hook.varChanged(() => {this._awaitingStart ? this._checkStart('vars', { type: 'vars' }) : this._check({ type: 'vars' }) }); + Hook.newGameButton(() => this._onNewGameButton()); + Hook.startPresetButton((preset, slotIndex) => this._onStartPresetButton(preset, slotIndex)); + Hook.update(() => this._update()); + Hook.enemyHP((name, hp) => { this._check({ type: 'damage', name, hp }) }); + Hook.teleport((mapName) => { this._check({ type: 'teleport', mapName }) }); + Hook.varChanged(() => { this._check({ type: 'vars' }) }); window.addEventListener('unload', () => this.onunload()); } - _cancelAwaitStart() { - if(this._awaitingStart) { - this._awaitingStart = false; - console.log('[timer] Cancelled Awaiting Start Condition'); + _update() { + if(sc.control.resetSplitsPress()) { + this._resetAndAwaitStart(); } } + _onNewGameButton() { + if(sc.options && sc.options.get('resetOnNewGame')) { + this._resetAndAwaitStart(); + } + + this._check({ type: 'newGame' }); + } + + _onStartPresetButton(preset, slotIndex) { + if(sc.options && sc.options.get('resetOnPreset')) { + this._resetAndAwaitStart(); + } + + this._check({ type:'preset', presetName: preset.slots[slotIndex].title.value } ); + } + + /** + * Called either automatically (by new game / preset if enabled) or manually via a hotkey. + * Resets all configs and tells the timer mod to await a start condition. + */ + _resetAndAwaitStart() { + this._resetConfigs(); + this._activeConfig = null; + this._awaitingStart = true; + console.log(`[timer] Resetting Splits and now Awaiting a Start Condition`); + } + _resetConfigs() { for (const config of this._configs) { config.reset(); } } - _checkStart(startType, action) { + /** + * While no config is selected, all events will go through _checkStart() to see if + * they match the start condition for one of the splitters. + * + * If one is found that matches, that splitter will be set as the _activeConfig, and + * splits will now go through _check() for that splitter alone. + */ + _checkStart(action) { + if(this._activeConfig || !this._awaitingStart) return; + for(const config of this._configs) { for(const event of config.splits) { if (event.disabled || event.type !== 'start') { continue; } - if(!event.on && startType != 'newGame') continue; //empty start condition must be new game + if(!event.on && action.type != 'newGame') continue; //empty start condition must be new game if (event.on) { const [start] = this._checkEvent(event.on, action); @@ -71,12 +106,13 @@ export class EventManager { } /** + * Once an _activeConfig has been assigned, all split events are handled here. * * @param {{type: 'damage', name: string, hp: number} | {type: 'teleport', mapName: string}} [action] */ _check(action) { if(!this._activeConfig) { - //console.log(`[timer] Error: Called _check() without active config`); + this._checkStart(action); return; } @@ -111,14 +147,6 @@ export class EventManager { } } - _onStart(type, presetName) { - console.log(`[timer] Awaiting start condition for type: ${type}` + (presetName ? `, preset: ${presetName}` : '')); - this._resetConfigs(); - this._activeConfig = null; - this._awaitingStart = true; - this._checkStart(type, { type: 'preset', presetName}); - } - /** * * @param {{type: 'start' | 'loadmap' | 'eventtriggered' | 'combined' | 'damage', name?: string, once?: boolean, value?: any, conditions?: any[], below?: number, above?: number}} event @@ -167,7 +195,7 @@ export class EventManager { } for (let i = 0; i < conds.length; i++) { - const [split, once] = this._checkEvent(conds[i]); + const [split, once] = this._checkEvent(conds[i], action); if (!split) { return [false, false]; } @@ -178,7 +206,6 @@ export class EventManager { } } - if (conds.length === 0) { return [true, true]; } diff --git a/timer/plugin.js b/timer/plugin.js index 3785d25..149d79a 100644 --- a/timer/plugin.js +++ b/timer/plugin.js @@ -35,6 +35,20 @@ export default class CCTimer extends Plugin { connection.connect(() => this.setupLivesplit(), () => ingameDisplay.run()); } + prestart() { + //This will not work if defined in main, so it's placed here instead + sc.OPTIONS_DEFINITION["keys-reset-splits"] = { + cat: sc.OPTION_CATEGORY.CONTROLS, + hasDivider: true, + header: 'ccTimer', + init: { + key1: ig.KEY.SEMICOLON, + key2: void 0 + }, + type: 'CONTROLS', + }; + } + async setupLivesplit() { Utils.log('[timer] Connected to livesplit'); Utils.log('[timer] Loading config..'); diff --git a/timer/utils.js b/timer/utils.js index 23defe2..bb12126 100644 --- a/timer/utils.js +++ b/timer/utils.js @@ -2,10 +2,6 @@ import { Hook } from './hooks.js'; export class Utils { addOptions() { - ig.lang.labels.sc.gui.options['dontResetTimerOnDeath'] = {name: 'Don\'t reset timer on death', description: 'Don\'t reset timer on death. \\c[1]WARNING: This will affect the actual IGT!'}; - ig.lang.labels.sc.gui.options['printEvents'] = {name: 'Print all events', description: 'Print all possible events that can be split on. Use "Log level: Default"'}; - ig.lang.labels.sc.gui.options['roomTimer'] = {name: 'Display room timer', description: 'Displays a room timer'}; - ig.lang.labels.sc.gui.options.headers['ccTimer'] = 'CCTimer'; sc.OPTIONS_DEFINITION.dontResetTimerOnDeath = { cat: sc.OPTION_CATEGORY.GENERAL, hasDivider: true, @@ -26,9 +22,33 @@ export class Utils { restart: true, type: 'CHECKBOX', }; + sc.OPTIONS_DEFINITION.resetOnNewGame = { + cat: sc.OPTION_CATEGORY.GENERAL, + init: true, + restart: true, + type: 'CHECKBOX', + }; + sc.OPTIONS_DEFINITION.resetOnPreset = { + cat: sc.OPTION_CATEGORY.GENERAL, + init: true, + restart: true, + type: 'CHECKBOX', + }; if (sc.options.values.dontResetTimerOnDeath == null) { sc.options.values.dontResetTimerOnDeath = false; } + if(sc.options.values.resetOnNewGame == null) { + sc.options.values.resetOnNewGame = true; + } + if(sc.options.values.resetOnPreset == null) { + sc.options.values.resetOnPreset = true; + } + + sc.Control.inject({ + resetSplitsPress() { + return ig.input.pressed("reset-splits"); + } + }); Hook.statsSet((val, stats) => { if(sc.options.get('dontResetTimerOnDeath') From 299a57c56562a98578c06f93502440fc08dfb3fb Mon Sep 17 00:00:00 2001 From: EpicYoshiMaster Date: Sat, 28 Jan 2023 10:49:21 -0600 Subject: [PATCH 2/2] Address requested changes - labels moved back into utils - removed injection into sc.Control - input-api is now an optional dependency (also fixed a visual timer bug that I forgot to address a while back) --- README.md | 6 +++ .../assets/data/lang/sc/gui.en_US.json.patch | 40 ------------------- timer/eventManager.js | 2 +- timer/ingameDisplay.js | 2 +- timer/plugin.js | 23 ++++++----- timer/utils.js | 14 ++++--- 6 files changed, 28 insertions(+), 59 deletions(-) delete mode 100644 timer/assets/data/lang/sc/gui.en_US.json.patch diff --git a/README.md b/README.md index 922031a..14aa583 100644 --- a/README.md +++ b/README.md @@ -251,3 +251,9 @@ Opening another preset or restarting at New Game will reset back to awaiting a ` 2. Place your additional autosplitters in this directory. These should be formatted the same way `settings.json` is, but they can be named however you'd like as long as they end in `.json`. Note: Time and State settings will be derived from the main `settings.json` autosplitter. + +## (Optional) Dependencies + +A **Reset Splits** hotkey is also optionally included, which can be relevant in cases where you need a `"start"` condition to fire without relying on loading a preset or starting at New Game. + +To make use of this, you'll need to install [input-api](https://github.com/CCDirectLink/input-api). \ No newline at end of file diff --git a/timer/assets/data/lang/sc/gui.en_US.json.patch b/timer/assets/data/lang/sc/gui.en_US.json.patch deleted file mode 100644 index 8dd33a5..0000000 --- a/timer/assets/data/lang/sc/gui.en_US.json.patch +++ /dev/null @@ -1,40 +0,0 @@ -{ - "labels": { - "options": { - "headers": { - "ccTimer": "CCTimer" - }, - - "dontResetTimerOnDeath": { - "name": "Don't reset timer on death", - "description": "Don't reset timer on death, \\c[1]WARNING: This will affect the actual IGT!" - }, - - "printEvents": { - "name": "Print all events", - "description": "Print all possible events that can be split on. Use \"Log level: Default\"" - }, - - "roomTimer": { - "name": "Display room timer", - "description": "Displays a room timer." - }, - - "resetOnNewGame": { - "name": "Reset splits on new game", - "description": "Will check for split start conditions upon starting a new file." - }, - - "resetOnPreset": { - "name": "Reset splits on preset", - "description": "Will check for split start conditions upon starting a preset." - }, - - "controls": { - "keys": { - "reset-splits": "Reset Splits" - } - } - } - } -} \ No newline at end of file diff --git a/timer/eventManager.js b/timer/eventManager.js index 42c8f1a..264f6b9 100644 --- a/timer/eventManager.js +++ b/timer/eventManager.js @@ -28,7 +28,7 @@ export class EventManager { } _update() { - if(sc.control.resetSplitsPress()) { + if(ig.input.pressed('reset-splits')) { this._resetAndAwaitStart(); } } diff --git a/timer/ingameDisplay.js b/timer/ingameDisplay.js index 0525985..79e6709 100644 --- a/timer/ingameDisplay.js +++ b/timer/ingameDisplay.js @@ -37,7 +37,7 @@ export class IngameDisplay { if(hour <= 0) { this.timer.innerHTML = min + ((sec < 10) ? ':0': ':') + sec; } else { - this.timer.innerHTML = hour + ((min < 9) ? ':0': ':') + min + ((sec < 10) ? ':0': ':') + sec; + this.timer.innerHTML = hour + ((min < 10) ? ':0': ':') + min + ((sec < 10) ? ':0': ':') + sec; } } } \ No newline at end of file diff --git a/timer/plugin.js b/timer/plugin.js index 149d79a..a20e70b 100644 --- a/timer/plugin.js +++ b/timer/plugin.js @@ -36,17 +36,18 @@ export default class CCTimer extends Plugin { } prestart() { - //This will not work if defined in main, so it's placed here instead - sc.OPTIONS_DEFINITION["keys-reset-splits"] = { - cat: sc.OPTION_CATEGORY.CONTROLS, - hasDivider: true, - header: 'ccTimer', - init: { - key1: ig.KEY.SEMICOLON, - key2: void 0 - }, - type: 'CONTROLS', - }; + if(versions.hasOwnProperty('input-api')) { + sc.OPTIONS_DEFINITION["keys-reset-splits"] = { + cat: sc.OPTION_CATEGORY.CONTROLS, + hasDivider: true, + header: 'ccTimer', + init: { + key1: ig.KEY.SEMICOLON, + key2: void 0 + }, + type: 'CONTROLS', + }; + } } async setupLivesplit() { diff --git a/timer/utils.js b/timer/utils.js index bb12126..21b86be 100644 --- a/timer/utils.js +++ b/timer/utils.js @@ -2,6 +2,14 @@ import { Hook } from './hooks.js'; export class Utils { addOptions() { + ig.lang.labels.sc.gui.options['dontResetTimerOnDeath'] = {name: 'Don\'t reset timer on death', description: 'Don\'t reset timer on death. \\c[1]WARNING: This will affect the actual IGT!'}; + ig.lang.labels.sc.gui.options['printEvents'] = {name: 'Print all events', description: 'Print all possible events that can be split on. Use "Log level: Default"'}; + ig.lang.labels.sc.gui.options['roomTimer'] = {name: 'Display room timer', description: 'Displays a room timer'}; + ig.lang.labels.sc.gui.options['resetOnNewGame'] = {name: 'Reset splits on new game', description: 'Will check for split start conditions upon starting a new file.'}; + ig.lang.labels.sc.gui.options['resetOnPreset'] = {name: 'Reset splits on preset', description: 'Will check for split start conditions upon starting a preset.'}; + ig.lang.labels.sc.gui.options.controls.keys['reset-splits'] = 'Reset Splits'; + ig.lang.labels.sc.gui.options.headers['ccTimer'] = 'CCTimer'; + sc.OPTIONS_DEFINITION.dontResetTimerOnDeath = { cat: sc.OPTION_CATEGORY.GENERAL, hasDivider: true, @@ -44,12 +52,6 @@ export class Utils { sc.options.values.resetOnPreset = true; } - sc.Control.inject({ - resetSplitsPress() { - return ig.input.pressed("reset-splits"); - } - }); - Hook.statsSet((val, stats) => { if(sc.options.get('dontResetTimerOnDeath') && stats