Skip to content

Commit

Permalink
feat: Offline support
Browse files Browse the repository at this point in the history
Closes #142
  • Loading branch information
Alorel committed Dec 29, 2022
1 parent 82982dd commit 3f0c087
Show file tree
Hide file tree
Showing 20 changed files with 399 additions and 111 deletions.
4 changes: 1 addition & 3 deletions src/actions/combat/start-combat-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ interface Init extends Omit<ActionNodeDefinition<Props>, 'namespace' | 'category
registry: TypedKeys<Game, NamespaceRegistry<CombatArea>>;
}

function execMob({area, mob}: Props) {
game.stopActiveAction();
function execMob({area, mob}: Props): void {
game.combat.selectMonster(area.monsters[mob!], area);
}

Expand Down Expand Up @@ -104,7 +103,6 @@ mkAction({
</Fragment>
),
execute({area}) {
game.stopActiveAction();
game.combat.selectDungeon(area as Dungeon);
},
label: 'Start Dungeon',
Expand Down
5 changes: 3 additions & 2 deletions src/actions/core/delay-action.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {noop} from 'lodash-es';
import {Fragment} from 'preact';
import {map, noop, timer} from 'rxjs';
import {map, timer} from 'rxjs';
import {InternalCategory} from '../../lib/registries/action-registry.mjs';
import {defineLocalAction} from '../../lib/util/define-local.mjs';

Expand All @@ -21,7 +22,7 @@ defineLocalAction<Props>({
media: cdnMedia('assets/media/main/timer.svg'),
options: [
{
description: 'Wait for the given number of milliseconds. The actual wait may be significantly longer if the game is minimised in the browser.',
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.',
label: 'Duration',
localID: 'duration',
min: 0,
Expand Down
11 changes: 5 additions & 6 deletions src/actions/lib/recipe-action.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {nextComplete} from '@aloreljs/rxutils';
import {noop} from 'lodash-es';
import type {GatheringSkill} from 'melvor';
import {noop, Observable} from 'rxjs';
import {Observable} from 'rxjs';
import PersistClassName from '../../lib/decorators/PersistClassName.mjs';
import type {Obj} from '../../public_api';
import type {SkillActionInit} from './skill-action.mjs';
Expand Down Expand Up @@ -86,11 +87,9 @@ export class RecipeAction<T extends object, S extends Gathering, R>

/** @inheritDoc */
public execute(data: T): Observable<void> {
return new Observable<void>(s => {
const prep = this.prepareExec(data);
game.stopActiveAction();
this.exec(data, prep);
nextComplete(s);
return new Observable<void>(subscriber => {
this.exec(data, this.prepareExec(data));
nextComplete(subscriber);
});
}
}
Expand Down
10 changes: 2 additions & 8 deletions src/actions/start-skill/agility.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {nextComplete} from '@aloreljs/rxutils';
import type {Agility} from 'melvor';
import {Observable} from 'rxjs';
import {defineAction} from '../../lib/api.mjs';
import PersistClassName from '../../lib/decorators/PersistClassName.mjs';
import {InternalCategory} from '../../lib/registries/action-registry.mjs';
Expand All @@ -10,12 +8,8 @@ import SkillAction from '../lib/skill-action.mjs';
class AgilityAction extends SkillAction<{}, Agility> {

/** @inheritDoc */
public override execute(): Observable<void> {
return new Observable(subscriber => {
game.stopActiveAction();
this.skill.start();
nextComplete(subscriber);
});
public override execute(): void {
this.skill.start();
}
}

Expand Down
51 changes: 21 additions & 30 deletions src/actions/start-skill/alt-magic.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {AltMagicSpell, FoodItem, Item, SingleProductArtisanSkillRecipe} from 'melvor';
import {asyncScheduler, Observable, scheduled} from 'rxjs';
import {defineAction} from '../../lib/api.mjs';
import {InternalCategory} from '../../lib/registries/action-registry.mjs';
import {namespace} from '../../manifest.json';
Expand All @@ -22,37 +21,29 @@ interface Props {

defineAction<Props>({
category: InternalCategory.START_SKILL,
execute: ({bar, food, junk, item, recipe, superGem}) => (
scheduled(
new Observable<void>(subscriber => {
game.stopActiveAction();
magic.selectSpellOnClick(recipe);
execute({bar, food, junk, item, recipe, superGem}) {
magic.selectSpellOnClick(recipe);

switch (recipe.specialCost.type) {
case AltMagicConsumptionID.AnyItem:
magic.selectItemOnClick(item!);
break;
case AltMagicConsumptionID.AnyNormalFood:
magic.selectItemOnClick(food!);
break;
case AltMagicConsumptionID.JunkItem:
magic.selectItemOnClick(junk!);
break;
case AltMagicConsumptionID.BarIngredientsWithoutCoal:
case AltMagicConsumptionID.BarIngredientsWithCoal:
magic.selectBarOnClick(bar!);
break;
case AltMagicConsumptionID.AnySuperiorGem:
magic.selectItemOnClick(superGem!);
}
switch (recipe.specialCost.type) {
case AltMagicConsumptionID.AnyItem:
magic.selectItemOnClick(item!);
break;
case AltMagicConsumptionID.AnyNormalFood:
magic.selectItemOnClick(food!);
break;
case AltMagicConsumptionID.JunkItem:
magic.selectItemOnClick(junk!);
break;
case AltMagicConsumptionID.BarIngredientsWithoutCoal:
case AltMagicConsumptionID.BarIngredientsWithCoal:
magic.selectBarOnClick(bar!);
break;
case AltMagicConsumptionID.AnySuperiorGem:
magic.selectItemOnClick(superGem!);
}

magic.castButtonOnClick();

subscriber.complete();
}),
asyncScheduler
)
),
magic.castButtonOnClick();
},
label: 'Start Alt. Magic',
localID: 'startAltMagic',
media: magic.media,
Expand Down
48 changes: 48 additions & 0 deletions src/lib/data-update.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type {Workflow} from './data/workflow.mjs';
import update0001 from './updates/update-0001.mjs';

export interface SerialisedWorkflow extends Pick<Workflow, 'name' | 'rm'> {
steps: [string[], any[][]];
}

export type DataUpdateFn = (workflows: SerialisedWorkflow[]) => void;

export interface RunUpdatesResult {

/** Whether at least one update got applied or not */
applied: boolean;

/** The data version for this mod version */
update: number;
}

function getUpdatesArray(): DataUpdateFn[] {
return [
update0001,
];
}

/**
* Run data updates when the storage format changes to avoid users having to redefine all their workflows
* @param dataVersion The current data version
* @param data The raw loaded data
* @return Whether at least one update got applied or not
*/
export function runUpdates(dataVersion: number, data: SerialisedWorkflow[]): RunUpdatesResult {
const updateFns = getUpdatesArray();

// The version defaults to -1 - add 1 to get array index 0
const firstIdx = dataVersion + 1;
for (let i = firstIdx; i < updateFns.length; ++i) {
updateFns[i](data);
}

return {
applied: firstIdx < updateFns.length,
update: updateFns.length - 1,
};
}

export function getUpdateNumber(): number {
return getUpdatesArray().length - 1;
}
8 changes: 7 additions & 1 deletion src/lib/data/workflow.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {FormatToJsonArrayCompressed} from '../decorators/to-json-formatters/form
import type {FromJSON, ToJSON} from '../decorators/to-json.mjs';
import {JsonProp, Serialisable} from '../decorators/to-json.mjs';
import type {ReadonlyBehaviorSubject} from '../registries/workflow-registry.mjs';
import WorkflowRegistry from '../registries/workflow-registry.mjs';
import {WorkflowStep} from './workflow-step.mjs';

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

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

Expand Down
32 changes: 19 additions & 13 deletions src/lib/execution/workflow-execution.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import {nextComplete} from '@aloreljs/rxutils';
import {logError} from '@aloreljs/rxutils/operators';
import type {MonoTypeOperatorFunction, Observer, Subscription, TeardownLogic} from 'rxjs';
import {
asapScheduler,
asyncScheduler,
BehaviorSubject,
concat,
defer,
Expand All @@ -16,7 +14,6 @@ import {
map,
Observable,
of,
scheduled,
startWith,
takeUntil,
tap
Expand All @@ -32,6 +29,7 @@ import WorkflowRegistry from '../registries/workflow-registry.mjs';
import {debugLog, errorLog} from '../util/log.mjs';
import prependErrorWith from '../util/rxjs/prepend-error-with.mjs';
import ShareReplayLike from '../util/share-replay-like-observable.mjs';
import {stopAction} from '../util/stop-action.mjs';
import type {
ActionExecutionEvent,
StepCompleteEvent,
Expand All @@ -43,6 +41,8 @@ import {WorkflowEventType} from './workflow-event.mjs';

type Out = WorkflowEvent;

const DESC_EVENTS = Object.getOwnPropertyDescriptor(ShareReplayLike.prototype, 'events')!;

/** Represents a workflow in the middle of being executed */
@PersistClassName('WorkflowTrigger')
export class WorkflowExecution extends ShareReplayLike<Out> {
Expand Down Expand Up @@ -79,7 +79,7 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
this.activeStepIdxManual = false;
++this.activeStepIdx;
}
this.mainSub = asapScheduler.schedule(this.tick);
this.tick();
},
error: (e: Error) => {
const evt = this.mkCompleteEvent(false, e.message);
Expand All @@ -94,9 +94,12 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
},
};

public constructor(public readonly workflow: Workflow) {
public constructor(
public readonly workflow: Workflow,
step = 0
) {
super();
this._activeStepIdx$ = new BehaviorSubject<number>(0);
this._activeStepIdx$ = new BehaviorSubject<number>(step);
this.activeStepIdx$ = this._activeStepIdx$.asObservable();
}

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

public get isFinished(): boolean {
return this.finished;
}

public get running(): boolean {
return !this.finished;
}
Expand Down Expand Up @@ -161,6 +168,7 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
}
this.finished = false;
}

this.tick();
}

Expand Down Expand Up @@ -231,7 +239,9 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
return EMPTY;
}

return concat(...step.actions.map((_, i) => this.executeAction(step, stepIdx, i)));
return stopAction().pipe(
switchMap(() => concat(...step.actions.map((_, i) => this.executeAction(step, stepIdx, i))))
);
}

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

const trigger$ = step.trigger.listen().pipe(take(1));

const exec$: Observable<Out> = scheduled(trigger$, asyncScheduler)
const exec$: Observable<Out> = step.trigger.listen()
.pipe(
take(1),
switchMap(() => this.executeActions(step, stepIdx)),
logError(`Error executing step ${stepIdx} in workflow ${this.workflow.name}:`),
prependErrorWith(e => of<StepCompleteEvent>({
Expand Down Expand Up @@ -351,7 +360,6 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
}

/** Main ticking function for the workflow */
@BoundMethod()
private tick(): void {
const step = this.activeStep;
if (!step) {
Expand All @@ -368,5 +376,3 @@ export class WorkflowExecution extends ShareReplayLike<Out> {
return takeUntil(this._activeStepIdx$.pipe(filter(i => i !== idx)));
}
}

const DESC_EVENTS = Object.getOwnPropertyDescriptor(WorkflowExecution.prototype, 'events')!;
Loading

0 comments on commit 3f0c087

Please sign in to comment.