Stonks 9800: Catspeak Modding Guide
===================================

Quick start
-----------
1. Create a *.meow* file in `/mods/` that returns a `struct`.
2. Supported callback fields that are invoked automatically when a mod is enabled in the menu: `start_game`, `after_game_load`, `new_day`, `step`, `draw`, `draw_end`.
3. The mod list is read on game start. Enable/disable choices are stored in `mods/mods_config.json`.
4. The Catspeak runtime is fully exposed (`Catspeak.interface.exposeEverythingIDontCareIfModdersCanEditUsersSaveFilesJustLetMeDoThis = true`), so you can work directly with GML globals and scripts.

Minimal example
---------------
```
mod = {
  name: "Hello mod",
  description: "Smallest useful example",
  start_game: fun () {
    mods_notify("Hello!", "Mod started");
  },
  new_day: fun () {
    global.money += 1_000_000; // simple daily bonus
  }
}
return mod
```

Localization for mod title/description
--------------------------------------
The mod list can show a localized name/description if you provide them in your mod
struct. The game will pick the entry that matches the player's current language and
fallback to the default `name`/`description` when not found.

Supported patterns (you can use any one of these):
```
-- 1) Flat fields with language suffixes
mod = {
  name: "Default title",
  description: "Default description",
  name_uk: "Українська назва",
  description_uk: "Опис українською",
  name_en: "English title",
  description_en: "English description",
}

-- 2) Localization map per language
mod = {
  name: "Default title",
  description: "Default description",
  name_localized: {
    uk: "Українська назва",
    en: "English title",
  },
  description_localized: {
    uk: "Опис українською",
    en: "English description",
  }
}

-- 3) Nested localization struct
mod = {
  name: "Default title",
  description: "Default description",
  localization: {
    uk: { name: "Українська назва", description: "Опис українською" },
    en: { name: "English title", description: "English description" },
  }
}
```
Language keys supported by default are: `en`, `uk`, `ja`, `ko` (plus common aliases
like `english`, `ukrainian`, `japanese`, `korean`).

Localization for arbitrary in-mod strings
-----------------------------------------
The game keeps every localized UI string in a single table indexed by numeric
ID and reached via `txt(id)`. Mods can extend that table at runtime by
loading a tab-separated CSV from the mod folder and feeding it to
`add_loc_strings`. The CSV's column order matches the engine's language
table (one column per language); each row's first column is the string ID
the mod claims, the remaining columns are the translations.

```
mod = {
  name: "Alien strings",
  start_game: fun () {
    -- mods_csv_load resolves the path relative to the currently-executing
    -- .meow file, so it works regardless of whether the mod was installed
    -- via Workshop, dropped manually, or unpacked to a non-standard
    -- folder. The second arg is the column separator — "\t" matches the
    -- engine's built-in localization table.
    add_loc_strings(mods_csv_load("alien", "\t"))
    create_notification(txt(50000), txt(50001))   -- IDs your CSV defined
  }
}
return mod
```

Pick IDs in a range that won't collide with the engine's built-ins (very
large numbers like 90000+ are safe). For non-mod paths (debugging,
testing) the bare `csv_load("mods/alien/alien", "\t")` form also works.

