Skip to content

feat(server): make scripted spell services work-aware #264

Description

@zoeyrose

Outcome

Give Classic a server-owned, script-facing contract for variable-work spell
services to:

  1. query a side-effect-free lower and upper bound for the work a spell can do;
  2. report the work the real cast actually performed; and
  3. settle the resulting content-owned price, including explicitly confirmed,
    provider-specific debt when actual work costs more than available funds.

This is a blocking dependency of
atrinik/content#106 and its draft implementation PR,
atrinik/content#211.

Why work ranges are the contract

A boolean answers too little. Cure disease can guarantee some cures while
other diseases may resist; identification can price per eligible item; and
restoration may perform several different kinds of work. The server should
therefore report bounded, spell-specific work—not money and not an arbitrary
random price.

For each named work component:

  • {0, 0} means the spell cannot beneficially change that state;
  • {0, N} means up to N units may be performed;
  • {N, N} with N > 0 is an exact amount; and
  • {M, N} with M > 0 guarantees at least M and permits at most N.

Unsupported is separate from an empty/zero range. Healing points, diseases,
curses, depleted attributes, and identified items remain distinct named units;
they are not summed into a meaningless universal score.

Identification is a first-class consumer. With current rules its preflight is
usually exact—the number of eligible unidentified items in marked, container,
or normal inventory scope—and the actual result is the number identified. A
future uncertain identification mechanic can widen the same range without
changing the service contract.

Required sub-issues

  • #265 — native,
    side-effect-free bounded work queries and shared selection predicates;
  • #266 — Python exposure of
    those work ranges;
  • #248 — structured actual-work
    outcomes from spell and identification casts; and
  • #267 — bounded persistent
    provider credit and repayment.

All four are required before content#106 resumes its final implementation and
validation cycle.

The nonblocking authored identification consumer is tracked in
atrinik/content#217. It will
replace Smith's flat fee after this native contract lands, but does not delay
the temple-specific consumer.

Ordered implementation plan

Use isolated worktrees for each workstream. One Classic integration owner must
assemble the final branch, resolve shared-file changes, and run whole-diff
validation. Contributors may hand off reviewable commits in parallel, but must
not race independent edits to the positional plugin hook ABI or Python method
registry.

contract freeze
      |
     #265
    /    \
 #266    #248       <- parallel group A
    \    /
     #267
      |
Classic integration, latest-head review, and maintainer merge
    /    \
 #106    #217       <- parallel group B; #217 is nonblocking

Stage 0 — freeze the shared contract

  • Resolve the work-unit vocabulary, bounded native/Python result shapes,
    supported modes, effective-provider-level semantics, and the distinction
    between unsupported, rejected, executed no-op, and beneficial work.
  • Freeze common fixtures that query and actual-outcome tests will share,
    including probabilistic disease, identification scopes, heterogeneous
    restoration, and state drift.
  • Resolve the append-only plugin ABI strategy and reserve one integration
    owner for shared hook-table and Python-registry edits.
  • Freeze feat(server): persist provider-specific service debt #267's npc_id, ledger bounds, preauthorization, settlement,
    repayment, and persistence semantics before irreversible spell work is
    implemented against them.

Exit gate: the four sub-issue contracts agree on names and invariants; no
parallel branch needs to invent a competing representation.

Stage 1 — land #265 as the native foundation

One workstream owns the shared spell applicability/selection predicates, work
units, and side-effect-free bounded query. Because #265 and #248 both need the
spell handlers and their declarations, outcome instrumentation must not fork
those predicates before this foundation is stable.

While #265 is active, other contributors may prepare black-box Python tests and
documentation for #266, outcome fixtures for #248, and persistence/ledger test
fixtures for #267. Those preparations must not change the unsettled production
contract.

Exit gate: focused native tests prove fixed bounds, no gameplay RNG advance,
no side effects, and exact parity with the current selection rules.

Stage 2 — parallel group A: #266 and #248

After #265 stabilizes:

  • Workstream A1 — feat(server): expose spell work quotes to Python #266: implement the Python query adapter, result
    conversion, documentation, and runtime tests. This workstream owns the query
    side of the Python object methods but does not independently reorder or
    finalize the shared plugin hook table.
  • Workstream A2 — feat(server): expose actual spell work outcomes to Python #248: implement dispatcher-carried actual outcomes and
    handler instrumentation, preserving legacy native and Python Cast() and
    CastIdentify() behavior. It owns the native dispatcher/outcome path and
    tests; the integration owner adds or reconciles its Python methods at the
    shared binding join.

Join gate: a single integrator appends ABI hooks, reconciles shared headers and
the Python method registry, rebuilds every plugin, and proves for unchanged
state that every actual component lies within the corresponding queried range.
Only then are #266 and #248 independently complete.

Stage 3 — implement #267 against the settled APIs

Implement the bounded npc_id-keyed debt ledger, durable character storage,
credit preauthorization, atomic cash/debt settlement, exact repayment, and the
narrow native/Python surface. Design and fixtures may have been prepared
earlier, but production integration starts after #265/#266/#248 settle the
quote and actual-work contracts.

Exit gate: tests prove no charge or debt for zero work; no liability beyond the
confirmed maximum; isolation between providers; full-repayment gating;
bounded/checked arithmetic; bank-funded payment; reconnect and save-failure
behavior; and idempotence under repeated confirmation.

Stage 4 — Classic integration and readiness gate

  • Assemble the four workstreams on one current Classic integration head in
    dependency order; stacked leaf commits are acceptable, but the final
    review target must contain the whole diff.
  • Run focused native, Python-plugin, persistence, and compatibility tests,
    then the repository-required server build/test matrix, sanitizers or
    equivalent required diagnostics, and git diff --check.
  • Repeat independent whole-diff review/fix cycles until no known actionable
    findings remain.
  • Re-run required GitHub checks on the latest reviewed head and leave the
    Classic PR ready to merge. The delivery agent does not merge it.

