From 4b5ba4906c3b88a713ccbf3a7ff6c647e7df2b2c Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 4 Oct 2026 19:32:39 -0500 Subject: [PATCH 1/2] feat(classic): add refillable pen ink and book editor guidance --- arch/hfiles | 32 ++++++++++++++++-- arch/intern/skills/inscription.arc | 2 +- arch/intern/skills/inscription/ink_bottle.arc | 17 ++++++++++ .../intern/skills/inscription/writing_pen.arc | 3 ++ docs/ARCHETYPE_PLURALS.md | 8 +++-- maps/light-source-review.json | 6 ++-- maps/shattered_islands/world_0_69 | 15 +++++++++ tools/archetype_plurals.py | 12 ++++--- tools/tests/test_archetype_plurals.py | 33 +++++++++++++++++++ 9 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 arch/intern/skills/inscription/ink_bottle.arc diff --git a/arch/hfiles b/arch/hfiles index 3cb628da2..f379bdf63 100644 --- a/arch/hfiles +++ b/arch/hfiles @@ -59,6 +59,7 @@ List of [a=help]contributors[/a] [title]Miscellaneous[/title] [a=help]drop and take[/a] [a=help]writing pen[/a] +[a=help]ink bottle[/a] endmsg end @@ -281,6 +282,8 @@ The mark command marks a specified item in your inventory. It is, by default, bo Smith will identify your current marked item. A quest reward could require you to mark an item which will be improved. + +Mark a book or letter, then apply a [a=help]writing pen[/a] to edit it. Mark a writing pen, then apply an [a=help]ink bottle[/a] to refill it. endmsg end @@ -1111,6 +1114,8 @@ title Use Skill Command msg The [b]/use_skill[/b] command can be used to use a skill, for example, the [b]construction[/b] skill. Note that many skills cannot be used in this way, for example, melee, archery, etc. +For Inscription, mark an inventory book or letter and apply a [a=help]writing pen[/a]. The old inscription message command is retired. + [title]Syntax:[/title] /use_skill @@ -1153,9 +1158,32 @@ end help writing pen title Writing Pen msg -[book]Inscription Skill[/book]You can apply a writing pen to use the inscription skill in order to write messages into books and letters. You simply mark the object you want to write into and type: +[book]Inscription Skill[/book]With Inscription and Literacy, mark a book or letter in your inventory (B by default), then apply a writing pen. The pen opens an editor with Title and Contents fields. You do not need to keep the pen equipped or type a skill command. You can edit an unsigned book even if someone else originally wrote it. + +[title]Save and Cancel[/title] +Split stacked books or letters into single items before editing or signing. Use a single pen rather than a stack of pens. + +Save commits your title and text. Opening the editor or choosing Cancel changes nothing and uses no ink. The editor shows remaining ink. Each added or changed UTF-8 text byte costs 1 ink unit; unchanged text and deletions are free. Renaming and signing are free. Copying uses the same text cost. If you lack ink or a save fails, your draft remains available. + +[title]Copy book[/title] +Choose a source and destination from your inventory and confirm replacement of the destination's title and contents. The source is preserved. Signed books can be copy sources, but cannot be overwritten. A copy remains unsigned and editable and does not inherit the source's signature or dates. + +[title]Sign and date[/title] +Confirm this action only when you have finished editing. It records your character's name and the in-game date and permanently locks both title and contents. A signed book remains readable and can still be used as a copy source. + +[title]Refill[/title] +Mark a single pen, then apply an [a=help]ink bottle[/a], just as you mark a lamp and apply lamp oil. Split a stack of pens first. A full pen holds 1000 ink units. Pens and ink bottles are sold in Brynknot's shop. + +The old inscription message command is retired; use the pen editor instead. +endmsg +end + +help ink bottle +title Ink Bottle +msg +An ink bottle contains 1000 ink units for a [a=help]writing pen[/a]. Mark a single pen in your inventory (B by default), then apply the bottle. Split a stack of pens first. Only the amount needed to fill the pen is used; any ink left in the bottle is kept. Applying a bottle to a full pen uses no ink. -/use_skill inscription +Pens and ink bottles are sold in Brynknot's shop. Each pen holds 1000 ink units. Saving or copying costs 1 ink unit per added or changed UTF-8 text byte. Unchanged text, deletions, renaming and signing are free. endmsg end diff --git a/arch/intern/skills/inscription.arc b/arch/intern/skills/inscription.arc index f70cd6e24..d37b7778c 100644 --- a/arch/intern/skills/inscription.arc +++ b/arch/intern/skills/inscription.arc @@ -7,7 +7,7 @@ no_drop 1 stand_still 1 level 1 msg -Allows you to write messages in books, parchments, etc. +Mark a book or letter in your inventory and apply a writing pen to edit its title and contents. Requires Literacy. Refill the pen by marking it and applying an ink bottle. Signed books can be read and copied, but cannot be edited. endmsg name_pl inscription end diff --git a/arch/intern/skills/inscription/ink_bottle.arc b/arch/intern/skills/inscription/ink_bottle.arc new file mode 100644 index 000000000..c61a8f911 --- /dev/null +++ b/arch/intern/skills/inscription/ink_bottle.arc @@ -0,0 +1,17 @@ +Object ink_bottle +name ink bottle +race writing_ink +layer 3 +face icon_7soul1_i_ink.101 +type 82 +material 512 +item_condition 100 +item_quality 100 +value 50 +weight 400 +food 1000 +nrof 1 +can_stack 1 +identified 1 +name_pl ink bottles +end diff --git a/arch/intern/skills/inscription/writing_pen.arc b/arch/intern/skills/inscription/writing_pen.arc index 36658ff0c..13bf8afca 100644 --- a/arch/intern/skills/inscription/writing_pen.arc +++ b/arch/intern/skills/inscription/writing_pen.arc @@ -1,11 +1,14 @@ Object writing_pen name writing pen type 52 +race writing_ink layer 3 face writing_pen.101 weight 100 value 500 sp 21 +maxhp 1000 +food 1000 nrof 1 item_condition 100 item_quality 100 diff --git a/docs/ARCHETYPE_PLURALS.md b/docs/ARCHETYPE_PLURALS.md index 9285b6711..10fd5fc71 100644 --- a/docs/ARCHETYPE_PLURALS.md +++ b/docs/ARCHETYPE_PLURALS.md @@ -41,8 +41,12 @@ python3 tools/archetype_plurals.py audit-source-delta --root . The source-delta audit proves that the archetype corpus differs from the reviewed branch baseline only by its 3,559 approved `name_pl` additions. The semantic audit permanently requires exactly one matching plural on every -canonical definition and rejects plurals on the 234 excluded multipart or -nested objects. `python3 tools/validate.py` runs the permanent semantic audit +reviewed canonical definition. New canonical archetypes must have exactly one +explicit, non-empty plural; their ordinary content review owns the vocabulary. +The historical manifest and cross-line proof remain unchanged, and migration +and recovery continue to reject additions to their reviewed catalog. The audit +rejects plurals on excluded multipart or nested objects. +`python3 tools/validate.py` runs the permanent semantic audit and verifies that runtime collection preserves every authored `name_pl` line byte-for-byte. The baseline-bound source-delta audit above is separate one-time delivery evidence so it does not reject legitimate future archetype edits. diff --git a/maps/light-source-review.json b/maps/light-source-review.json index dff6999ce..d0ff49fd4 100644 --- a/maps/light-source-review.json +++ b/maps/light-source-review.json @@ -1522,7 +1522,7 @@ }, "c72bb232af06c8b3fcae2b566674894d27bc71e4c470b36c7d89c898b4d4672e": { "rationale": "Applied runtime review covers torch at active radius 3 with ffc080 light and its Classic clone-derived torch art.", - "semantic_sha256": "e6d72aed5982d61073bdf00dae9d5788a8fef8a0f24027a21d3dfa8f0fb262a9" + "semantic_sha256": "d93d1d0d94fbcf0c15ae45071b1479778c0e9abcff4810f3fd4950df0221f3c1" }, "d4a8b47cfd0bdfa350827fbf3ce1c6e7d7e0b73113da3b0c70443953e9ecedba": { "rationale": "Applied runtime review covers lamp at active radius 5 with ffd080 light and its Classic clone-derived lamp art.", @@ -3435,7 +3435,7 @@ "rationale": "Rendered Brynknot in the strakewood_island batch; neutral map-local fill preserves the outdoor ambient hierarchy while explicit emitters carry the scene's art cues. Reviewed all 2 wall-fireplace placements (direction 1, 7) at midnight with smooth lighting; paired discrete representatives cover both supported directions, wall/corner boundaries, narrow rooms, and dense overlaps. The radius-1 ff6030 default preserves silhouettes and navigation without flattening ambient darkness.", "visible_neutral": {}, "art_overrides": {}, - "semantic_sha256": "279b1aaa868fa498f7ee58ae54e915cdf1af8f876b0c9939b0bff2b932f576f5" + "semantic_sha256": "2474127c8a3990bbd79076355959601d8febc6c4ea8f6fd078ad23a5959a5136" }, "maps/shattered_islands/world_0_69_-1": { "uncolored_disposition": "neutral", @@ -6666,7 +6666,7 @@ "expected_emitting_overlaps": 0, "intentional_non_emitters": {}, "rationale": "All 402 visible magic lanterns own warm-gold illumination: 239 neutral helper sources move onto their fixtures, five duplicate compositions collapse atomically, established heterogeneous radius-3/4/5/6/7/9 footprints remain exact, and the 101 previously dark fixtures inherit form-specific reviewed defaults.", - "semantic_sha256": "6a84ea7b4bf8e8e728e9cabb09111f70f23c04feee67da5d9cb346245813ef95" + "semantic_sha256": "b58b91ffec78d1234e848a1bc02552732c5e05b7099d4d4e65803be9e1a58b74" }, "magic-wall-rune-fixtures": { "archetypes": [ diff --git a/maps/shattered_islands/world_0_69 b/maps/shattered_islands/world_0_69 index f004aa9f9..f478c10b2 100644 --- a/maps/shattered_islands/world_0_69 +++ b/maps/shattered_islands/world_0_69 @@ -2516,6 +2516,13 @@ arch shop_floor x 15 y 8 end +arch writing_pen +unpaid 1 +no_pick 1 +identified 1 +x 15 +y 8 +end arch light4 x 15 y 8 @@ -2705,6 +2712,14 @@ arch shop_floor x 16 y 9 end +arch ink_bottle +unpaid 1 +no_pick 1 +identified 1 +nrof 5 +x 16 +y 9 +end arch shop_floor_normal x 16 y 10 diff --git a/tools/archetype_plurals.py b/tools/archetype_plurals.py index 09db976a5..3e5daec33 100644 --- a/tools/archetype_plurals.py +++ b/tools/archetype_plurals.py @@ -482,20 +482,22 @@ def load_manifest(path: Path) -> Mapping[str, Any]: def _checked_rows( - root: Path, manifest: Mapping[str, Any] + root: Path, manifest: Mapping[str, Any], *, allow_new: bool = False ) -> tuple[dict[str, dict[str, Any]], list[Mapping[str, Any]]]: entries, _ = _canonical_nodes(root) rows = manifest["rows"] by_id = {row["archetype_id"]: row for row in rows} actual_ids = set(entries) expected_ids = set(by_id) - if actual_ids != expected_ids: + if expected_ids - actual_ids or (not allow_new and actual_ids - expected_ids): raise PluralMigrationError( "catalog ID drift: missing={}, unexpected={}".format( sorted(expected_ids - actual_ids), sorted(actual_ids - expected_ids) ) ) for archetype_id, entry in entries.items(): + if archetype_id not in by_id: + continue row = by_id[archetype_id] if entry["singular"] != row["singular"] or entry["object_type"] != row["object_type"]: raise PluralMigrationError( @@ -951,7 +953,7 @@ def recover( def audit(root: Path, manifest: Mapping[str, Any]) -> Mapping[str, Any]: - entries, rows = _checked_rows(root, manifest) + entries, rows = _checked_rows(root, manifest, allow_new=True) expected = {row["archetype_id"]: row["name_pl"] for row in rows} failures = [] canonical = { @@ -962,7 +964,9 @@ def audit(root: Path, manifest: Mapping[str, Any]) -> Mapping[str, Any]: for archetype_id, entry in entries.items(): if len(entry["plurals"]) != 1: failures.append("{} does not have exactly one name_pl".format(archetype_id)) - elif entry["plurals"][0].typed_value != expected[archetype_id]: + elif not str(entry["plurals"][0].typed_value).strip(): + failures.append("{} has an empty name_pl".format(archetype_id)) + elif archetype_id in expected and entry["plurals"][0].typed_value != expected[archetype_id]: failures.append("{} does not match its reviewed name_pl".format(archetype_id)) excluded = 0 for relative, document in documents.items(): diff --git a/tools/tests/test_archetype_plurals.py b/tools/tests/test_archetype_plurals.py index 0a8874890..db66a647a 100644 --- a/tools/tests/test_archetype_plurals.py +++ b/tools/tests/test_archetype_plurals.py @@ -91,6 +91,39 @@ def setUp(self) -> None: def tearDown(self) -> None: self.temporary.cleanup() + def test_audit_allows_new_explicit_plurals_without_rewriting_migration(self) -> None: + migrate(self.root, self.manifest, apply=True, check_git=False) + (self.root / "arch" / "ink.arc").write_text( + "Object ink_bottle\nname ink bottle\ntype 82\nname_pl ink bottles\nend\n", + encoding="utf-8", + ) + self.assertEqual(3, audit(self.root, self.manifest)["canonical_name_pl"]) + with self.assertRaisesRegex(PluralMigrationError, "catalog ID drift"): + migrate(self.root, self.manifest, check_git=False) + + def test_audit_rejects_new_missing_empty_or_duplicate_plurals(self) -> None: + migrate(self.root, self.manifest, apply=True, check_git=False) + for plural in ("", "name_pl \n", "name_pl inks\nname_pl inks\n"): + with self.subTest(plural=plural): + (self.root / "arch" / "ink.arc").write_text( + "Object ink_bottle\nname ink bottle\ntype 82\n" + plural + "end\n", + encoding="utf-8", + ) + with self.assertRaises((PluralMigrationError, ContentCoreError)): + audit(self.root, self.manifest) + + def test_audit_still_rejects_removed_reviewed_archetypes(self) -> None: + migrate(self.root, self.manifest, apply=True, check_git=False) + source = self.root / "arch" / "objects.arc" + source.write_text( + source.read_text(encoding="utf-8").replace( + "Object fallback\ntype 79\nname_pl fallbacks\nend\n", "" + ), + encoding="utf-8", + ) + with self.assertRaisesRegex(PluralMigrationError, "catalog ID drift"): + audit(self.root, self.manifest) + def test_inventory_selects_canonical_definitions_only(self) -> None: report = inventory(self.root) From f7c0bcb9de33edc1fbf52fcf0616f9e72f2c8cf2 Mon Sep 17 00:00:00 2001 From: Zoey Rose Date: Sun, 4 Oct 2026 20:49:54 -0500 Subject: [PATCH 2/2] docs(classic): explain retained writing drafts and refill recovery --- arch/hfiles | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arch/hfiles b/arch/hfiles index f379bdf63..100ee005b 100644 --- a/arch/hfiles +++ b/arch/hfiles @@ -1163,7 +1163,9 @@ msg [title]Save and Cancel[/title] Split stacked books or letters into single items before editing or signing. Use a single pen rather than a stack of pens. -Save commits your title and text. Opening the editor or choosing Cancel changes nothing and uses no ink. The editor shows remaining ink. Each added or changed UTF-8 text byte costs 1 ink unit; unchanged text and deletions are free. Renaming and signing are free. Copying uses the same text cost. If you lack ink or a save fails, your draft remains available. +Save commits your title and text. Opening the editor changes nothing and uses no ink. Cancel, Escape or X closes without saving and keeps your draft while you remain connected. Disconnecting clears unsaved drafts. The editor shows remaining ink. Each added or changed UTF-8 text byte costs 1 ink unit; unchanged text and deletions are free. Renaming and signing are free. Copying uses the same text cost. If you lack ink or a save fails, your draft remains available. + +To refill while keeping your draft, close the editor, mark the pen and apply an ink bottle. Then mark the same book or letter again and apply the pen to resume your draft. If the book has changed since you started editing, review the draft against its current contents before saving again. Replacing a draft by choosing another destination or copying asks for confirmation. [title]Copy book[/title] Choose a source and destination from your inventory and confirm replacement of the destination's title and contents. The source is preserved. Signed books can be copy sources, but cannot be overwritten. A copy remains unsigned and editable and does not inherit the source's signature or dates.