Skip to content

Latest commit

 

History

History
312 lines (236 loc) · 14.6 KB

File metadata and controls

312 lines (236 loc) · 14.6 KB

Rebuilding ACG

ACG is a Rolemaster character generator written in Delphi in 1998. This is a rebuild of it for the browser, so that it can be modified again.

Goal for v1: faithful reproduction. Same rules, same behaviour, same character sheet. The user interface may be modernised; the numbers on the page may not change. New features come after it works.


Two generations, and which one matters

ACG 4.2 (September 2003) is the target. It is what the campaign actually uses -- the live characters are dated December 2025 -- and it is a single self-contained XML file per character rather than the family of five that 3.x wrote.

No source code for ACG 4 survives. Nothing on this machine: no Delphi sources outside the two 3.x trees, no SourceSafe repository, no archive. That matters much less than it sounds, for three reasons:

  • The rules arithmetic did not change. ACG 4's character files carry exactly the fields 3.x computed with -- rank, adolescence, cost, development, st, special1, special2, item, profession -- and its database uses the same column names.
  • The format is XML and self-describing, so it needs no source to read.
  • ACG 4 ships its own acg.mdb, which extracts with the same tooling.

So the 3.x source remains the specification for the maths, verified by the Aglarond golden master, while ACG 4 supplies the data and the file format.

Generation Character file Identifiers Source
ACG 3.10 (1999) .acg key/value + four siblings integers complete
ACG 4.2 (2003) one XML document GUIDs none

What ACG 4 changed

  • Skills are nested inside their category rather than following it with a five-space indent. Strictly better: the parent is explicit, not positional.
  • Skills carry their own stats expression, for the optional stat system -- which every live character has switched on.
  • A mm field per row, and several training packages rather than one.
  • Height in centimetres and weight in grams, converted on import.
  • Resistances keyed by name, and written heat-before-cold -- the opposite of 3.10's order, so they must be mapped by name and never by position.
  • Substantially more data: 379 skills against 315, 64 races against 49, 57 professions against 44, 5,638 spells against 3,118.

What we are rebuilding from

Two 3.x source trees survive, and they play different roles.

