Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions arch/hfiles
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 <skill name>

Expand Down Expand Up @@ -1153,9 +1158,34 @@ 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 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.

[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 <message to write>
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

Expand Down
2 changes: 1 addition & 1 deletion arch/intern/skills/inscription.arc
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions arch/intern/skills/inscription/ink_bottle.arc
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions arch/intern/skills/inscription/writing_pen.arc
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 6 additions & 2 deletions docs/ARCHETYPE_PLURALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions maps/light-source-review.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": [
Expand Down
15 changes: 15 additions & 0 deletions maps/shattered_islands/world_0_69
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions tools/archetype_plurals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 = {
Expand All @@ -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():
Expand Down
33 changes: 33 additions & 0 deletions tools/tests/test_archetype_plurals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading