Skip to content

Commit 3f0c087

Browse files
committed
feat: Offline support
Closes #142
1 parent 82982dd commit 3f0c087

File tree

20 files changed

+399
-111
lines changed

20 files changed

+399
-111
lines changed

src/actions/combat/start-combat-actions.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ interface Init extends Omit<ActionNodeDefinition<Props>, 'namespace' | 'category
2323
registry: TypedKeys<Game, NamespaceRegistry<CombatArea>>;
2424
}
2525

26-
function execMob({area, mob}: Props) {
27-
game.stopActiveAction();
26+
function execMob({area, mob}: Props): void {
2827
game.combat.selectMonster(area.monsters[mob!], area);
2928
}
3029

@@ -104,7 +103,6 @@ mkAction({
104103
</Fragment>
105104
),
106105
execute({area}) {
107-
game.stopActiveAction();
108106
game.combat.selectDungeon(area as Dungeon);
109107
},
110108
label: 'Start Dungeon',

src/actions/core/delay-action.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import {noop} from 'lodash-es';
12
import {Fragment} from 'preact';
2-
import {map, noop, timer} from 'rxjs';
3+
import {map, timer} from 'rxjs';
34
import {InternalCategory} from '../../lib/registries/action-registry.mjs';
45
import {defineLocalAction} from '../../lib/util/define-local.mjs';
56

@@ -21,7 +22,7 @@ defineLocalAction<Props>({
2122
media: cdnMedia('assets/media/main/timer.svg'),
2223
options: [
2324
{
24-
description: 'Wait for the given number of milliseconds. The actual wait may be significantly longer if the game is minimised in the browser.',
25+
description: 'Wait for the given number of milliseconds. The actual wait may be significantly longer if the game is minimised in the browser. Does NOT work offline.',
2526
label: 'Duration',
2627
localID: 'duration',
2728
min: 0,

src/actions/lib/recipe-action.mts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {nextComplete} from '@aloreljs/rxutils';
2+
import {noop} from 'lodash-es';
23
import type {GatheringSkill} from 'melvor';
3-
import {noop, Observable} from 'rxjs';
4+
import {Observable} from 'rxjs';
45
import PersistClassName from '../../lib/decorators/PersistClassName.mjs';
56
import type {Obj} from '../../public_api';
67
import type {SkillActionInit} from './skill-action.mjs';
@@ -86,11 +87,9 @@ export class RecipeAction<T extends object, S extends Gathering, R>
8687

8788
/** @inheritDoc */
8889
public execute(data: T): Observable<void> {
89-
return new Observable<void>(s => {
90-
const prep = this.prepareExec(data);
91-
game.stopActiveAction();
92-
this.exec(data, prep);
93-
nextComplete(s);
90+
return new Observable<void>(subscriber => {
91+
this.exec(data, this.prepareExec(data));
92+
nextComplete(subscriber);
9493
});
9594
}
9695
}

src/actions/start-skill/agility.mts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import {nextComplete} from '@aloreljs/rxutils';
21
import type {Agility} from 'melvor';
3-
import {Observable} from 'rxjs';
42
import {defineAction} from '../../lib/api.mjs';
53
import PersistClassName from '../../lib/decorators/PersistClassName.mjs';
64
import {InternalCategory} from '../../lib/registries/action-registry.mjs';
@@ -10,12 +8,8 @@ import SkillAction from '../lib/skill-action.mjs';
108
class AgilityAction extends SkillAction<{}, Agility> {
119

1210
/** @inheritDoc */
13-
public override execute(): Observable<void> {
14-
return new Observable(subscriber => {
15-
game.stopActiveAction();
16-
this.skill.start();
17-
nextComplete(subscriber);
18-
});
11+
public override execute(): void {
12+
this.skill.start();
1913
}
2014
}
2115

src/actions/start-skill/alt-magic.mts

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type {AltMagicSpell, FoodItem, Item, SingleProductArtisanSkillRecipe} from 'melvor';
2-
import {asyncScheduler, Observable, scheduled} from 'rxjs';
32
import {defineAction} from '../../lib/api.mjs';
43
import {InternalCategory} from '../../lib/registries/action-registry.mjs';
54
import {namespace} from '../../manifest.json';
@@ -22,37 +21,29 @@ interface Props {
2221

2322
defineAction<Props>({
2423
category: InternalCategory.START_SKILL,
25-
execute: ({bar, food, junk, item, recipe, superGem}) => (
26-
scheduled(
27-
new Observable<void>(subscriber => {
28-
game.stopActiveAction();
29-
magic.selectSpellOnClick(recipe);
24+
execute({bar, food, junk, item, recipe, superGem}) {
25+
magic.selectSpellOnClick(recipe);
3026

31-
switch (recipe.specialCost.type) {
32-
case AltMagicConsumptionID.AnyItem:
33-
magic.selectItemOnClick(item!);
34-
break;
35-
case AltMagicConsumptionID.AnyNormalFood:
36-
magic.selectItemOnClick(food!);
37-
break;
38-
case AltMagicConsumptionID.JunkItem:
39-
magic.selectItemOnClick(junk!);
40-
break;
41-
case AltMagicConsumptionID.BarIngredientsWithoutCoal:
42-
case AltMagicConsumptionID.BarIngredientsWithCoal:
43-
magic.selectBarOnClick(bar!);
44-
break;
45-
case AltMagicConsumptionID.AnySuperiorGem:
46-
magic.selectItemOnClick(superGem!);
47-
}
27+
switch (recipe.specialCost.type) {
28+
case AltMagicConsumptionID.AnyItem:
29+
magic.selectItemOnClick(item!);
30+
break;
31+
case AltMagicConsumptionID.AnyNormalFood:
32+
magic.selectItemOnClick(food!);
33+
break;
34+
case AltMagicConsumptionID.JunkItem:
35+
magic.selectItemOnClick(junk!);
36+
break;
37+
case AltMagicConsumptionID.BarIngredientsWithoutCoal:
38+
case AltMagicConsumptionID.BarIngredientsWithCoal:
39+
magic.selectBarOnClick(bar!);
40+
break;
41+
case AltMagicConsumptionID.AnySuperiorGem:
42+
magic.selectItemOnClick(superGem!);
43+
}
4844

49-
magic.castButtonOnClick();
50-
51-
subscriber.complete();
52-
}),
53-
asyncScheduler
54-
)
55-
),
45+
magic.castButtonOnClick();
46+
},
5647
label: 'Start Alt. Magic',
5748
localID: 'startAltMagic',
5849
media: magic.media,

src/lib/data-update.mts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type {Workflow} from './data/workflow.mjs';
2+
import update0001 from './updates/update-0001.mjs';
3+
4+
export interface SerialisedWorkflow extends Pick<Workflow, 'name' | 'rm'> {
5+
steps: [string[], any[][]];
6+
}
7+
8+
export type DataUpdateFn = (workflows: SerialisedWorkflow[]) => void;
9+
10+
export interface RunUpdatesResult {
11+
12+
/** Whether at least one update got applied or not */
13+
applied: boolean;
14+
15+
/** The data version for this mod version */
16+
update: number;
17+
}
18+
19+
function getUpdatesArray(): DataUpdateFn[] {
20+
return [
21+
update0001,
22+
];
23+
}
24+
25+
/**
26+
* Run data updates when the storage format changes to avoid users having to redefine all their workflows
27+
* @param dataVersion The current data version
28+
* @param data The raw loaded data
29+
* @return Whether at least one update got applied or not
30+
*/
31+
export function runUpdates(dataVersion: number, data: SerialisedWorkflow[]): RunUpdatesResult {
32+
const updateFns = getUpdatesArray();
33+
34+
// The version defaults to -1 - add 1 to get array index 0
35+
const firstIdx = dataVersion + 1;
36+
for (let i = firstIdx; i < updateFns.length; ++i) {
37+
updateFns[i](data);
38+
}
39+
40+
return {
41+
applied: firstIdx < updateFns.length,
42+
update: updateFns.length - 1,
43+
};
44+
}
45+
46+
export function getUpdateNumber(): number {
47+
return getUpdatesArray().length - 1;
48+
}

src/lib/data/workflow.mts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {FormatToJsonArrayCompressed} from '../decorators/to-json-formatters/form
66
import type {FromJSON, ToJSON} from '../decorators/to-json.mjs';
77
import {JsonProp, Serialisable} from '../decorators/to-json.mjs';
88
import type {ReadonlyBehaviorSubject} from '../registries/workflow-registry.mjs';
9+
import WorkflowRegistry from '../registries/workflow-registry.mjs';
910
import {WorkflowStep} from './workflow-step.mjs';
1011

1112
type Init = Partial<Pick<Workflow, 'name' | 'rm' | 'steps'>>;
@@ -55,9 +56,14 @@ export class Workflow {
5556
return this.steps.length > 1;
5657
}
5758

58-
public get isValid(): boolean {
59+
/**
60+
* @param editedName Duplicate workflow names aren't permitted, but this check would trigger on itself when editing
61+
* a workflow, so the workflow's original name should be provided on edits so it can be used as an exception.
62+
*/
63+
public isValid(editedName?: string): boolean {
5964
return this.name.trim().length !== 0
6065
&& this.steps.length !== 0
66+
&& (editedName === this.name || !WorkflowRegistry.inst.workflows.some(w => w.name === editedName))
6167
&& this.steps.every(s => s.isValid);
6268
}
6369

src/lib/execution/workflow-execution.mts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import {nextComplete} from '@aloreljs/rxutils';
44
import {logError} from '@aloreljs/rxutils/operators';
55
import type {MonoTypeOperatorFunction, Observer, Subscription, TeardownLogic} from 'rxjs';
66
import {
7-
asapScheduler,
8-
asyncScheduler,
97
BehaviorSubject,
108
concat,
119
defer,
@@ -16,7 +14,6 @@ import {
1614
map,
1715
Observable,
1816
of,
19-
scheduled,
2017
startWith,
2118
takeUntil,
2219
tap
@@ -32,6 +29,7 @@ import WorkflowRegistry from '../registries/workflow-registry.mjs';
3229
import {debugLog, errorLog} from '../util/log.mjs';
3330
import prependErrorWith from '../util/rxjs/prepend-error-with.mjs';
3431
import ShareReplayLike from '../util/share-replay-like-observable.mjs';
32+
import {stopAction} from '../util/stop-action.mjs';
3533
import type {
3634
ActionExecutionEvent,
3735
StepCompleteEvent,
@@ -43,6 +41,8 @@ import {WorkflowEventType} from './workflow-event.mjs';
4341

4442
type Out = WorkflowEvent;
4543

44+
const DESC_EVENTS = Object.getOwnPropertyDescriptor(ShareReplayLike.prototype, 'events')!;
45+
4646
/** Represents a workflow in the middle of being executed */
4747
@PersistClassName('WorkflowTrigger')
4848
export class WorkflowExecution extends ShareReplayLike<Out> {
@@ -79,7 +79,7 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
7979
this.activeStepIdxManual = false;
8080
++this.activeStepIdx;
8181
}
82-
this.mainSub = asapScheduler.schedule(this.tick);
82+
this.tick();
8383
},
8484
error: (e: Error) => {
8585
const evt = this.mkCompleteEvent(false, e.message);
@@ -94,9 +94,12 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
9494
},
9595
};
9696

97-
public constructor(public readonly workflow: Workflow) {
97+
public constructor(
98+
public readonly workflow: Workflow,
99+
step = 0
100+
) {
98101
super();
99-
this._activeStepIdx$ = new BehaviorSubject<number>(0);
102+
this._activeStepIdx$ = new BehaviorSubject<number>(step);
100103
this.activeStepIdx$ = this._activeStepIdx$.asObservable();
101104
}
102105

@@ -113,6 +116,10 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
113116
this._activeStepIdx$.next(v);
114117
}
115118

119+
public get isFinished(): boolean {
120+
return this.finished;
121+
}
122+
116123
public get running(): boolean {
117124
return !this.finished;
118125
}
@@ -161,6 +168,7 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
161168
}
162169
this.finished = false;
163170
}
171+
164172
this.tick();
165173
}
166174

@@ -231,7 +239,9 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
231239
return EMPTY;
232240
}
233241

234-
return concat(...step.actions.map((_, i) => this.executeAction(step, stepIdx, i)));
242+
return stopAction().pipe(
243+
switchMap(() => concat(...step.actions.map((_, i) => this.executeAction(step, stepIdx, i))))
244+
);
235245
}
236246

237247
/**
@@ -245,10 +255,9 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
245255
workflow: this.workflow,
246256
};
247257

248-
const trigger$ = step.trigger.listen().pipe(take(1));
249-
250-
const exec$: Observable<Out> = scheduled(trigger$, asyncScheduler)
258+
const exec$: Observable<Out> = step.trigger.listen()
251259
.pipe(
260+
take(1),
252261
switchMap(() => this.executeActions(step, stepIdx)),
253262
logError(`Error executing step ${stepIdx} in workflow ${this.workflow.name}:`),
254263
prependErrorWith(e => of<StepCompleteEvent>({
@@ -351,7 +360,6 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
351360
}
352361

353362
/** Main ticking function for the workflow */
354-
@BoundMethod()
355363
private tick(): void {
356364
const step = this.activeStep;
357365
if (!step) {
@@ -368,5 +376,3 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
368376
return takeUntil(this._activeStepIdx$.pipe(filter(i => i !== idx)));
369377
}
370378
}
371-
372-
const DESC_EVENTS = Object.getOwnPropertyDescriptor(WorkflowExecution.prototype, 'events')!;

0 commit comments

Comments
 (0)