Lifecycle callbacks
-------------------
- `start_game()`: Runs once before all loading.
- `start_new_game()`: Runs once after a new game starts. Good for seeding data or registering UI.
- `after_creating_character()`: Runs once after the player creates their character at the beginning of the game.
- `after_game_load()`: Runs once during o_control's Create event, on both new games and loaded games. Useful for one-shot setup (greetings, registering UI). **Caveat:** on a fresh load this fires BEFORE the async save file actually finishes restoring globals, so reading per-mod state here will see the struct-literal defaults, not the loaded values. For "react to restored state" use `after_save_load` instead.
- `after_save_load()`: Runs once at the very end of loadgame, after `mods_apply_persist_state` has handed every enabled mod its `load_data(state)` call. This is the right hook for anything that needs to read `mod.foo` fields populated from the save (notifications showing loaded counters, registering loaded entities, etc.). Fires only on save loads, not on new games.
- `before_game_save()`: Runs once just before a save is written. Use to flush any pending values into the struct fields that `save_data()` returns.
- `save_data()`: Runs while the game writes a save. Return a struct/array/scalar that the engine will persist alongside the save. See "Persisting mod state across sessions" below.
- `load_data(state)`: Runs after the save's core globals are restored. Receives whatever `save_data()` returned last session. Use it to rehydrate your mod's runtime state.
- `new_day()`: Runs every in-game day at turnover.
- `step()`: Runs every step during gameplay. Keep light to avoid frame drops.
- `draw()`: Runs during the main draw event; avoid heavy logic.
- `draw_end()`: Runs during the draw end event for overlays.
- `draw_portrait_before()` and `draw_portrait_after()`
- `draw_portrait_mini_before()` and `draw_portrait_mini_after()`
- bar events: `bar_start()`, `bar_step()`, `bar_destroy()`, `bar_draw()`
- notification window events: `window_start()`, `window_destroy()`, `window_draw()`
- `window_sprite()` - script for amy sprite. You can write like `amy_sprite="spr_amy" or sprite_index="spr_amy"`
- shareholder meetings events: meeting_start(), meeting_step(), meeting_draw()
- generator hooks: `cars_generate_after()`, `building_generate_after()`, `art_market_generate_after()` — fired after the random rotation of cars / houses / art has been generated, ideal for substituting mod-defined entries (see "Custom sprites for Cars / Houses / Art" below).
- game-action hooks (fired by the engine when the player or simulation performs a specific action). Each one passes a single struct argument with the action's operands — declare your callback as `fun (info) { ... }` to receive it. Callbacks declared without parameters still work (the extra argument is just ignored). `self` is bound to `o_control` for all game-action hooks, matching the standard lifecycle convention.
  - `on_stock_buy(info)` — fires on every cash-spending buy. `info.kind` is one of:
    - `"long_buy"` — standard long-position purchase via the exchange UI.
    - `"short_close"` — covering an open short (buying back to return borrowed shares).
    - `"issuance"` — buying newly-issued shares from an IPO/event dialog (`buynew()`).
    Payload: `{ kind, company, shares, total_cost, price_per_share }`.
  - `on_stock_sell(info)` — fires on every cash-receiving sell. `info.kind` is one of:
    - `"long_sell"` — selling shares of a long position. Includes `cost_basis` and `realized_pnl` (approximations: accurate when one buy + one sell per slot per tick, lossy when both happen the same tick).
    - `"short_open"` — opening a short position (selling borrowed shares for cash).
    Long-sell payload: `{ kind, company, shares, proceeds, price_per_share, cost_basis, realized_pnl }`. Short-open payload: `{ kind, company, shares, proceeds, price_per_share }`.
  - `on_company_create(info)` — fires at the end of `generate_company`. `info = { slot, name, field, price, profit, hype, stockprice, shares_issued }`. The fields snapshot the freshly initialized company so a mod doesn't have to read 8 globals manually; the slot index is available if you need anything else.
  - `on_company_liquidate(info)` — fires INSIDE `delete_company` BEFORE the company's fields are wiped, so the payload reflects the company's last-known state. `info = { slot, name, field, price, profit, hype, stockprice, shares_circulating, shares_issued, short_position }`. By the time `delete_company` returns the numeric fields on `global.company_*[info.slot]` will all be zero, so use the snapshot if you need them.
  - `on_human_hire(info)` — fires from `scr_hire`. `info = { human, company }` — `human` is the hired NPC's index, `company` is the hiring company.
  - `on_human_fire(info)` — fires from `human_uvolit(human)`. `info = { human, former_company }` — `former_company` is the human's actual workplace at the time of firing (captured before the work record is wiped). Note that human_uvolit can be called for non-player firings (NPCs leaving voluntarily), so `former_company` is not necessarily the player's company.
  - `on_day_end(info)` — fires once at the very end of the daily turnover (after market settlement, salary payments, trend events, liquidations). `info = { game_day }`. Differs from `new_day` (which fires at the START of a new day) — use this for code that needs the post-settlement snapshot. Note: like `new_day`, this hook is gated on `onlyload < 0` in o_control/Other_10, so it does NOT fire when a tick is being skipped during a mid-load fast-forward.

Core market structure
---------------------
- Up to 30 companies. `global.numberofcompany` controls active slots; empty slots have `global.company_liq[i] == 1`.
- Key company fields (arrays indexed by company slot):
  - `global.company_name[i]` — display name.
  - `global.company_field[i]` — sector (0..7, influences goods).
  - `global.company_price[i]` — base price used for reports.
  - `global.company_profit[i]` — yearly profitability (e.g., 0.12 = +12%).
  - `global.company_hype[i]`, `global.company_hype_effect[i]` — marketing/news impact on price and events.
  - `global.company_number_a[i]` — shares issued.
  - `global.company_limit[i]`, `global.company_limit_kil[i]` — buy limits (per order and total).
  - `global.company_rezerv[i]` — reserve funds.
  - `global.company_ac[i]` — shares currently circulating (updated by the game).
  - `global.company_stockprice[i]` — capitalization (generated from `price * hype * global.rynochek * global.trend`).
- Defaults are filled by `generate_company(i)`, and UI/reporting data are refreshed by `company_info_reset(i)`.

Player and economy data
-----------------------
- Global economy: `global.rynochek` (market condition), `global.trend`, `global.day` (current day), `global.inflation`, `global.news_*` (current event info).
- Player state: `global.money`, `global.happy`, `global.stress`, `global.fund`, `global.invest`, `global.human_*` (per-person stats), `global.karma`, `global.prestige`.
- Portfolio: `global.human_finans[ii][i]` tracks per-human holdings by company; `global.company_player[i]` and `global.company_create_player[i]` flag player-created firms.

Humans and contacts
-------------------
- Population counts: `global.human_kol` (active humans) and `global.human_kol2` (extra slots used by events/yakuza).
- Identity and visuals: `global.human_name[i]`, `global.human_surname[i]`, `global.human_sex[i]`, `global.human_unique[i]`, and portrait layers (`global.human_face[i]`, `global.human_eye[i]`, `global.human_hair[i]`, `global.human_body[i]`, `global.human_sprite_unique[i]`, `global.human_sprite_chibi_unique[i]`).
- Core stats: `global.human_intelligence[i]`, `global.human_charisma[i]`, `global.human_precision[i]`, `global.human_stamina[i]`, `global.human_xp[i]` (skill), `global.human_hyst[i]` (quirks that boost or penalize pay), `global.human_friend[i]` (relationship 0–100), and `global.human_nastriy[i]` (mood 0–100, often modified by events, pay, or gifts).
- Employment and pay: `global.human_type[i]` (role), `global.human_price[i]` (current salary), `global.human_desired_price[i]` (target salary), `global.human_zp_echo[i]` (salary multiplier by mood), and `global.human_days_off[i]` (time off that pauses work and lowers portraits).
- Finance and perks: `global.human_finans[i][company]` (per-human stock), `global.human_point_precision/intelligence/charisma[i]` (unused perk points), `global.human_gifted[i]` (gift item slot), `global.human_cultist[i]` (cultist flag), and `global.human_yakuza[i]` (yakuza state used in events).
- Utilities worth reusing: `change_friend(delta, human_id)` adjusts `global.human_friend`, `calc_price_human(human_id)` estimates salary requirements, and `draw_portrait(human_id, x, y, size)` renders the layered portrait for overlays.
- Tips for mods: clamp human indices to `1..global.human_kol`, keep `global.human_friend` and `global.human_nastriy` between 0–100, and prefer `change_friend` for relationship changes so UI popups stay consistent.
- Use `txt(id)` for localized UI strings when showing text in notifications or overlays.

