@px-lsp/protocol 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/protocol.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  // unused-vars analysis does not see.
11
11
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
12
12
  import type { IndexStats } from "./types";
13
+ import type { DefSource } from "./types";
13
14
 
14
15
  /** Resolved extension settings, computed client-side (path validation, Steam
15
16
  * detection fallbacks, workspace-folder default) and pushed to the server. */
@@ -191,6 +192,40 @@ export interface LocEntryInfo {
191
192
  value?: string;
192
193
  }
193
194
 
195
+ /**
196
+ * Request: a localization value as the PLAYER reads it;
197
+ * {@link LocTextParams} -> {@link LocTextResult}.
198
+ *
199
+ * {@link lookupLocRequest} answers the value verbatim, which is what an editor
200
+ * needs. A panel that SHOWS the value needs the sentence: the games write a
201
+ * culture parameter as `"The [GetTrait('rough_terrain_expert').GetName(
202
+ * GetNullCharacter )] Commander Trait is more common"` (145 of the 280
203
+ * parameter values with a real call take that one shape), and a modder reading
204
+ * a form must not be shown the brackets.
205
+ *
206
+ * Everything the renderer knows is DERIVED: the words come from the loc index
207
+ * (mod entries shadow the game's), the kind a `Get<Something>('name')` chain
208
+ * names comes from the definition index, and the loc key that kind's names take
209
+ * comes from the active profile's schema. No table of function names, so a
210
+ * workspace of any of the games gets the same behavior from its own schema.
211
+ */
212
+ export const locTextRequest = "paradox/locText";
213
+ export interface LocTextParams extends ModScopedParams {
214
+ keys: string[];
215
+ }
216
+ export interface LocTextValue {
217
+ /** The value verbatim, exactly as {@link lookupLocRequest} answers it. */
218
+ raw: string;
219
+ /** The same value as plain text: markup stripped, datafunctions resolved. */
220
+ text: string;
221
+ /** False when any part of the value stayed a word for something unresolved. */
222
+ resolved: boolean;
223
+ }
224
+ export interface LocTextResult {
225
+ /** Loc key -> its rendering. A key the loc index cannot find is ABSENT. */
226
+ values: Record<string, LocTextValue>;
227
+ }
228
+
194
229
  // ---- server -> client ------------------------------------------------------
195
230
 
196
231
  /** Notification: data health for the status bar; payload {@link StatusPayload}. */
@@ -241,6 +276,20 @@ export interface OverviewDef {
241
276
  name: string;
242
277
  file: string;
243
278
  line: number;
279
+ /**
280
+ * The loc-resolved display name, when the kind's loc pattern resolves to one
281
+ * ({@link EventVocabularyItem.label}). Set by {@link definitionFormRequest}
282
+ * only, so a creator listing what the mod already has can show the player's
283
+ * word for it; absent everywhere else.
284
+ */
285
+ label?: string;
286
+ /**
287
+ * Where the definition comes from. Set by {@link definitionFormRequest} only,
288
+ * whose list includes the game's and a dependency's definitions (a creator
289
+ * opens one to duplicate or override it) and must say which is which; absent
290
+ * everywhere else, where every definition listed is the mod's own.
291
+ */
292
+ source?: "vanilla" | "parent" | "mod";
244
293
  }
