@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.
@@ -10,12 +10,6 @@
10
10
  * hover) and the extension (the Insert Date command), unit-tested in plain
11
11
  * Node.
12
12
  */
13
- export interface CalendarMonth {
14
- /** Display name ("March", "Narvinye"). */
15
- name: string;
16
- /** Day count; the engine has no leap years, so one number per month. */
17
- days: number;
18
- }
19
13
  export interface CalendarSetting {
20
14
  /** Script year displayed as year 1 of the `after` era (no year zero). */
21
15
  epoch: number;
@@ -24,12 +18,20 @@ export interface CalendarSetting {
24
18
  /** Era label for script years < epoch ("BC"). Omitted = single-era
25
19
  * calendar: years before the epoch get no display form. */
26
20
  before?: string;
27
- /** Custom month names and day counts, first month first. Omitted = the
28
- * standard 12 months (Feb 28: the engine has no leap years). */
29
- months?: CalendarMonth[];
21
+ /**
22
+ * The engine's twelve months under the mod's own names, first month first.
23
+ * Omitted = January to December. Only the NAMES are the mod's: the game has
24
+ * twelve months of fixed length (31 28 31 30 31 30 31 31 30 31 30 31, no
25
+ * leap years) and no script can change that, so a date's month and day are
26
+ * always the engine's and only read differently.
27
+ */
28
+ months?: string[];
30
29
  }
31
- export declare const GREGORIAN_MONTHS: CalendarMonth[];
32
- export declare function monthsOf(cal: CalendarSetting): CalendarMonth[];
30
+ /** Days per engine month; what a script date's day is bounded by. */
31
+ export declare const ENGINE_MONTH_DAYS: number[];
32
+ export declare const GREGORIAN_MONTHS: string[];
33
+ /** The month names a date reads with: the mod's twelve, or the engine's. */
34
+ export declare function monthNames(cal: CalendarSetting): string[];
33
35
  /**
34
36
  * Validate a calendar straight out of JSON settings (any client, any hand-
35
37
  * edited settings file). Returns a clean copy, or undefined when the value is
@@ -42,8 +44,8 @@ export declare function parseScriptDate(text: string): {
42
44
  m: number;
43
45
  d: number;
44
46
  } | null;
45
- /** Month/day within the calendar's bounds (year just has to be positive). */
46
- export declare function isValidScriptDate(cal: CalendarSetting, y: number, m: number, d: number): boolean;
47
+ /** A date the engine reads: a positive year, one of its twelve months, a day that month has. */
48
+ export declare function isValidScriptDate(y: number, m: number, d: number): boolean;
47
49
  /** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
48
50
  export declare function displayYear(cal: CalendarSetting, y: number): string | null;
49
51
  /**
package/dist/calendar.js CHANGED
@@ -12,15 +12,16 @@
12
12
  * Node.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.GREGORIAN_MONTHS = void 0;
16
- exports.monthsOf = monthsOf;
15
+ exports.GREGORIAN_MONTHS = exports.ENGINE_MONTH_DAYS = void 0;
16
+ exports.monthNames = monthNames;
17
17
  exports.sanitizeCalendar = sanitizeCalendar;
18
18
  exports.parseScriptDate = parseScriptDate;
19
19
  exports.isValidScriptDate = isValidScriptDate;
20
20
  exports.displayYear = displayYear;
21
21
  exports.displayDate = displayDate;
22
22
  exports.convertDisplayInput = convertDisplayInput;
23
- const DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
23
+ /** Days per engine month; what a script date's day is bounded by. */
24
+ exports.ENGINE_MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
24
25
  exports.GREGORIAN_MONTHS = [
25
26
  "January",
26
27
  "February",
@@ -34,8 +35,9 @@ exports.GREGORIAN_MONTHS = [
34
35
  "October",
35
36
  "November",
36
37
  "December",
37
- ].map((name, i) => ({ name, days: DAYS[i] }));
38
- function monthsOf(cal) {
38
+ ];
39
+ /** The month names a date reads with: the mod's twelve, or the engine's. */
40
+ function monthNames(cal) {
39
41
  return cal.months ?? exports.GREGORIAN_MONTHS;
40
42
  }
41
43
  /**
@@ -61,21 +63,22 @@ function sanitizeCalendar(raw) {
61
63
  if (cal.before && cal.before.toLowerCase() === cal.after.toLowerCase())
62
64
  return undefined;
63
65
  if (Array.isArray(o.months) && o.months.length > 0) {
66
+ // Exactly the engine's twelve. A name may still arrive as the older
67
+ // `{ name, days }` object; its day count never meant anything to the
68
+ // game and is dropped.
69
+ if (o.months.length !== exports.GREGORIAN_MONTHS.length)
70
+ return undefined;
64
71
  const months = [];
65
72
  const seen = new Set();
66
73
  for (const m of o.months) {
67
- if (typeof m !== "object" || m === null)
68
- return undefined;
69
- const { name, days } = m;
74
+ const name = typeof m === "string" ? m : (m?.name ?? null);
70
75
  if (typeof name !== "string" || name.trim() === "")
71
76
  return undefined;
72
- if (typeof days !== "number" || !Number.isInteger(days) || days < 1 || days > 999)
73
- return undefined;
74
77
  const key = name.trim().toLowerCase();
75
78
  if (seen.has(key))
76
79
  return undefined;
77
80
  seen.add(key);
78
- months.push({ name: name.trim(), days });
81
+ months.push(name.trim());
79
82
  }
80
83
  cal.months = months;
81
84
  }
@@ -88,10 +91,9 @@ function parseScriptDate(text) {
88
91
  return null;
89
92
  return { y: Number(match[1]), m: Number(match[2]), d: Number(match[3]) };
90
93
  }
91
- /** Month/day within the calendar's bounds (year just has to be positive). */
92
- function isValidScriptDate(cal, y, m, d) {
93
- const months = monthsOf(cal);
94
- return y >= 1 && m >= 1 && m <= months.length && d >= 1 && d <= months[m - 1].days;
94
+ /** A date the engine reads: a positive year, one of its twelve months, a day that month has. */
95
+ function isValidScriptDate(y, m, d) {
96
+ return y >= 1 && m >= 1 && m <= exports.ENGINE_MONTH_DAYS.length && d >= 1 && d <= exports.ENGINE_MONTH_DAYS[m - 1];
95
97
  }
96
98
  /** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
97
99
  function displayYear(cal, y) {
@@ -105,25 +107,23 @@ function displayYear(cal, y) {
105
107
  * date does not fit the calendar.
106
108
  */
107
109
  function displayDate(cal, y, m, d) {
108
- if (!isValidScriptDate(cal, y, m, d))
110
+ if (!isValidScriptDate(y, m, d))
109
111
  return null;
110
112
  const year = displayYear(cal, y);
111
113
  if (!year)
112
114
  return null;
113
115
  if (m === 1 && d === 1)
114
116
  return year;
115
- return `${d} ${monthsOf(cal)[m - 1].name} ${year}`;
117
+ return `${d} ${monthNames(cal)[m - 1]} ${year}`;
116
118
  }
117
119
  /** Case-insensitive month lookup: exact name, else unique prefix. */
118
120
  function monthByName(cal, text) {
119
121
  const needle = text.toLowerCase();
120
- const months = monthsOf(cal);
121
- const exact = months.findIndex((m) => m.name.toLowerCase() === needle);
122
+ const months = monthNames(cal);
123
+ const exact = months.findIndex((m) => m.toLowerCase() === needle);
122
124
  if (exact >= 0)
123
125
  return exact + 1;
124
- const prefixed = months
125
- .map((m, i) => ({ m, i }))
126
- .filter(({ m }) => m.name.toLowerCase().startsWith(needle));
126
+ const prefixed = months.map((m, i) => ({ m, i })).filter(({ m }) => m.toLowerCase().startsWith(needle));
127
127
  return prefixed.length === 1 ? prefixed[0].i + 1 : null;
128
128
  }
129
129
  /**
@@ -173,11 +173,10 @@ function convertDisplayInput(cal, input) {
173
173
  const y = era === cal.after ? year + cal.epoch - 1 : cal.epoch - year;
174
174
  if (y < 1)
175
175
  return { ok: false, error: `${year} ${era} is before script year 1 (epoch ${cal.epoch})` };
176
- if (!isValidScriptDate(cal, y, m, d)) {
177
- const months = monthsOf(cal);
178
- return m >= 1 && m <= months.length
179
- ? { ok: false, error: `${months[m - 1].name} has ${months[m - 1].days} days` }
180
- : { ok: false, error: `this calendar has ${months.length} months` };
176
+ if (!isValidScriptDate(y, m, d)) {
177
+ return m >= 1 && m <= exports.ENGINE_MONTH_DAYS.length
178
+ ? { ok: false, error: `${monthNames(cal)[m - 1]} has ${exports.ENGINE_MONTH_DAYS[m - 1]} days` }
179
+ : { ok: false, error: `the game has ${exports.ENGINE_MONTH_DAYS.length} months` };
181
180
  }
182
181
  return { ok: true, script: `${y}.${m}.${d}`, display: displayDate(cal, y, m, d) };
183
182
  }
@@ -0,0 +1,26 @@
1
+ import { type CalendarSetting } from "./calendar";
2
+ import { type ConfigDirNames } from "./configDir";
3
+ export declare const CALENDAR_FILE = "calendar.json";
4
+ export interface CalendarFile {
5
+ /** Where the declaration was read from (or would be written to). */
6
+ file: string;
7
+ /** The declared calendar, when the file parses and sanitizes. */
8
+ calendar?: CalendarSetting;
9
+ /** Why an existing file yields no calendar: unparsable JSON or an unusable shape. */
10
+ error?: string;
11
+ }
12
+ /** The path the file is read from: the mod's config dir (legacy name included). */
13
+ export declare function calendarFilePath(modRoot: string, names: ConfigDirNames): string;
14
+ /**
15
+ * Read `<mod>/.px-toolkit/calendar.json`. Null when the file does not exist;
16
+ * a `CalendarFile` without `calendar` when it exists but is not usable, so a
17
+ * client can say so instead of silently showing no dates.
18
+ */
19
+ export declare function readCalendarFile(modRoot: string, names: ConfigDirNames): CalendarFile | null;
20
+ /**
21
+ * Write the declaration into the mod (renaming a legacy config dir first,
22
+ * like every other config-dir write). Returns the file path.
23
+ */
24
+ export declare function writeCalendarFile(modRoot: string, names: ConfigDirNames, cal: CalendarSetting): string;
25
+ /** True when `fsPath` is a calendar declaration file (any config dir name). */
26
+ export declare function isCalendarFile(fsPath: string, names: ConfigDirNames): boolean;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CALENDAR_FILE = void 0;
37
+ exports.calendarFilePath = calendarFilePath;
38
+ exports.readCalendarFile = readCalendarFile;
39
+ exports.writeCalendarFile = writeCalendarFile;
40
+ exports.isCalendarFile = isCalendarFile;
41
+ /**
42
+ * The per-mod calendar declaration: `<mod>/.px-toolkit/calendar.json`, the
43
+ * JSON form of calendar.ts `CalendarSetting`. A display calendar is a fact
44
+ * about the mod, so it travels with the mod (committed, one per mod, read by
45
+ * every client and by the server itself) instead of living in one editor's
46
+ * window-scoped `px.calendar` setting. The setting stays as the fallback for
47
+ * a mod without the file.
48
+ *
49
+ * No `vscode` imports: unit-tested in plain Node.
50
+ */
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const calendar_1 = require("./calendar");
54
+ const configDir_1 = require("./configDir");
55
+ exports.CALENDAR_FILE = "calendar.json";
56
+ /** The path the file is read from: the mod's config dir (legacy name included). */
57
+ function calendarFilePath(modRoot, names) {
58
+ return path.join((0, configDir_1.resolveConfigDir)(modRoot, names), exports.CALENDAR_FILE);
59
+ }
60
+ /**
61
+ * Read `<mod>/.px-toolkit/calendar.json`. Null when the file does not exist;
62
+ * a `CalendarFile` without `calendar` when it exists but is not usable, so a
63
+ * client can say so instead of silently showing no dates.
64
+ */
65
+ function readCalendarFile(modRoot, names) {
66
+ const file = calendarFilePath(modRoot, names);
67
+ let text;
68
+ try {
69
+ text = fs.readFileSync(file, "utf8");
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ let raw;
75
+ try {
76
+ raw = JSON.parse(text.replace(/^\uFEFF/, ""));
77
+ }
78
+ catch (err) {
79
+ return { file, error: `not valid JSON (${err.message})` };
80
+ }
81
+ const calendar = (0, calendar_1.sanitizeCalendar)(raw);
82
+ if (!calendar) {
83
+ return {
84
+ file,
85
+ error: 'not a usable calendar: needs a whole-number "epoch" (1 or more), a non-empty "after" era label, ' +
86
+ 'a "before" label different from "after" when present, and, when "months" is given, exactly twelve distinct names',
87
+ };
88
+ }
89
+ return { file, calendar };
90
+ }
91
+ /**
92
+ * Write the declaration into the mod (renaming a legacy config dir first,
93
+ * like every other config-dir write). Returns the file path.
94
+ */
95
+ function writeCalendarFile(modRoot, names, cal) {
96
+ const dir = (0, configDir_1.migrateConfigDir)(modRoot, names);
97
+ fs.mkdirSync(dir, { recursive: true });
98
+ const file = path.join(dir, exports.CALENDAR_FILE);
99
+ fs.writeFileSync(file, JSON.stringify(cal, null, 2) + "\n", "utf8");
100
+ return file;
101
+ }
102
+ /** True when `fsPath` is a calendar declaration file (any config dir name). */
103
+ function isCalendarFile(fsPath, names) {
104
+ const parts = fsPath.split(/[\\/]/);
105
+ if (parts.length < 2 || parts[parts.length - 1].toLowerCase() !== exports.CALENDAR_FILE)
106
+ return false;
107
+ const dir = parts[parts.length - 2].toLowerCase();
108
+ return dir === names.configDirName.toLowerCase() || dir === names.legacyConfigDirName?.toLowerCase();
109
+ }
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CAL_ERA_KEY = exports.CAL_YEAR_KEY = void 0;
4
4
  exports.generateCalendarLoc = generateCalendarLoc;
5
- const calendar_1 = require("./calendar");
6
5
  /** Keys the generated files define; exported so tests and docs stay in sync. */
7
6
  exports.CAL_YEAR_KEY = "PX_CAL_YEAR";
8
7
  exports.CAL_ERA_KEY = "PX_CAL_ERA";
@@ -72,23 +71,20 @@ function generateCalendarLoc(cal, spec, lang) {
72
71
  overrideLines.push(` ${key}:0 "${value}"`);
73
72
  }
74
73
  if (cal.months) {
75
- const months = (0, calendar_1.monthsOf)(cal);
74
+ const months = cal.months;
76
75
  if (!spec.monthKeys) {
77
76
  notes.push("This game's month-name keys are not mapped yet; custom month names were not generated.");
78
77
  }
79
- else if (months.length !== spec.monthKeys.length) {
80
- notes.push(`The engine has exactly ${spec.monthKeys.length} months; your calendar declares ${months.length}, ` +
81
- "so month names were not generated (the month count itself cannot be modded).");
82
- }
83
78
  else {
79
+ // The calendar carries the engine's twelve names (sanitizeCalendar); the
80
+ // spec lists the keys of the months it knows, first month first.
84
81
  overrideLines.push(" # Engine month names (long and abbreviated forms both get the custom name).");
85
- months.forEach((m, i) => {
86
- const [long, short] = spec.monthKeys[i];
87
- overrideLines.push(` ${long}:0 "${m.name.replace(/"/g, '\\"')}"`);
82
+ spec.monthKeys.forEach(([long, short], i) => {
83
+ const name = months[i].replace(/"/g, '\\"');
84
+ overrideLines.push(` ${long}:0 "${name}"`);
88
85
  if (short !== long)
89
- overrideLines.push(` ${short}:0 "${m.name.replace(/"/g, '\\"')}"`);
86
+ overrideLines.push(` ${short}:0 "${name}"`);
90
87
  });
91
- notes.push("Month day counts are engine-fixed (31/28/31...); custom day counts only affect the editor's display.");
92
88
  }
93
89
  }
94
90
  notes.push(`Generated for '${lang}' only; other languages the mod ships need the same overrides in their replace folder.`);
@@ -0,0 +1,17 @@
1
+ export declare const PX_CONFIG_DIR = ".px-toolkit";
2
+ export interface ConfigDirNames {
3
+ configDirName: string;
4
+ /** The pre-0.4.0 per-game name, still read as a fallback. */
5
+ legacyConfigDirName?: string;
6
+ }
7
+ /**
8
+ * The config dir to READ from: the current name when it exists, else the
9
+ * legacy one when that exists, else the current name. Never touches disk.
10
+ */
11
+ export declare function resolveConfigDir(root: string, names: ConfigDirNames): string;
12
+ /**
13
+ * The config dir to WRITE to. Renames a legacy dir to the current name first;
14
+ * if the rename fails (locked file, read-only parent) the legacy dir stays in
15
+ * use so the write still lands where reads look.
16
+ */
17
+ export declare function migrateConfigDir(root: string, names: ConfigDirNames): string;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PX_CONFIG_DIR = void 0;
37
+ exports.resolveConfigDir = resolveConfigDir;
38
+ exports.migrateConfigDir = migrateConfigDir;
39
+ /**
40
+ * The toolkit's per-mod config dir: `<mod>/.px-toolkit/`, holding
41
+ * `workshop.json`, `schema.json`, `playset.json`, the tiger baseline, the GUI
42
+ * preview values and the Workshop listing folder. Mods created before 0.4.0
43
+ * have a per-game name instead (each GameMeta's `legacyConfigDirName`);
44
+ * reads keep finding it, and the first write renames it.
45
+ *
46
+ * No `vscode` imports: unit-tested in plain Node.
47
+ */
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ exports.PX_CONFIG_DIR = ".px-toolkit";
51
+ /**
52
+ * The config dir to READ from: the current name when it exists, else the
53
+ * legacy one when that exists, else the current name. Never touches disk.
54
+ */
55
+ function resolveConfigDir(root, names) {
56
+ const current = path.join(root, names.configDirName);
57
+ if (!names.legacyConfigDirName || fs.existsSync(current))
58
+ return current;
59
+ const legacy = path.join(root, names.legacyConfigDirName);
60
+ return fs.existsSync(legacy) ? legacy : current;
61
+ }
62
+ /**
63
+ * The config dir to WRITE to. Renames a legacy dir to the current name first;
64
+ * if the rename fails (locked file, read-only parent) the legacy dir stays in
65
+ * use so the write still lands where reads look.
66
+ */
67
+ function migrateConfigDir(root, names) {
68
+ const current = path.join(root, names.configDirName);
69
+ const resolved = resolveConfigDir(root, names);
70
+ if (resolved === current)
71
+ return current;
72
+ try {
73
+ fs.renameSync(resolved, current);
74
+ return current;
75
+ }
76
+ catch {
77
+ return resolved;
78
+ }
79
+ }