Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CryZe/asr-assemblyscript/llms.txt

Use this file to discover all available pages before exploring further.

Watcher classes eliminate the boilerplate of tracking memory value changes across ticks. Rather than manually reading a value, storing a copy from the previous tick, and comparing the two, each watcher manages the current / old pair for you. Call update() once per tick with your process handle, then read changed, current, or old to react to state transitions. Watchers also handle module-relative address resolution internally, so you only supply a module name and a relative offset once — at construction time.
import { I32Watcher, StringWatcher } from 'asr-assemblyscript/watcher';
All watcher classes are named exports. Import only the types you need.
Watcher instances must be declared at module level (outside your update() function) so they persist across ticks. Re-creating a watcher every tick resets old to zero on every call, making changed meaningless and defeating the entire purpose of the abstraction.

Shared Watcher Interface

All typed watcher classes expose the same properties and method:
MemberTypeDescription
currentTThe most recently read value from game memory.
oldTThe value read during the previous update() call.
changedbool (getter)true when current !== old after the most recent update().
update(processId)boolReads memory, shifts currentold, stores the new value in current, and returns whether the value changed.

update method

update(processId: Process.ProcessId): bool
processId
Process.ProcessId
required
The handle returned by Process.attach(). The watcher uses this to call Process.getModuleAddress() and Process.read() internally.
Returns: booltrue if the value changed this cycle; false otherwise (or if the read failed).

Numeric and Boolean Watcher Classes

All of the following classes share the same constructor signature:
constructor(moduleName: string, address: Process.Address)
moduleName
string
required
The name of the module (executable or DLL) whose base address will be resolved via Process.getModuleAddress() each tick, e.g. "Celeste.exe".
address
Process.Address
required
The relative offset from the module’s base address to the memory location you want to read. The watcher adds this to the resolved module base internally.
ClassAS TypeBuffer size
BoolWatcherbool1 byte
I8Watcheri81 byte
I16Watcheri162 bytes
I32Watcheri324 bytes
I64Watcheri648 bytes
ISizeWatcherisize4 bytes
U8Watcheru81 byte
U16Watcheru162 bytes
U32Watcheru324 bytes
U64Watcheru648 bytes
USizeWatcherusize4 bytes
F32Watcherf324 bytes
F64Watcherf648 bytes

StringWatcher

StringWatcher tracks a fixed-length string in game memory. Because a string requires a length and an encoding to be decoded correctly, its constructor takes two additional parameters compared to the numeric watcher classes.
constructor(moduleName: string, address: Process.Address, length: u32, useUTF16: bool = false)
moduleName
string
required
The module name whose base address is resolved each tick, e.g. "GameAssembly.dll".
address
Process.Address
required
The relative offset from the module’s base address to the start of the string buffer in memory.
length
u32
required
The number of bytes to read from memory. For UTF-8 strings this is the byte length; for UTF-16 strings this is also the byte length (i.e. twice the character count).
useUTF16
bool
When true, the raw bytes are decoded as UTF-16. When false (the default), they are decoded as null-terminated UTF-8. Most game strings are UTF-8; use true for games that store strings in UTF-16 (common in Unity/Windows games using wchar_t or C# string representations).
The current and old properties are both string. The changed getter and update() method work identically to the numeric watchers.

Complete Usage Example

import 'asr-assemblyscript/runtime';
import * as Process from 'asr-assemblyscript/process';
import * as Timer   from 'asr-assemblyscript/timer';
import { I32Watcher, StringWatcher } from 'asr-assemblyscript/watcher';

// Declare watchers at module level so current/old persist across ticks
const levelId   = new I32Watcher("Celeste.exe", 0x001A2B3C);
const levelName = new StringWatcher("Celeste.exe", 0x001A2B50, 64); // 64 UTF-8 bytes

let processId: Process.ProcessId = 0;

export function update(): void {
  // Manage process handle
  if (processId === 0 || !Process.isOpen(processId)) {
    if (processId !== 0) Process.detach(processId);
    processId = Process.attach("Celeste.exe");
    if (processId === 0) return;
  }

  // Update watchers — each call resolves module base + reads memory
  levelId.update(processId);
  levelName.update(processId);

  // React to changes
  if (levelId.changed) {
    // levelId.old holds the previous value, levelId.current the new one
    Timer.split();
  }

  if (levelName.changed) {
    // Can compare strings directly
    if (levelName.current === "Forsaken City") {
      Timer.start();
    }
  }
}

Build docs developers (and LLMs) love