Helper utilities for mods
-------------------------
- `mods_notify(title, text, human=0, type=0, callback=Pusto, rightnow=false, estate=-1)` — shows a standard UI notification. All parameters are sanitized to safe values, and `callback` runs when the player confirms. The `Pusto` default is the engine's built-in no-op (`scripts/Pusto/Pusto.gml`); omit the arg or pass `Pusto` explicitly for "no action on confirm". To run code on confirm, pass a function reference: `mods_notify("Hello", "Click OK to continue", 0, 0, my_handler)`. Catspeak doesn't allow skipping middle positional args, so you must supply `0, 0` to reach the callback slot.
- `mods_create_company(props)` — creates or revives a company slot and returns its index. Optional `props` fields: `{ name, field, price, profit, hype, hype_effect, shares, reserve, limit, player_owned }`. After the call, stock price, buy limits, investor karma, and info panels are refreshed automatically.

Runtime mod inspection
- `mods_is_enabled(name_or_path)` — true when a mod matching the given name OR path is loaded, enabled, and not broken. Use for soft inter-mod integration: `if (mods_is_enabled("Trade pack")) { ... }`.
- `mods_get_metadata(name_or_path)` — returns a snapshot struct `{ path, name, description, author, enabled, broken, error, requires }`, or `undefined` when nothing matches. `requires` is always an array (single-string declarations are coerced; an empty array means no deps declared). The struct is a copy; mutating it does not affect global.mods.
- `mods_list_active()` — array of metadata snapshots for every enabled, non-broken mod, in load order. Useful for "show me what's running" UIs and for iterating to coordinate behavior across mods.
- `mods_current()` — returns the metadata snapshot of the mod whose code is currently executing, or `undefined` when called outside a mod context. Lets a logger helper prefix lines with the running mod's name without hardcoding it.
- `mods_get_dep_failures()` — returns an array of `{ name, error }` for mods that the engine disabled at the last refresh because their `requires` declaration wasn't met. The startup notification consumes this list once, so reading it later returns an empty array unless a refresh happens again.
- `mods_session_load_count()` — returns how many times a save has been loaded in this session (0 before any load, 1 inside the first load_data, 2 on subsequent slot switches). Read this inside `load_data` or `after_save_load` to branch on "first load this session" vs "in-session reload" (e.g. show a "Welcome back" greeting only once).

Game-time accessors
- `mods_get_date()` — returns `{ year, month, day, day_of_week, hour }` sampled from `o_control.mydatetime`. Returns `undefined` when o_control isn't alive yet (title menu, early init) — guard the return value before reading fields.
- `mods_days_since(year, month, day)` — full days elapsed between the supplied calendar date and the current in-game date. Negative when the reference date is in the future. Returns `undefined` when o_control isn't alive or any argument isn't numeric, so callers can distinguish "no game running" from a real zero-day result.

Sandboxed paths
- `mods_safe_path(rel)` — like `mods_resource_path`, but explicitly rejects absolute paths and any segment containing `..`. Use this when handling paths that come from external/untrusted input (config values, save data, network) instead of hard-coded literals — defends against a malicious path escaping the mod's own folder. Returns `""` and logs a debug message on rejection. Also returns `""` when called outside a mod context (no current mod = nothing to sandbox against); use `mods_resource_path` directly if you genuinely need base-dir resolution there.

Sprite strip loading
- `mods_load_sprite_strip(rel, frame_count, xorigin=0, yorigin=0, removeback=false, smooth=false, register_as=undefined)` — convenience wrapper around `mods_load_sprite` for animation strips (one PNG with N frames laid out horizontally). The wrapper just clarifies intent and orders the most common args first; under the hood it still calls `sprite_add` via `mods_load_sprite`. Pass `register_as` to also publish the sprite in the mod sprite registry.

Removing previously-registered extras
- `car_extra_unregister(_id)` / `house_extra_unregister(_id)` / `art_extra_unregister(_id)` — counterparts to the `*_extra_register` calls. Returns `true` when an entry with the matching `_id` was found and removed, `false` otherwise. Useful for mods that swap content at runtime (seasonal themes, toggleable palettes) instead of overwriting by id.

Where files end up on disk (Workshop and offline mods)
------------------------------------------------------
When a player subscribes to a Workshop mod, every file shipped with that
subscription is unpacked **flat** into the game's mods base directory:
- Windows: the game install folder (same as `working_directory`).
- macOS:   `~/Library/Application Support/<game_save_id>/`.
- Linux:   `~/.config/<game_save_id>/`.

So if the Workshop bundle contains:
```
my_mod/
├── my_mod.meow
├── karaoke/song.ogg
└── sprites/avatar.png
```
…then on Windows the player ends up with:
```
<game folder>/my_mod.meow
<game folder>/karaoke/song.ogg
<game folder>/sprites/avatar.png
```
That means an existing path like `audio_create_stream(working_directory + "karaoke/song.ogg")`
just works: the file landed in the same place the game's own `karaoke/`
content lives. Add new karaoke tracks, palette PNGs, etc. by shipping them in
the matching subfolder of your Workshop bundle.

Each subscription is tracked in `mods/.workshop_manifest.json`, so when the
player unsubscribes, the game removes only the files that subscription owned
(and any directories it created that ended up empty). Files shared between
two subscribed mods are kept until the last subscription drops them.

Offline mods (a `.meow` you drop into the game folder yourself) work the same
way: paths are resolved relative to the same base directory.

Mod-aware resource helpers (recommended for new mods)
-----------------------------------------------------
The helpers below resolve any **relative** path against the directory of the
`.meow` that's currently executing (works for both module init and any
callback). Absolute paths are passed through unchanged. They're optional —
plain `sprite_add("karaoke/song.ogg")` still works because the file is
already under `working_directory` — but the helpers make a mod portable
across platforms (on macOS/Linux, `working_directory` points inside the game
bundle, while resources live under the save dir; the helpers do the right
thing in both cases).

```
mod = {
  name: "Avatar mod",

  start_game: fun () {
    self.my_avatar = mods_load_sprite("sprites/avatar.png", 1, false, false, 0, 0)
    self.my_music  = mods_load_audio_stream("karaoke/theme.ogg")
    self.my_table  = mods_csv_load("data/dialogue", "\t") -- ".csv" is added automatically
  },

  window_sprite: fun () {
    self.sprite_index = mod.my_avatar
  }
}
return mod
```

Available helpers:
- `mods_resource_path(rel)` — returns the absolute path of `<rel>` joined to
  the directory of the currently-executing `.meow`.
- `mods_load_sprite(rel, imgnum=1, removeback=false, smooth=false, xorigin=0, yorigin=0)`
  — wraps `sprite_add` with the resolved path.
- `mods_load_audio_stream(rel)` — wraps `audio_create_stream`.
- `mods_buffer_load(rel)` — wraps `buffer_load`.
- `mods_file_exists(rel)` — wraps `file_exists`.
- `mods_csv_load(rel, sep=",")` — mirrors the built-in `csv_load`. Pass either
  `"data/foo"` or `"data/foo.csv"`; the `.csv` suffix is added when missing.
- `mods_active_dir()` — for advanced cases, returns the absolute folder of
  the currently-executing mod. Returns `""` when called outside of a mod
  context.

A few rules of thumb:
- Always use **forward slashes** in mod-relative paths (`"sprites/avatar.png"`),
  even on Windows. They normalize correctly on every platform.
- Load big assets once (in `start_game` or at module top level) and cache the
  returned sprite/sound id on `self` or a top-level variable. `sprite_add`
  keeps the asset loaded for the whole session — repeating the call leaks
  memory.
- Disabled mods don't run their body, so a mod that's been toggled off won't
  read or unpack any of its resource files.
- Two Workshop mods that both ship `karaoke/song.ogg` will overwrite each
  other on disk — last-installed wins. Pick distinctive filenames if you
  expect coexistence.

Practical patterns
------------------
- Creating an event-driven scenario: use `new_day` to schedule news; inside, call `mods_notify` and adjust `global.company_hype[i]` or `global.company_profit[i]` to simulate announcements.
- Custom companies for story arcs: prepare a struct with target values and call `mods_create_company(props)`. Store the returned slot in your mod struct to update it later.
- Custom UI overlays: attach drawing code to `draw_end` for HUD elements; use `txt()` to keep UI consistent with localization.
- Save data: implement `save_data()` / `load_data(state)` on your mod struct (see "Persisting mod state across sessions"). The engine round-trips whatever `save_data()` returns through the save file and hands it back to `load_data()` on the next load — you no longer need to hijack `global` to keep mod-owned state.