Tree Role
OldAcg/Source/frmChar.pas Authority for behaviour. 3,106 lines, monolithic, complete. Matches Release/acg.exe v3.10 and the shipped character files.
Source/*.pas Authority for rules math. The same logic, split into units and easier to read.
Source/acg.mdb The data. Normalized and ID-keyed, with the four Instr* junction tables.

Source/ is a work in progress that was abandoned mid-refactor: the monolithic form was being split into PtSubform subforms, and Load and Save were commented out along the way. That build cannot open a character file. Where the two trees disagree about behaviour, OldAcg wins.

Where they disagree about rules, they do not: StatBonus and GetRankBonus are byte-identical across both trees, modulo trailing comments.

The golden master

Aglarond-Skills-.dat is 118 rows x 13 columns, and column 0 is the computed skill total. It was written by the working 3.10 binary in February 1999.

That file is a complete regression suite for the entire category-to-skill bonus cascade. Phase 1 is not finished until all 118 totals reproduce exactly.


Architecture

A static, local-first web application. No backend, no accounts, no database server. All rules evaluation happens in the browser.

packages/rules      pure TypeScript, zero dependencies -- the engine
packages/pack       pack schema and validation
packages/import     readers for the 1999 file formats
apps/web            React + Vite
tools/              .mdb extraction and pack building (build time only)
packs/              rules packs (data, shipped separately from the app)

The application ships without rules data. A pack is a file the user loads, cached locally thereafter. This is what makes hosting and bring-your-own-rules the same design rather than opposing ones, and it keeps the ACG application (which we own) separate from the game system content (which we do not).

One deliberate deviation from the original

The 1999 code keyed everything by display string — it looked skills up with SELECT categoryid, stats FROM category WHERE Category='<caption>', and told a skill from a category by whether the caption began with five spaces. That is the direct cause of the "Unable to find the category" errors in the FAQ.

The rebuild uses stable IDs and an explicit kind: 'category' | 'skill'. Names are for display only. This changes no number on any sheet; it deletes a class of bugs.


Phases

Phase 0 — Extraction and scaffold — done

  • Both .mdb files extracted with a pure-Python Jet reader: 12,037 rows in 15 tables for 3.10, 19,495 in 31 tables for 4.2
  • Two rules packs built and validated, one per generation
  • Identifiers normalised to strings, so the engine does not care whether a pack came from the integer era or the GUID one
  • Two defects found in the 1999 data, recorded rather than corrected
  • Workspace, types, and pack conformance tests (8 passing)
  • Golden-master fixtures copied into fixtures/aglarond/

Phase 1 — The rules engine, headless — done

Ported one-to-one from the Pascal. Pure functions, no UI, no I/O, 62 tests.

  • statBonus — the piecewise band table
  • evaluateStatExpression+ sums, / averages and rounds half up
  • rankCount — Re/St/Ev/Oc weighting, integer-divided by two
  • rankBonus — progressions in bands of ten, truncated per band
  • costOfRanks — per-rank costs, returning 0 once exhausted
  • the category-to-skill cascade, and development points
  • the twelve resistance rolls, stride, and the derived block
  • the package language — NO / MO / MR over Rank / ST / SP / RR

Gate met: all 118 of Aglarond's skill totals reproduce exactly, along with the stat bonuses and every derived total the 1999 binary stored in the file.

Two things worth remembering:

  • Body Development and Power Point Development are each both a category and a skill. GetSkill prefixed lookups with the five-space indent to disambiguate; the rebuild filters on kind. Getting this wrong reads the category's total and silently gives the wrong hits and power points. It is also a live bug in Source/Instr.pas, which dropped the prefix that OldAcg/Source/Instr.pas:260 still has — more evidence that OldAcg is the behavioural authority.
  • Math.trunc can return negative zero, which Pascal integers cannot. Left alone a sheet prints "-0". trunc normalises it.
  • The optional stat system replaces the last term of a category's stat expression with the skill's own primary stat, for that skill only. Validated against Suneseren's Stalking: 36 with SD, 29 with In, a difference of exactly SD minus In. With the flag off the calculation is identical to 3.10's, which the Aglarond golden master still proves.
  • Actions and fields are compared case-insensitively, because ParseInstruction used CompareText. Not academic: ACG 4's data contains a row whose action is spelled no in lower case, which an exact comparison drops in silence.

Phase 2 — Character I/O — done

  • Readers for .acg, -Skills-.dat, -Notes.dat and the CSVs
  • The column shift Load applies to files at version 3 or below
  • ACG 4 XML reader, with the format detected rather than asked for
  • A native .acgj format: one JSON file, versioned, refusing to half-read anything newer than it understands

Import only. Exporting back to the 1999 format is not worth building.

Phase 3 — Interface — first pass done

React and Vite, static build, no backend. The original tab structure kept -- General, Stats, Points, Traits, Items, Skills, Notes -- because it maps to both the domain and the printed sheet.

  • Pack gate: the app ships with no game data and asks for a pack once, remembering it in IndexedDB

  • Import a 1999 character by selecting its family of files

  • Open and save .acgj, saving in place where the browser allows it

  • All seven tabs, every field editable, recalculating live

  • Skill filtering that never reorders rows and keeps a matched skill's category visible

  • Undo/redo, and a warning before losing unsaved work

  • Render tests: the screens show the engine's numbers, not their own

  • The Items tab: labelled entries with skill and rank lookups, flat bonuses, a computed total, and range tables for ranged entries

Still to do in this phase:

  • The A/B defensive bonus grids are stored and round-trip, but their subtotals are not yet computed
  • Training mode and its password gate
  • Adding and deleting skill rows from the UI

Phase 3.5 — What the bundle may contain

The build is checked to hold no game data: not a skill name, not a race, not a spell. Two leaks were found and closed this way -- the three skill names the derived block looks up were hard-coded in the engine, and one had also been copied into a tooltip. Both now come from the pack, which means renaming a skill in a pack can no longer silently break hits or power points.

Phase 4 — The character sheet

The layout is recovered in full from ACG 4's .rtm templates, which are binary Delphi component streams: 353 components on A4, positioned in millimetres, 81 static labels and 259 data fields.

Of the calculations the sheet needs, all are now specified except three:

  • The movement and moving-manoeuvre grids. Not a rulebook lookup after all -- Report1.pas computes both. Twelve columns of weight-allowance units, one unit being a tenth of body weight, each costing -8. Validated against Sections 17.3 and 22.1.

  • Hit and power point exhaustion thresholds, including the frenzied row.

  • The Unc (KO) and Dea (Dead) columns, and the rounds of grace once dying. Validated against the rulebook's own worked example.

  • The three Spell Preparation figures, from table T-4.6.

  • The rendering: HTML and a print stylesheet, previewed on screen at page proportions before printing.

Not a millimetre-for-millimetre copy of the 1999 layout, deliberately. The sections are the original's and in roughly its order, but laid out with CSS rather than absolute positions, so a character with forty skills and one with three hundred both print sensibly. Pages a character has nothing for -- no items, no notes -- are left out rather than printed empty.

Markdown was considered and rejected: it has no page model at all. No @page, no page breaks, no control of margins or columns, and it has to become HTML to print regardless.

A4 and Letter from one layout. ACG 4 shipped two templates -- Character2A4 and Character2Letter -- because a layout of absolute positions cannot be anything else. A CSS layout can serve both, provided the content is built to the intersection: A4 is the narrower at 210mm, Letter the shorter at 279mm, so anything fitting 210 x 279 fits either. The page box follows the chosen paper; the content is held to the narrower one regardless, so reprinting on the other moves nothing. The choice is remembered, and first guessed from the locale.

Report1Report6 become HTML and a print stylesheet, reproducing the 1999 layout. Report6 is the only consumer of the spell data: it scans the skill list for entries named Spell List <name>, joins to SpellList and Spells, and prints the spell sheets. No spell editing UI in v1.

Phase 5 — Wizard, packages, training

Character creation (class, realm, race, weapon preferences, adolescence) and package application. A thin layer over Phase 1.

Phase 6 — Packs and the GM editor

Pack loading and local caching. An in-app table editor — DataView.pas's "GM Tools", except it exports JSON instead of requiring Access and the Paradox export filters.

Phase 7 — Ship

Static hosting, offline support, no backend.


Known data gaps in ACG 4, recorded not corrected

  • Profession Criminal has no cost rows at all, so it cannot be costed.
  • 21 MO instructions in the modern-setting content name neither a category nor a skill. They report an unfound skill and are skipped, as the original did.

The battle simulator

Fights the same battle a few hundred times against a chosen opponent, at several settings of offensive bonus traded into defensive, and shows the curve. The question it answers -- whether that trade is worth making -- is one no amount of staring at a character sheet will tell you.

It is a model, not the rules. Rolemaster resolves an attack through a table per weapon, twenty armour columns wide, and then a critical table by kind and severity. None of that data is in either ACG database and none is implemented: attacks resolve through a documented approximation, and the opponents are estimates. What is real is everything the character brings -- offensive bonus from a weapon skill, defensive bonus from the defence matrix, hits, and the exhaustion penalties as those hits are taken -- along with the open-ended d100, which is what gives the system its long tails.

Real attack tables now load. The Broadsword table is extracted from Arms Law -- 99 rows, results 30 to 150, twenty armour columns, 1,980 cells -- and where one is supplied the approximation is not consulted at all. Extraction rather than transcription: the tables are text in the PDF, so no cell is typed by hand, and the extractor checks its own work.

A weapon does not necessarily have its own table. The Weapon Statistics Chart maps each onto one plus a modifier that varies by armour band, so a bastard sword swung one-handed is the Broadsword table at +5 / 0 / -5 / -10 / -10, and two-handed it is the 2H-Sword table capped at 140.

One approximation remains: the critical tables are a second body of data, twelve of them by kind and severity, and none is loaded. A critical is counted as a quantity of hits by severity so that it registers rather than being ignored, which would badly understate every weapon that delivers them.

Runs are seeded: the same seed fights the same battles, so a sweep compares the settings rather than two different sets of dice.

Deferred until it works

Anything that changes the rules or adds surface area: spell management screens, sheet redesign, a newer edition, house-rule support beyond what packs already allow.