A maintainer merge is the gate into content adoption. Validation against an
unmerged Classic branch is useful evidence, but does not replace refreshing
the consumer onto the exact merged Classic main.

Stage 5 — establish the content integration seam

After the Classic merge, refresh the content worktrees and derived Classic
runtime against the exact merged API. Assign one owner to any shared quote,
confirmation, price-from-work, debt-call, or schema-projection helper before
the two consumers diverge.

Provider capability remains authored content metadata: temple NPCs use the
custom ADS key temple_service_rank, and debt/confirmation use canonical
npc_id. No hard-coded provider registry or new server-recognized object field
is introduced.

Stage 6 — parallel group B: content#106 and content#217

  • Workstream B1 — content#106 / PR feat(protocol)!: disclose immutable world-profile metadata before and after join #211: remove the hard-coded temple
    provider list; author and validate provider rank/identity metadata; quote
    exact or ranged work; revalidate on confirmation; cast once; settle from
    actual work; handle provider-specific repayment; and cover every temple
    provider and service.
  • Workstream B2 — content#217: replace Smith's flat identification fee with
    a quote based on eligible unidentified items and a final charge based on
    items actually identified, using the same confirmation and provider-debt
    contract.

The workstreams may proceed concurrently after the shared seam is fixed. If
they touch the same helper, validator, schema projection, or provider metadata,
one owner lands that shared change and the other rebases before final
validation. #217 is explicitly nonblocking: it must not delay #106 or PR #211
unless it exposes a correctness gap that #106 itself must fix.

Stage 7 — consumer validation and handoff

Each content workstream must pass its own aggregate/schema/provider tests and
whole-diff review. Then run separate deterministic derived-Classic scenarios
for temple treatment and identification, covering incapable and differently
ranked providers, zero/exact/ranged quotes, drift and reconfirmation, partial
and full work, unaffordable actual work, cash/debt splits, repayment,
reconnect/persistence, repeated confirmation, and clean shutdown.

Re-run all required GitHub checks on each latest reviewed content head. PR #211
may become ready independently of #217; neither PR is merged by the delivery
agent.

Completion semantics

Because content#106 is formally blocked by #264, the Classic initiative closes
when all four required Classic sub-issues are merged and the Stage 4 gates
pass. Full temple adoption remains tracked by content#106/PR #211, and Smith
identification by content#217; these downstream consumers do not create a
circular closure dependency for #264.

Quote and settlement lifecycle

  1. Query work against an explicit target, cast/identify mode, and provider
    effective level.
  2. Let content apply its service-specific formula to each component and display
    a minimum/maximum monetary quote. Exact work produces an exact quote.
  3. Bind the state evidence, work range, price range, provider npc_id, provider
    rank, existing provider debt, and maximum permitted new debt into explicit
    confirmation.
  4. Re-query on confirmation; drift requires a new quote and does not cast.
  5. Execute once and receive the actual named work components.
  6. Compute the actual charge from actual work. Zero beneficial work costs zero;
    the charge can never exceed the confirmed maximum.
  7. Collect available funds and, only when the player explicitly accepted the
    exposure, record the remainder as debt to that provider. That provider
    requires full repayment before another paid service.

There is no unpriced cast, random price variance unrelated to work, interest,
or charge for messages, sounds, particles, attempted work, or a no-op.

Initial supported scope

  • remove depletion;
  • cure poison, confusion, and disease;
  • remove curse and remove damnation, preserving NPC-versus-player inventory
    scope and effective-level rules;
  • normal, container, and marked identification; and
  • minor healing, greater healing, and restoration using separate named HP,
    symptom, condition, effect, and food components rather than one scalar.

Unsupported offensive, projectile, area, creation, charging, and
transformation spells must fail explicitly instead of guessing.

Acceptance criteria

  • Native and Python query APIs return bounded named work components and
    distinguish unsupported from zero work.
  • Queries are side-effect-free, do not advance gameplay RNG, and preserve
    objects, events, messages, sounds, metrics, combat state, and cooldowns.
  • Queries and execution share predicates so scope, capability, and work
    units cannot drift.
  • Cast mode, target scope, effective provider level, and identification mode
    are explicit and match execution semantics.
  • Outcome-bearing cast APIs report whether a handler executed and the exact
    beneficial work performed without changing legacy Python Cast() return
    compatibility.
  • Content can derive an exact or ranged quote, then derive the final charge
    from actual work with zero charge/debt for zero work and a hard confirmed
    maximum.
  • Provider debt is keyed by stable npc_id, bounded, interest-free,
    durable, explicitly authorized, and collectible only by that provider.
  • Focused tests cover exact and ranged work, probabilistic disease without
    query-time RNG advancement, curse/damnation capability, identification
    counts/scopes, heterogeneous restoration, quote drift, partial funding,
    repayment, reconnect, and repeated confirmation.
  • The Classic server build, Python-plugin suite, repository-required checks,
    and git diff --check pass.
  • Python documentation and tests prove that a consumer can derive cash plus
    debt from actual work. Full content#106 adoption remains tracked
    downstream and does not gate closing feat(server): make scripted spell services work-aware #264.

Boundaries

  • Money formulas, newcomer policy, provider ranks, and authored provider IDs
    remain owned by content#106 and later service-specific content issues.
  • The query is not a dry-run cast and does not predict later projectile/combat
    outcomes.
  • No client protocol change is required.
  • Future replacement commerce and renderer-neutral quote/action/result services
    remain tracked by Implement native commerce services server#52.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Fields

Priority

Medium

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions