You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 23:36:19 +00:00
152 lines
7.5 KiB
Markdown
152 lines
7.5 KiB
Markdown
# Furni Editor — create furnidata entry if missing (upsert)
|
|
|
|
**Date:** 2026-06-13
|
|
**Status:** Approved design → implementation
|
|
**Repos:** Arcturus-Morningstar-Extended (emulator, primary), Nitro-V3 (client, minor)
|
|
|
|
## Problem
|
|
|
|
In the in-client **Furni Editor**, many furni have **no matching entry in the
|
|
furnidata** (split-tier `*.json5` files). Today the editor detects this
|
|
(`furniDataEntry === null`), shows a "Public Name (DB fallback)" and **locks**
|
|
the name/description fields with the warning "this furni has no matching
|
|
furnidata entry … so its display name can't be edited here."
|
|
`FurnidataWriter.write()` is **edit-only** — it refuses classnames absent from
|
|
furnidata. There is no path to **create** the missing entry.
|
|
|
|
Goal: let an operator create the missing furnidata entry directly from the
|
|
editor, so the furni gets a real, editable name/description.
|
|
|
|
## Decisions (from brainstorming)
|
|
|
|
1. **Trigger = upsert Save.** When the entry is missing, the name/desc fields
|
|
are *unlocked* (name prefilled from the DB Public Name); the existing "Save
|
|
name/desc" creates the entry if absent, edits it if present. No separate
|
|
button beyond a relabel ("Create entry" when missing).
|
|
2. **Completeness = full entry seeded from `items_base`.** The created entry is
|
|
a complete furnidata object (structural fields read from the item's DB row),
|
|
not a name-only stub.
|
|
3. **Target = config key `furnidata.editor.create_tier` (default `custom`).**
|
|
Split-tier → that tier file; single-file furnidata → the single file.
|
|
|
|
## Approach
|
|
|
|
**Reuse the existing `FurniEditorUpdateFurnidata` packet** (outgoing header
|
|
`10046`, result `10044`) and make the **server handler upsert**. Rejected
|
|
alternative: a dedicated `Create` packet (10050) — unnecessary, because the
|
|
create needs **no extra client-supplied fields** (the server reads `items_base`
|
|
for the structural fields and takes name/desc from the existing 10046 payload).
|
|
|
|
**Net wire impact: none.** No renderer changes, no new packet. Only:
|
|
- Emulator: a new `FurnidataWriter.create(...)` + the 10046 handler becomes
|
|
upsert + one config key + an `items_base → furnidata` field mapper.
|
|
- Client: unlock the name/desc fields when the entry is missing + relabel Save.
|
|
|
|
## Emulator changes (Java)
|
|
|
|
### 1. `habbohotel/items/FurnidataWriter.create(...)`
|
|
New method, mirrors `write()`'s safety (locate target file, **backup +
|
|
atomic write**, preserve JSON5 formatting/comments):
|
|
- Resolve target file: read config `furnidata.editor.create_tier` (default
|
|
`custom`). If split-tier (manifest present) → that tier's file (create the
|
|
file with a valid empty-array JSON5 shell if it doesn't exist yet). If
|
|
single-file furnidata → the single file.
|
|
- Append a complete entry object (see field mapping) to the correct array
|
|
(`roomitemtypes` for floor / `wallitemtypes` for wall).
|
|
- **Guards:** refuse if the classname already exists anywhere in furnidata
|
|
(caller routes to edit instead); refuse if the chosen `id` (sprite id) is
|
|
already used by a *different* classname (id collision would break
|
|
`roomItem.name.{id}` / `getFloorItemData(typeId)` resolution).
|
|
- Return a result enum/boolean (created / already-exists / id-collision /
|
|
io-error) so the handler can message the operator precisely.
|
|
|
|
### 2. `FurniEditorUpdateFurnidataEvent` (header 10046) → upsert
|
|
- Resolve classname + the full `items_base` row from `itemId` (handler already
|
|
resolves classname).
|
|
- If furnidata **has** the classname → existing edit path (`write()`).
|
|
- Else → build the complete entry from `items_base` + submitted name/desc →
|
|
`FurnidataWriter.create(...)`.
|
|
- After either path (unchanged from edit): `FurnitureTextProvider.reindexFromSource()`,
|
|
broadcast `FurnitureDataReloadComposer` (10047), mirror name into
|
|
`items_base.public_name`, audit log (action `"create"` vs `"edit"`), respond
|
|
`FurniEditorResultComposer` (10044) with success/precise error.
|
|
- Permission `ACC_CATALOGFURNI` + 1000ms rate-limit (unchanged).
|
|
|
|
### 3. Config key
|
|
`furnidata.editor.create_tier` (default `custom`), read where the writer
|
|
resolves the target file.
|
|
|
|
### 4. `items_base → furnidata` field mapping (helper)
|
|
Read the item's DB definition and map to furnidata JSON. Minimum complete set
|
|
(exact column/field names verified during implementation against the
|
|
`FurnidataReader` schema + `items_base`):
|
|
- `id` = item **sprite id** (the visual/type id — MUST match so the furni
|
|
resolves its name/data), `classname` = `item_name`,
|
|
- `type` = `"s"` (floor) / `"i"` (wall) from the item type,
|
|
- `name` = submitted name (fallback: public_name → classname), `description` =
|
|
submitted description,
|
|
- `xdim`/`ydim` = width/length, `canstandon`/`cansiton`/`canlayon` from the
|
|
item's stand/sit/lay flags, plus the standard furnidata defaults for the
|
|
remaining fields (`partcolors`, `offerid = -1`, `buyout`, `bc`,
|
|
`excludeddynamic`, `customparams`, `specialtype`, `furniline`,
|
|
`environment`, `rare`, `revision`, `category`).
|
|
|
|
## Client changes (React) — `FurniEditorEditView.tsx`
|
|
|
|
- When `furniDataEntry === null`: **unlock** the name/description inputs
|
|
(currently gated by the `furnidataEditable` memo), prefill name from
|
|
`item.publicName`, description blank. Replace the "can't be edited here"
|
|
warning with an informational note: "No furnidata entry yet — saving will
|
|
create one in the «custom» tier." Relabel the Save button to "Create entry"
|
|
while missing.
|
|
- The Save handler is unchanged — it already sends
|
|
`FurniEditorUpdateFurnidataComposer(itemId, { name, description })`.
|
|
- On `FurniEditorResultEvent` success, re-fetch detail
|
|
(`FurniEditorDetailComposer(itemId)`) so `furniDataEntry` populates and the UI
|
|
flips to normal edit mode.
|
|
|
|
## Data flow
|
|
|
|
```
|
|
Save (entry missing)
|
|
→ 10046 UpdateFurnidata(itemId, {name, desc})
|
|
→ handler: classname absent → build complete entry from items_base + name/desc
|
|
→ FurnidataWriter.create(...) into the custom tier (atomic + backup)
|
|
→ reindexFromSource() + broadcast 10047 FurnitureDataReload
|
|
→ every client's catalog/inventory/infostand refreshes; the rendered
|
|
furni now resolves its real name
|
|
→ mirror items_base.public_name
|
|
→ audit "create"
|
|
→ 10044 result(success)
|
|
→ client re-fetches detail → entry now present → normal edit mode
|
|
```
|
|
|
|
## Error handling / edge cases
|
|
|
|
- Classname already present (lookup race) → routed to edit (upsert).
|
|
- Sprite id already used by a different classname → refuse + "id N already
|
|
used by classname X".
|
|
- `items_base` row missing → refuse + error (shouldn't happen for a known item).
|
|
- Tier file absent → created with a valid JSON5 shell.
|
|
- Empty submitted name → fall back to public_name, else classname.
|
|
- Concurrency: reuse `write()`'s file lock + atomic write + backup.
|
|
|
|
## Testing
|
|
|
|
- **Emulator unit:** `FurnidataWriter.create` writes a valid JSON5 entry into
|
|
the target tier; idempotency guard (already-exists); id-collision guard;
|
|
round-trips through `FurnidataReader`.
|
|
- **Runtime (Chrome handle available):** in the Furni Editor select a furni
|
|
with no furnidata entry (the live "DB fallback" case), type a name, Save →
|
|
entry created, furni name updates live (10047 broadcast), reopen → entry
|
|
present and editable. Verify the new object lands in the `custom` tier file
|
|
and `FurnidataReader` parses it.
|
|
|
|
## Out of scope
|
|
|
|
- No new wire packet; no renderer changes.
|
|
- No bulk/batch creation; one furni at a time via the editor.
|
|
- No editing of structural fields from the UI (only name/desc, as today); the
|
|
structural fields are seeded once at creation from `items_base`.
|
|
- No deletion of furnidata entries (separate concern).
|