245
294
  export interface OverviewKind {
246
295
  kind: string;
@@ -1364,6 +1413,13 @@ export interface EventGraphParams {
1364
1413
  /** Also read each mod event's `theme`. Off by default: it costs one parse per
1365
1414
  * event file, and only a client that draws the theme's art needs it. */
1366
1415
  themes?: boolean;
1416
+ /**
1417
+ * Leave out every definition that has no edge in the answer. ON by default
1418
+ * (absent = true): the pruned definitions are dropped before their cards
1419
+ * are read, so a mod with hundreds of standalone events stays cheap. `root`
1420
+ * is always kept. Send `false` to see the whole namespace, edges or not.
1421
+ */
1422
+ connectedOnly?: boolean;
1367
1423
  }
1368
1424
  /**
1369
1425
  * One row of a mod event's card, in EXECUTION order (immediate, then the
@@ -1488,11 +1544,41 @@ export interface EventVocabularyItem {
1488
1544
  doc?: string;
1489
1545
  /** Dimmer right-hand label: where the value comes from (mod / vanilla / a kind). */
1490
1546
  hint?: string;
1547
+ /**
1548
+ * The name the PLAYER reads: the loc value of the kind's first loc pattern
1549
+ * with `$` replaced by the definition name (`trait_$` -> `trait_brave` ->
1550
+ * "Brave"). Set by {@link definitionFormRequest} only, and absent when
1551
+ * nothing resolves, so a client shows the key rather than an invented word.
1552
+ */
1553
+ label?: string;
1554
+ /**
1555
+ * The family this definition belongs to, when one folder holds several and
1556
+ * the schema entry names the key that says so (`type = ethos` in
1557
+ * common/culture/pillars). Set by {@link definitionFormRequest} only, so a
1558
+ * creator can draw one picker per family; absent everywhere else.
1559
+ */
1560
+ group?: string;
1491
1561
  }
1492
1562
  /** Caps: an editor lists a page at a time, and these ride on every open. */
1493
1563
  export const EVENT_VOCABULARY_MAX_TOKENS = 600;
1494
1564
  export const EVENT_VOCABULARY_MAX_VALUES = 400;
1495
1565
 
1566
+ /**
1567
+ * Most values {@link DefinitionFormKey.sampled} carries, and the point past
1568
+ * which a key is taken to have no value SET at all (a key whose value differs
1569
+ * per definition is a free field, not a list to offer).
1570
+ */
1571
+ export const DEFINITION_FORM_MAX_SAMPLED = 80;
1572
+
1573
+ /**
1574
+ * How long a block body {@link DefinitionFormKey.example} may be. A placeholder
1575
+ * is read at a glance, and the shortest bodies a game writes for a block key
1576
+ * (a trait's `triggered_opinion`, a culture's `parameters`) fit well inside
1577
+ * this; a longer one is cut with an ellipsis rather than dropped, because half
1578
+ * a real body still says what the key wants.
1579
+ */
1580
+ export const DEFINITION_FORM_MAX_EXAMPLE = 120;
1581
+
1496
1582
  /**
1497
1583
  * Request: the value set a VALUE belongs to, resolved through the definition
1498
1584
  * index; {@link EventValueOptionsParams} -> {@link EventValueOptionsResult} |
@@ -1611,6 +1697,59 @@ export interface GuiUseSite {
1611
1697
  via: string[];
1612
1698
  }
1613
1699
 
1700
+ /**
1701
+ * Request: the code snippets a host can offer for one open script document;
1702
+ * {@link SnippetsParams} -> {@link SnippetsResult}. Answers for OPEN script
1703
+ * documents only (the server reads the client's text, not the disk); a document
1704
+ * it does not know answers with an EMPTY list, never an error.
1705
+ *
1706
+ * Two sources, neither hand-written. The definition and child-block skeletons
1707
+ * are the measured shape of the document folder's own definition kind (at least
1708
+ * half of the game's definitions of that kind carry each key, in the median
1709
+ * order they hold there). The token entries are the block form of the `usage:`
1710
+ * example the game's own script_docs dump ships for an engine trigger or effect,
1711
+ * filtered to the block the cursor sits in.
1712
+ *
1713
+ * Every entry carries BOTH insert forms, exactly like completion does: `snippet`
1714
+ * for a host that expands `${1:…}` tabstops, `plain` for one that does not.
1715
+ */
1716
+ export const snippetsRequest = "paradox/snippets";
1717
+ export interface SnippetsParams {
1718
+ uri: string;
1719
+ /** 0-based, as in LSP. Decides which engine block templates fit. */
1720
+ position: { line: number; character: number };
1721
+ }
1722
+
1723
+ /** One offer, ready to insert at the cursor. */
1724
+ export interface SnippetItem {
1725
+ /**
1726
+ * Stable id: the definition kind (`event`), the kind and its child block
1727
+ * (`event.option`), or the engine token (`if`) — plus `<token>.full` when the
1728
+ * token's example marks fields optional and an all-fields form follows it.
1729
+ * Suitable as a picker key.
1730
+ */
1731
+ id: string;
1732
+ /** Reads as what it inserts: "new event", "option block", "if". */
1733
+ label: string;
1734
+ /** Provenance, with the measurement behind it. */
1735
+ detail: string;
1736
+ /**
1737
+ * `definition` = a whole definition of the document's kind, including the
1738
+ * file header line when the document declares none; `block` = one child block
1739
+ * of that kind; `token` = an engine trigger/effect's own dumped example.
1740
+ */
1741
+ form: "definition" | "block" | "token";
1742
+ /** `${1:…}` tabstop form. */
1743
+ snippet: string;
1744
+ /** The same shape free of `${`, for hosts without snippet expansion. */
1745
+ plain: string;
1746
+ }
1747
+
1748
+ export interface SnippetsResult {
1749
+ /** Skeletons first (definition, then its child blocks), then engine tokens. */
1750
+ snippets: SnippetItem[];
1751
+ }
1752
+
1614
1753
  /**
1615
1754
  * Request: the inferred scope chain at a cursor position;
1616
1755
  * {@link ScopeAtParams} -> {@link ScopeAtResult} | null. Answers for OPEN
@@ -1667,3 +1806,351 @@ export interface ScopeAtResult {
1667
1806
  */