Persisting mod state across sessions
------------------------------------
Mods can opt into the save file by exposing two callbacks on their struct:

- `save_data()` — called while the game writes a save. Whatever you return
  is JSON-encoded and stored next to the game's own data. Return any
  struct / array / number / string. Returning `undefined` skips persistence
  for this save.
- `load_data(state)` — called after the save's core globals have been
  restored. `state` is the exact value you returned from `save_data()` last
  session. Use it to put your mod back into the same shape it had when the
  player saved.

The engine matches saved blobs back to active mods by mod path (falling
back to mod name), so renaming or moving a `.meow` will orphan its old
state. Mods that don't expose the callbacks behave exactly as before —
the contract is fully optional and backwards-compatible with older saves.

Tips:
- Keep the payload JSON-friendly: structs, arrays, strings, numbers, and
  booleans round-trip cleanly. Don't try to persist sprite ids, ds_*
  handles, or buffers — re-create those in `load_data` instead.
- If you need to finalize values before they're collected, use the
  `before_game_save()` hook — it fires at the very top of `savegame()`,
  before the savedb metadata is sampled and before `save_data()` is
  collected from any mod. Use it for cross-mod flushes that have to
  see globals in a consistent state across every mod's save_data; for
  per-mod cleanup that doesn't depend on other mods, you can do
  everything inside `save_data` itself.
- `load_data(state)` is called with `state == undefined` for any
  enabled mod that didn't get an entry in the save being loaded — this
  covers pre-feature saves (no `mods_state` section at all), saves
  written while the mod was disabled, mods enabled mid-campaign, AND
  the in-session "load a different slot" case where fields populated
  by a previous load_data would otherwise persist. Reset your fields
  to defaults at the start of `load_data` so stale state from the
  previous load doesn't bleed through, then overlay anything `state`
  provides. The reset call always fires when the mod is enabled but
  missing — there's no separate "pre-feature save" code path to opt
  out of.
- Both callbacks are wrapped in try/catch by the engine. If your code
  throws, the mod's state is skipped for that save/load and a debug
  message is logged — the rest of the save still completes.

A note about `self` and `mod` in callbacks:
Most lifecycle callbacks (`start_game`, `new_day`, `step`, `after_game_load`,
…) are dispatched with `self` rebound to `o_control` (the shared game
controller), so writing `self.foo = ...` inside them lands on `o_control`,
NOT on your mod struct — and it would clobber state with other mods. To
track mod-owned state across callbacks, reference the top-level `mod`
variable via closure capture.

Gotcha: a Catspeak `let`-binding is **not** visible to the closures defined
inside its own struct literal — `let mod = { new_day: fun () { mod.x = 1 } }`
will throw at runtime because `mod` is undefined inside that closure. Drop
the `let` so `mod` lives in the file's top-level scope; the closures will
then capture it correctly.

Inside `save_data()` and `load_data(state)` the engine additionally binds
`self` to the mod struct, so `self.foo` works there as well — but for
consistency with the other callbacks, stick with `mod.foo` everywhere.

`before_game_save()` is treated like the other lifecycle hooks (not like
`save_data`/`load_data`): `self` is rebound to `o_control`, not to your
mod struct. Use `mod.foo` to mutate per-mod state inside it; reserve
`save_data` itself for actually *returning* the payload.

Minimal example: a daily counter that survives quit + relaunch.
```
mod = {                  -- no `let`! closures need to see this name.
  name: "Counter mod",
  days_played: 0,

  new_day: fun () {
    mod.days_played += 1
    mods_notify("Counter mod", "Day " + string(mod.days_played))
  },

  save_data: fun () {
    return { days_played: mod.days_played }
  },

  load_data: fun (state) {
    mod.days_played = 0                          -- reset first
    if (is_struct(state) && variable_struct_exists(state, "days_played")) {
      mod.days_played = state.days_played        -- overlay save data
    }
  }
}
return mod
```

Declaring dependencies between mods
-----------------------------------
A mod can advertise that it depends on other enabled mods by setting a
`requires` array on its struct. Each entry is a string matching either the
exact `path` or the displayed `name` of another mod. After every refresh
the engine scans `global.mods` and disables any mod whose requirements
aren't satisfied — its lifecycle callbacks (start_game, step, new_day, …)
won't fire, and `mods_is_enabled()` returns false for it.

```
mod = {
  name: "Trade widget HUD",
  requires: ["Trade pack"],          -- array form; a single string ("Trade pack") is also accepted

  start_game: fun () {
    -- Safe to assume "Trade pack" exposes its API by now.
  }
}
return mod
```

Important caveat: the dependency check runs *after* every mod's top-level
body has executed (the engine has to compile each .meow once to read its
`requires` field). So a mod that declares `requires` should keep any
side-effectful initialization (sprite_add, add_loc_strings, registering
event handlers on third-party APIs) inside `start_game` rather than at
the top level — otherwise that work runs even when the dep is missing.
Lifecycle callbacks are the safe place for it because they skip disabled
mods automatically.

References
----------
- Catspeak language: https://www.katsaii.com/catspeak-lang
- Built-in examples: see `datafiles/mods/test_mod.meow` and the loader script `scripts/mods_call_event/mods_call_event.gml`.


Creating new traits example:
    new_trait("cat_lover", 3206, undefined, {
        description_id: 4055,
        incompatible_with: ["frugal"],
        effects: {
		money_plus: 481246,
		money_debt: 7000000,
		rep_changes: [-3, 0, -5, 0, 0, 0, 0],
            bonus_lines: [function () { return txt(12) + " +10%"; }, 4056]
        }
    });

Using delay_action in Mods
------------------
- The game includes a system called delay_action, which lets you schedule a function to run after a certain number of in-game days. Normally it is used like this inside the game:
`delay_action(5, "action_name", arg0, arg1, ...)`
- When 5 in-game days pass, the game will call "action_name" with the given arguments.
- To use delay_action with your own mod functions, you must first register your function inside the game. The function to register is:
`mod_register_func(name, fn)`

- name — a string key the game will use to find your function later
- fn — the function you want to run

- After registering, you can use this name inside delay_action

==Example: Registering a custom function
==In your .meow file:

-- Define your function (DO NOT use "let" here)
give_money = fun (amount) {
  global.money += amount
}

-- Register it so the game can call it later
mod_register_func("my_mod.give_money", give_money)

let mod = {
  name: "My Test Mod",

  start_game: fun () {
    -- Schedule your function to run 3 days later
    delay_action(3, "my_mod.give_money", 5000)
  }
}

return mod

==What happens here:

give_money is defined
mod_register_func("my_mod.give_money", give_money) stores it in the game
delay_action(3, "my_mod.give_money", 5000) schedules it
After 3 in-game days, the game automatically calls:
my_mod.give_money(5000)

Notes:
- You must register your function before using it in delay_action
- Use string names (e.g. "my_mod.give_money")
- Your function can have up to 7 parameters
- Works alongside the game’s own built-in actions
- Your function will be executed even if other mods define their own actions — no conflict

==Portraits:
portrait_part_register(sex, part, sprite, sprite_chibi = undefined)
Parts can be: "head", "eyes", "hair", "mouth", "nose", "body"

For unique sprites (full portrait):
portrait_extra_register(sprite, sprite_chibi = undefined, _id = undefined)
_id must be a unique string so as not to conflict with other mods.

==Custom sprites for Cars / Houses / Art:

The car, house and art systems store sprite assignments as string names and
look them up via asset_get_index(). Built-in asset_get_index() only knows
about sprites compiled into the game, so dynamic sprites returned by
mods_load_sprite() were previously invisible to those systems.

Two ways to use a mod-loaded sprite anywhere the game expects a sprite name
string (`global.cars_sprite[i]`, `global.building_sprite[i]`,
`global.art_market_sprite[i]`, etc.):