1668
1807
  savedScopes: SavedScopeInfo[];
1669
1808
  }
1809
+
1810
+ // ---- content creators --------------------------------------------------------
1811
+
1812
+ /**
1813
+ * Request: everything a visual creator needs to draw a form for one definition
1814
+ * kind; {@link DefinitionFormParams} -> {@link DefinitionForm} | null (null =
1815
+ * the active game's schema has no such kind, which is the honest answer for a
1816
+ * client asking about content this game does not have).
1817
+ *
1818
+ * Nothing in the answer is hand-written for the creator: the folder, the loc
1819
+ * key patterns and the icon folder come from the schema table, the keys from
1820
+ * the harvested `_*.info` structures, the option lists from the definition
1821
+ * index (the same resolver {@link eventValueOptionsRequest} answers with) and
1822
+ * `existing` from the same index walk {@link modOverviewRequest} does. A game
1823
+ * patch that adds a key or a value changes the form without a release.
1824
+ */
1825
+ export const definitionFormRequest = "paradox/definitionForm";
1826
+ export interface DefinitionFormParams {
1827
+ /** Definition kind, as the schema table spells it ("trait"). */
1828
+ kind: string;
1829
+ /** Load this definition into `current` (edit rather than create). */
1830
+ name?: string;
1831
+ /** Restrict mod-side entries to one workspace mod (plus vanilla/parents). */
1832
+ modRoot?: string | null;
1833
+ }
1834
+
1835
+ /** One key of a definition body, with what is known about the values it takes. */
1836
+ export interface DefinitionFormKey {
1837
+ key: string;
1838
+ /** The game's own one-line documentation, capped. Absent when it has none. */
1839
+ doc?: string;
1840
+ /** Coarse value hint from the schema: `loc`, `bool`, `block`, `enum:a|b|c`. */
1841
+ values?: string;
1842
+ /** Vanilla usage count from the harvest, the order the keys arrive in. */
1843
+ freq?: number;
1844
+ /**
1845
+ * Definition kinds this key's value names, when the profile says so. The
1846
+ * lists live in {@link DefinitionForm.options}, keyed by kind, so several
1847
+ * keys naming the same kind share one list.
1848
+ */
1849
+ refKinds?: string[];
1850
+ /**
1851
+ * The values the indexed definitions of this kind actually write for this
1852
+ * key, most used first, for keys no definition index can answer (a culture's
1853
+ * `clothing_gfx` names an art set, not a definition). Measured from the game
1854
+ * and mod files the server has indexed, at request time, so a patch changes
1855
+ * the list without a release; absent when the key has no refKinds-free value
1856
+ * set of at most {@link DEFINITION_FORM_MAX_SAMPLED} entries, which is the
1857
+ * honest answer for a key whose value is different in every definition.
1858
+ */
1859
+ sampled?: string[];
1860
+ /**
1861
+ * The literal the indexed definitions of this kind write most often for this
1862
+ * key: a real value, so a form can show it as the input's placeholder
1863
+ * instead of inventing one. Unlike {@link sampled} it counts numbers and
1864
+ * quoted text too (quotes stripped), and it survives the cap, so a key whose
1865
+ * value differs in every definition still has an example.
1866
+ *
1867
+ * A key whose value is a BLOCK gets the most written body instead, collapsed
1868
+ * onto one line and capped at {@link DEFINITION_FORM_MAX_EXAMPLE}
1869
+ * characters, so a script field has a placeholder too.
1870
+ *
1871
+ * A key whose value set is already stated (`bool`, `enum:`) carries an
1872
+ * example as well, though no {@link sampled}: a dropdown showing the value
1873
+ * the game itself writes says more than one reading "not set".
1874
+ */
1875
+ example?: string;
1876
+ }
1877
+
1878
+ export interface DefinitionForm {
1879
+ kind: string;
1880
+ /** Schema path the definition is written into, e.g. `common/traits`. */
1881
+ folder: string;
1882
+ /**
1883
+ * Every loc key the game reads for this kind, `$` being the definition name
1884
+ * (`trait_$_desc`). The full set a form should offer, not the conservative
1885
+ * `requiredLoc` subset a diagnostic is allowed to demand.
1886
+ */
1887
+ locPatterns: string[];
1888
+ /** Where the game looks for this kind's icon, e.g. `gfx/interface/icons/traits`. */
1889
+ iconFolder?: string;
1890
+ /** Top-level keys, harvest order (most used first), curated keys ahead. */
1891
+ keys: DefinitionFormKey[];
1892
+ /** Named sub-blocks with their own keys, when the harvest has them. */
1893
+ blocks?: Record<string, DefinitionFormKey[]>;
1894
+ /** Ref kind -> every indexed definition of it, mod entries first, capped. */
1895
+ options: Record<string, EventVocabularyItem[]>;
1896
+ /**
1897
+ * Trigger name -> the values that trigger accepts, for the handful of
1898
+ * triggers a no-code condition builder offers rows for (`has_dlc_feature`,
1899
+ * `has_game_rule`, `scripted_trigger`). Which triggers those are, and where
1900
+ * each list comes from, is the game profile's own table; the values
1901
+ * themselves are read from what the server already holds (the trigger's own
1902
+ * script_docs entry, the definition index), never written for the creator.
1903
+ * A trigger with no resolvable list is ABSENT rather than empty, so a client
1904
+ * offers a free input instead of a picker with nothing in it.
1905
+ */
1906
+ conditions?: Record<string, EventVocabularyItem[]>;
1907
+ /** The modifier vocabulary, most used first: what a modifier row may offer. */
1908
+ modifiers: { name: string; doc?: string }[];
1909
+ /** Definitions of this kind the mod already has (modRoot or every workspace mod). */
1910
+ existing: OverviewDef[];
1911
+ /** The definition `params.name` asked for, when it is indexed. */
1912
+ current?: {
1913
+ file: string;
1914
+ /** 0-based. */
1915
+ line: number;
1916
+ source: DefSource;
1917
+ /** The block verbatim, `name = { ... }`, exactly as the file has it. */
1918
+ text: string;
1919
+ };
1920
+ }
1921
+
1922
+ /**
1923
+ * Request: text edits that write a definition into a script file;
1924
+ * {@link DefinitionEditParams} -> {@link DefinitionEditResult}. The script
1925
+ * sibling of {@link guiSourceEditRequest}, over the same span model, and with
1926
+ * the same division of labour: the server never writes, it returns offsets
1927
+ * into the text it was handed and the host applies them as ONE
1928
+ * `WorkspaceEdit`, which keeps undo and dirty state in the editor.
1929
+ *
1930
+ * Offsets are UTF-16 into `params.text` (the document text, with no BOM, the
1931
+ * way an editor delivers it), computed against that one text and applied
1932
+ * end-first. Every edit is surgical, so a file's other definitions, its
1933
+ * comments, its CRLF and its indentation stay byte-identical.
1934
+ */
1935
+ export const definitionEditRequest = "paradox/definitionEdit";
1936
+ export interface DefinitionEditParams {
1937
+ /** For display only; the text is authoritative. */
1938
+ uri: string;
1939
+ /** Authoritative document text every offset refers to. */
1940
+ text: string;
1941
+ /** Computed in order against the one text and answered as one edit set. */
1942
+ ops: DefinitionOp[];
1943
+ }
1944
+
1945
+ export type DefinitionOp =
1946
+ /**
1947
+ * Set or (with a null value) remove keys on the top-level definition `name`.
1948
+ * `value` is raw script text: `2`, `{ craven }`, `"quoted"`.
1949
+ */
1950
+ | { op: "setProperties"; name: string; properties: { key: string; value: string | null }[] }
1951
+ /**
1952
+ * Write the whole `name = { ... }` block: replaces the top-level block of
1953
+ * that name, or appends it after a blank separator line when the file has
1954
+ * none.
1955
+ */
1956
+ | { op: "upsertBlock"; name: string; text: string };
1957
+
1958
+ export interface DefinitionEditResult {
1959
+ /** Every applied op's edits together. Apply the whole set as ONE change. */
1960
+ edits: GuiTextEdit[];
1961
+ /** One verdict per requested op, in request order; `refused` names why it wrote nothing. */
1962
+ ops: { refused?: string }[];
1963
+ }
1964
+
1965
+ /**
1966
+ * Request: how the GAME prints each modifier; {@link ModifierFormatsParams} ->
1967
+ * {@link ModifierFormatsResult} | null (null = the active profile names no
1968
+ * formats source, or the game folder is not configured).
1969
+ *
1970
+ * A creator that lets a modder add `monthly_income = 0.5` has to show what the
1971
+ * player will see, and the player sees "[gold_i] +0.50 Monthly Income" in
1972
+ * green. None of that is written here: the flags come from the game's own
1973
+ * `common/modifier_definition_formats/` (documented by `_definitions.info`
1974
+ * there), every word comes from the loc index, and every icon comes from the
1975
+ * `texticon` blocks of the game's `gui/texticons.gui`. A modifier no format
1976
+ * block names gets the file's documented defaults, so the answer covers every
1977
+ * modifier token the server knows rather than only the formatted ones.
1978
+ */
1979
+ export const modifierFormatsRequest = "paradox/modifierFormats";
1980
+ export interface ModifierFormatsParams extends ModScopedParams {
1981
+ /**
1982
+ * Loc keys to render as parts too, through the same texticon chain the
1983
+ * prefixes take. A client that prints a line of the game's own UI (a cost
1984
+ * line such as `"[prestige_i] $VALUE|0$"`) asks for the key and gets its
1985
+ * icon and text back; a key the loc index cannot resolve is absent.
1986
+ */
1987
+ lines?: string[];
1988
+ }
1989
+
1990
+ /**
1991
+ * One piece of a prefix or suffix: a word, or a texticon. `[gold_i]` in a loc
1992
+ * value resolves through `game_concept_gold_i` = `"@gold_icon!"` to the
1993
+ * `texticon` block naming the sprite, which is what an icon part carries.
1994
+ */
1995
+ export type FormatPart =
1996
+ { text: string } | { icon: { texture: string; uv?: [number, number, number, number] } };
1997
+
1998
+ /** How one modifier is printed, straight out of the game's own format files. */
1999
+ export interface ModifierFormat {
2000
+ /** The player's word for the modifier, loc-resolved; the key title-cased when it has none. */
2001
+ label: string;
2002
+ /** Digits after the point. The file's documented default is 2. */
2003
+ decimals: number;
2004
+ /** Scale the value by 100 and print a `%`. */
2005
+ percent?: boolean;
2006
+ /** Print a `%` without scaling: the value already is one. */
2007
+ alreadyPercent?: boolean;
2008
+ /** Which direction is good for the player. The file's documented default is `bad`. */
2009
+ color: "good" | "neutral" | "bad";
2010
+ /** `no_difference_sign`: print the number without a leading `+`/`-`. */
2011
+ noSign?: boolean;
2012
+ /** The game does not show this modifier at all. */
2013
+ hidden?: boolean;
2014
+ /** Drawn before the number (`[gold_i]`). */
2015
+ prefix?: FormatPart[];
2016
+ /** Drawn after the number (`/month`). */
2017
+ suffix?: FormatPart[];
2018
+ /** Used in place of `suffix` for negative values, when the game defines one. */
2019
+ negativeSuffix?: FormatPart[];
2020
+ }
2021
+
2022
+ export interface ModifierFormatsResult {
2023
+ /** Modifier name -> its format. Every modifier token the server knows. */
2024
+ formats: Record<string, ModifierFormat>;
2025
+ /** Loc key -> its parts, for each `lines` entry the loc index resolved. */
2026
+ lines?: Record<string, FormatPart[]>;
2027
+ }
2028
+
2029
+ /**
2030
+ * Request: a dynasty as a family tree; {@link DynastyTreeParams} ->
2031
+ * {@link DynastyTreeResult}.
2032
+ *
2033
+ * Two answers behind one method. Without `dynasty` the result is the picker
2034
+ * list: every dynasty the index knows, mod entries first. With `dynasty` it is
2035
+ * that dynasty's houses and members, read out of the game's own
2036
+ * `history/characters` files.
2037
+ *
2038
+ * Everything is DERIVED: the folders come from the active profile's schema
2039
+ * (`dynasty`, `dynasty_house`, `character` kinds), the members from the
2040
+ * character blocks themselves, the display names from the loc index. A profile
2041
+ * whose schema has no `dynasty` kind answers `supported: false` and empty
2042
+ * lists, which is what a client shows instead of an empty tree.
2043
+ */
2044
+ export const dynastyTreeRequest = "paradox/dynastyTree";
2045
+ export interface DynastyTreeParams extends ModScopedParams {
2046
+ /** A dynasty id: answer that dynasty's houses and members instead of the list. */
2047
+ dynasty?: string;
2048
+ }
2049
+
2050
+ /** One dynasty, as the picker lists it. */
2051
+ export interface DynastySummary {
2052
+ /** The block's own key, which is what a character's `dynasty = ` names. */
2053
+ id: string;
2054
+ /** The `name = ` value, a loc key (`dynn_Karling`). */
2055
+ nameKey: string;
2056
+ /** The loc text when the server can resolve it, else `nameKey` itself. */
2057
+ name: string;
2058
+ culture?: string;
2059
+ source: DefSource;
2060
+ file: string;
2061
+ /** 0-based. */
2062
+ line: number;
2063
+ /** Characters whose `dynasty`, or whose house's dynasty, is this one. */
2064
+ characterCount: number;
2065
+ houseCount: number;
2066
+ }
2067
+
2068
+ /** One house of a dynasty (`house_karling = { name = … dynasty = 25061 }`). */
2069
+ export interface DynastyHouse {
2070
+ id: string;
2071
+ nameKey: string;
2072
+ name: string;
2073
+ /** The dynasty id the house belongs to. */
2074
+ dynasty: string;
2075
+ source: DefSource;
2076
+ file: string;
2077
+ /** 0-based. */
2078
+ line: number;
2079
+ }
2080
+
2081
+ /**
2082
+ * The character-level skill keys, in the order a client shows them. MEASURED
2083
+ * over the vanilla `history/characters` corpus (2026-09-03): stewardship 8 964,
2084
+ * martial 8 940, diplomacy 8 908, intrigue 8 892, learning 495, prowess 150.
2085
+ */
2086
+ export const DYNASTY_SKILLS = [
2087
+ "diplomacy",
2088
+ "martial",
2089
+ "stewardship",
2090
+ "intrigue",
2091
+ "learning",
2092
+ "prowess",
2093
+ ] as const;
2094
+
2095
+ /**
2096
+ * One character of `history/characters`. Dates are the game's own
2097
+ * `Y.M.D` strings, taken from the dated block that carries the `birth`/`death`
2098
+ * statement.
2099
+ */
2100
+ export interface DynastyCharacter {
2101
+ /** The block's own key: numeric in vanilla, but `han_1234` shapes exist too. */
2102
+ id: string;
2103
+ /** The `name = ` value, a plain string in history, not a loc key. */
2104
+ name: string;
2105
+ female: boolean;
2106
+ dynasty?: string;
2107
+ /** `dynasty_house = `; a character carries the house OR the dynasty, not both. */
2108
+ house?: string;
2109
+ father?: string;
2110
+ mother?: string;
2111
+ culture?: string;
2112
+ religion?: string;
2113
+ /** `Y.M.D` of the dated block holding `birth`. */
2114
+ birth?: string;
2115
+ death?: string;
2116
+ /**
2117
+ * `dna = `, the portrait DNA name, without the quotes the file may put
2118
+ * around it (350 of 438 vanilla statements write it bare).
2119
+ */
2120
+ dna?: string;
2121
+ /**
2122
+ * The skills the block sets, keyed by {@link DYNASTY_SKILLS}. A skill the
2123
+ * block does not name is absent, which is not the same as zero: the game
2124
+ * rolls one it was not given.
2125
+ */
2126
+ skills?: Record<string, number>;
2127
+ traits: string[];
2128
+ /** Ids this character is married to (`add_spouse`), in file order. */
2129
+ spouses: string[];
2130
+ /**
2131
+ * Set when the character belongs to ANOTHER dynasty and is only in the
2132
+ * answer because a member names them as a parent or a spouse. A client draws
2133
+ * them, but the tree is not theirs.
2134
+ */
2135
+ external?: true;
2136
+ source: DefSource;
2137
+ file: string;
2138
+ /** 0-based. */
2139
+ line: number;
2140
+ }
2141
+
2142
+ export interface DynastyTreeResult {
2143
+ /** False when the active profile's schema has no `dynasty` kind. */
2144
+ supported: boolean;
2145
+ /** The picker list. Empty when `params.dynasty` asked for one dynasty. */
2146
+ dynasties: DynastySummary[];
2147
+ /** Present exactly when `params.dynasty` named a dynasty the index knows. */
2148
+ dynasty?: DynastySummary;
2149
+ houses?: DynastyHouse[];
2150
+ /** Members plus the external parents and spouses they name. */
2151
+ characters?: DynastyCharacter[];
2152
+ /** Largest numeric character id across game and mods, plus one. */
2153
+ nextCharacterId?: string;
2154
+ /** Largest numeric dynasty id across game and mods, plus one. */
2155
+ nextDynastyId?: string;
2156
+ }
@@ -31,15 +31,10 @@ export interface WorkshopMeta {
31
31
  translations?: Record<string, WorkshopTranslation>;
32
32
  }
33
33
 
34
- /** Mod-root-relative path of the record, forward slashes. */
35
- export function workshopMetaRelPath(configDirName: string): string {
36
- return `${configDirName}/workshop.json`;
37
- }
38
-
39
- /** The parsed `<dir>/<configDir>/workshop.json`, or null when absent/unreadable. */
40
- export function readWorkshopMeta(dir: string, configDirName: string): WorkshopMeta | null {
34
+ /** The parsed `<configDir>/workshop.json`, or null when absent/unreadable. */
35
+ export function readWorkshopMeta(configDir: string): WorkshopMeta | null {
41
36
  try {
42
- const raw = JSON.parse(fs.readFileSync(path.join(dir, configDirName, "workshop.json"), "utf8")) as Record<
37
+ const raw = JSON.parse(fs.readFileSync(path.join(configDir, "workshop.json"), "utf8")) as Record<
43
38
  string,
44
39
  unknown
45
40
  >;
@@ -55,9 +50,9 @@ export function readWorkshopMeta(dir: string, configDirName: string): WorkshopMe
55
50
  * survive; a patch key set to `undefined` is left as it was. `translations`
56
51
  * replaces as a whole (the caller edits the full map).
57
52
  */
58
- export function upsertWorkshopMeta(dir: string, configDirName: string, patch: WorkshopMeta): void {
59
- const file = path.join(dir, configDirName, "workshop.json");
60
- const current = (readWorkshopMeta(dir, configDirName) ?? {}) as Record<string, unknown>;
53
+ export function upsertWorkshopMeta(configDir: string, patch: WorkshopMeta): void {
54
+ const file = path.join(configDir, "workshop.json");
55
+ const current = (readWorkshopMeta(configDir) ?? {}) as Record<string, unknown>;
61
56
  for (const [key, value] of Object.entries(patch)) {
62
57
  if (value !== undefined) current[key] = value;
63
58
  }