1) Register the sprite under a name and reference it by name:

	let spr = mods_load_sprite("sprites/my_car.png")
	mods_register_sprite("my_car_sprite", spr)
	global.cars_sprite[0] = "my_car_sprite"

2) Pass the registry name straight through `mods_load_sprite`:

	mods_load_sprite("sprites/my_car.png", 1, false, false, 0, 0, "my_car_sprite")
	global.cars_sprite[0] = "my_car_sprite"

The name lives in a global registry until the game closes, so it survives
save/load as long as your mod re-registers the same name on `start_game` (or
at module top level).

Sprite registry helpers:
- `mods_register_sprite(name, sprite_id)` — associates `name` with a dynamic
  sprite index. Returns the sprite id, or -1 on failure.
- `mods_unregister_sprite(name)` — removes a registration. No-op if absent.
- `mods_lookup_sprite(name)` — returns the registered sprite index, or -1.
- `mods_sprite_get_index(ref)` — drop-in mod-aware replacement for
  `asset_get_index`. Accepts string names (built-in or mod-registered) or
  numeric sprite ids; returns -1 when not found.
- `mods_resolve_sprite(ref, fallback=-1)` — same as above but returns
  `fallback` instead of -1 when the reference cannot be resolved.
- `mods_sprite_to_name(ref)` — returns the canonical string name for a
  sprite reference. If `ref` is a numeric dynamic id with no registered
  name, a placeholder name is auto-registered and returned.

Higher-level extras registries (similar to `portrait_extra_register`):
- `car_extra_register(props)` — register a mod car. Required: `sprite`.
  Optional: `_id`, `frame`, `type` (0..5: sedan, hatchback, van, pickup,
  limo, convertible), `brand`, `model`, `komfort`, `price`, `condition`.
- `house_extra_register(props)` — register a mod building. Required:
  `sprite`. Optional: `_id`, `frame`, `type` (0=apartment, 1=mansion,
  2=detached), `class`, `rooms`, `size`, `bukva`, `komfort`, `price`,
  `commercial`.
- `art_extra_register(props)` — register a mod artwork. Required:
  `sprite`. Optional: `_id`, `frame`, `rarity` (0..3), `base_price`,
  `special`.

Each registration is keyed by `_id`; re-registering the same id replaces
the previous entry. If you omit `_id`, a unique one is generated for you.
To actually remove an entry (not just shadow it), call the matching
`car_extra_unregister(_id)` / `house_extra_unregister(_id)` /
`art_extra_unregister(_id)`.

Random-pick helpers (return `undefined` when no extras are registered):
- `car_extras_pick_for_type(type)`
- `house_extras_pick_for_type(type)`
- `art_extras_pick_for_rarity(rarity)`

Generator hooks fired after the built-in random rotation has been written:
- `cars_generate_after`
- `building_generate_after`
- `art_market_generate_after`

Use these hooks to substitute mod entries into the freshly generated pools.

Example: inject a custom limo on every car refresh.

	mod = {
	  name: "Modders Garage",

	  start_game: fun () {
	    self.lambo = mods_load_sprite("sprites/lambo.png")
	    mods_register_sprite("modder_lambo", self.lambo)
	    car_extra_register({
	      _id: "modder_lambo",
	      sprite: "modder_lambo",
	      frame: 0,
	      type: 4,        -- limo slot
	      brand: 0,
	      model: "Lambdo",
	      komfort: 320,
	      price: 18000000
	    })
	  },

	  cars_generate_after: fun () {
	    for (var i = 0; i < 24; ++i) {
	      if (irandom(99) < 20) {
	        var extra = car_extras_pick_for_type(global.cars_type[i])
	        if (!is_undefined(extra)) {
	          global.cars_sprite[i] = mods_sprite_to_name(extra.sprite)
	          global.cars_image[i]  = extra.frame
	          global.cars_komfort[i] = extra.komfort
	          if (extra.price > 0) global.cars_price[i] = extra.price
	          if (string_length(extra.model) > 0)
	            global.cars_name_model[i] = extra.model
	        }
	      }
	    }
	  }
	}
	return mod
