ff9mapkit 1.0.0b3__py3-none-any.whl
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.
- ff9mapkit/__init__.py +18 -0
- ff9mapkit/__main__.py +36 -0
- ff9mapkit/_animdb.py +2994 -0
- ff9mapkit/_animdb_all.py +14125 -0
- ff9mapkit/_fieldtable.py +1516 -0
- ff9mapkit/_fieldtext.py +845 -0
- ff9mapkit/_held_poses.py +44 -0
- ff9mapkit/_itemdb.py +65 -0
- ff9mapkit/_modeldb.py +725 -0
- ff9mapkit/_narrowmap_data.py +10 -0
- ff9mapkit/_npcparams.py +634 -0
- ff9mapkit/_regen_animdb.py +72 -0
- ff9mapkit/_regen_animdb_all.py +66 -0
- ff9mapkit/_regen_fieldtable.py +95 -0
- ff9mapkit/_regen_fieldtext.py +66 -0
- ff9mapkit/_regen_modeldb.py +67 -0
- ff9mapkit/_regen_npcparams.py +123 -0
- ff9mapkit/_regen_scenedb.py +57 -0
- ff9mapkit/_scenedb.py +869 -0
- ff9mapkit/abilities.py +225 -0
- ff9mapkit/animations.py +120 -0
- ff9mapkit/archetypes.py +218 -0
- ff9mapkit/areatitle.py +76 -0
- ff9mapkit/battle/__init__.py +21 -0
- ff9mapkit/battle/abilityfeatures.py +294 -0
- ff9mapkit/battle/actiondelta.py +441 -0
- ff9mapkit/battle/aiauthor.py +305 -0
- ff9mapkit/battle/ailint.py +140 -0
- ff9mapkit/battle/aipatch.py +175 -0
- ff9mapkit/battle/battleai.py +148 -0
- ff9mapkit/battle/battlecsv.py +390 -0
- ff9mapkit/battle/battlepatch.py +395 -0
- ff9mapkit/battle/build.py +558 -0
- ff9mapkit/battle/camera_codec.py +332 -0
- ff9mapkit/battle/camera_data.py +128 -0
- ff9mapkit/battle/characterdelta.py +789 -0
- ff9mapkit/battle/event_data.py +72 -0
- ff9mapkit/battle/extract.py +540 -0
- ff9mapkit/battle/fbx.py +223 -0
- ff9mapkit/battle/reskin.py +149 -0
- ff9mapkit/battle/scene_codec.py +314 -0
- ff9mapkit/battle/scene_data.py +369 -0
- ff9mapkit/battle/scenelint.py +125 -0
- ff9mapkit/battle/seqasm.py +131 -0
- ff9mapkit/battle/seqauthor.py +220 -0
- ff9mapkit/battle/seqcodec.py +300 -0
- ff9mapkit/battle/seqdis.py +106 -0
- ff9mapkit/battle/seqpatch.py +137 -0
- ff9mapkit/battle_bgm.py +133 -0
- ff9mapkit/binutils.py +60 -0
- ff9mapkit/build.py +5445 -0
- ff9mapkit/campaign.py +1276 -0
- ff9mapkit/catalog.py +316 -0
- ff9mapkit/chain.py +358 -0
- ff9mapkit/cli.py +3114 -0
- ff9mapkit/config.py +360 -0
- ff9mapkit/content/__init__.py +13 -0
- ff9mapkit/content/areatitle.py +36 -0
- ff9mapkit/content/ate.py +118 -0
- ff9mapkit/content/camera.py +123 -0
- ff9mapkit/content/chest.py +186 -0
- ff9mapkit/content/choice.py +163 -0
- ff9mapkit/content/conductor.py +217 -0
- ff9mapkit/content/cutscene.py +290 -0
- ff9mapkit/content/encounter.py +41 -0
- ff9mapkit/content/entry_settle.py +50 -0
- ff9mapkit/content/equipment.py +93 -0
- ff9mapkit/content/event.py +191 -0
- ff9mapkit/content/gateway.py +101 -0
- ff9mapkit/content/inventory.py +59 -0
- ff9mapkit/content/itemdata.py +644 -0
- ff9mapkit/content/itemtext.py +168 -0
- ff9mapkit/content/jump.py +114 -0
- ff9mapkit/content/ladder.py +633 -0
- ff9mapkit/content/movement.py +53 -0
- ff9mapkit/content/music.py +97 -0
- ff9mapkit/content/npc.py +348 -0
- ff9mapkit/content/object.py +340 -0
- ff9mapkit/content/onentry.py +135 -0
- ff9mapkit/content/party.py +111 -0
- ff9mapkit/content/pathfind.py +138 -0
- ff9mapkit/content/platform.py +314 -0
- ff9mapkit/content/player.py +168 -0
- ff9mapkit/content/prop.py +75 -0
- ff9mapkit/content/region.py +340 -0
- ff9mapkit/content/reinit.py +59 -0
- ff9mapkit/content/savepoint.py +90 -0
- ff9mapkit/content/shop.py +178 -0
- ff9mapkit/content/sps_trigger.py +66 -0
- ff9mapkit/content/startup.py +71 -0
- ff9mapkit/content/synthesis.py +106 -0
- ff9mapkit/content/text.py +183 -0
- ff9mapkit/content/textcarry.py +290 -0
- ff9mapkit/content/verbatim.py +86 -0
- ff9mapkit/content/walkmesh_hotfix.py +38 -0
- ff9mapkit/data/__init__.py +48 -0
- ff9mapkit/data/_regen_provenance.py +142 -0
- ff9mapkit/data/provenance/blank.es.patch +1 -0
- ff9mapkit/data/provenance/blank.fr.patch +1 -0
- ff9mapkit/data/provenance/blank.gr.patch +1 -0
- ff9mapkit/data/provenance/blank.it.patch +1 -0
- ff9mapkit/data/provenance/blank.jp.patch +1 -0
- ff9mapkit/data/provenance/blank.uk.patch +1 -0
- ff9mapkit/data/provenance/blank.us.patch +1 -0
- ff9mapkit/data/provenance/manifest.json +65 -0
- ff9mapkit/data/provenance/region_template.patch +1 -0
- ff9mapkit/data/reference_arcs.toml +89 -0
- ff9mapkit/data/region_catalog.toml +593 -0
- ff9mapkit/deploystack.py +358 -0
- ff9mapkit/dialogue.py +803 -0
- ff9mapkit/eb/__init__.py +12 -0
- ff9mapkit/eb/_exprtable.py +59 -0
- ff9mapkit/eb/_membertable.py +38 -0
- ff9mapkit/eb/_optables.py +537 -0
- ff9mapkit/eb/_regen_optables.py +76 -0
- ff9mapkit/eb/cmdasm.py +323 -0
- ff9mapkit/eb/disasm.py +332 -0
- ff9mapkit/eb/edit.py +439 -0
- ff9mapkit/eb/exprasm.py +158 -0
- ff9mapkit/eb/model.py +178 -0
- ff9mapkit/eb/opcodes.py +463 -0
- ff9mapkit/eblint.py +177 -0
- ff9mapkit/editor/__init__.py +20 -0
- ff9mapkit/editor/app.py +950 -0
- ff9mapkit/editor/battle_forms.py +240 -0
- ff9mapkit/editor/breadcrumb.py +89 -0
- ff9mapkit/editor/dialogs.py +116 -0
- ff9mapkit/editor/feedback.py +208 -0
- ff9mapkit/editor/forms.py +632 -0
- ff9mapkit/editor/graphview.py +350 -0
- ff9mapkit/editor/jobs.py +342 -0
- ff9mapkit/editor/model.py +243 -0
- ff9mapkit/editor/picker.py +120 -0
- ff9mapkit/editor/theme.py +212 -0
- ff9mapkit/eventscan.py +1441 -0
- ff9mapkit/extract.py +2279 -0
- ff9mapkit/flags.py +693 -0
- ff9mapkit/forkreport.py +1383 -0
- ff9mapkit/hub.py +477 -0
- ff9mapkit/idgated.py +101 -0
- ff9mapkit/infohub.py +580 -0
- ff9mapkit/items.py +63 -0
- ff9mapkit/itemstats.py +346 -0
- ff9mapkit/journey.py +1902 -0
- ff9mapkit/keyitems.py +93 -0
- ff9mapkit/logic_add.py +632 -0
- ff9mapkit/logic_edit.py +728 -0
- ff9mapkit/logic_map.py +526 -0
- ff9mapkit/pack.py +175 -0
- ff9mapkit/playerswap.py +231 -0
- ff9mapkit/prop_archetypes.py +228 -0
- ff9mapkit/provision.py +282 -0
- ff9mapkit/refarc.py +825 -0
- ff9mapkit/save.py +337 -0
- ff9mapkit/save_items.py +1673 -0
- ff9mapkit/scene/__init__.py +11 -0
- ff9mapkit/scene/arena.py +63 -0
- ff9mapkit/scene/bgart.py +140 -0
- ff9mapkit/scene/bgi.py +732 -0
- ff9mapkit/scene/bgs.py +174 -0
- ff9mapkit/scene/bgx.py +185 -0
- ff9mapkit/scene/cam.py +345 -0
- ff9mapkit/scene/guide.py +311 -0
- ff9mapkit/scene/paint.py +506 -0
- ff9mapkit/scene/placeholder.py +107 -0
- ff9mapkit/sjbinary.py +285 -0
- ff9mapkit/sps/__init__.py +17 -0
- ff9mapkit/sps/author.py +294 -0
- ff9mapkit/sps/catalog.py +88 -0
- ff9mapkit/sps/codec.py +264 -0
- ff9mapkit/sps/edit.py +184 -0
- ff9mapkit/sps/lint.py +58 -0
- ff9mapkit/sps/render.py +116 -0
- ff9mapkit/sps/templates.py +47 -0
- ff9mapkit/sps/texture.py +131 -0
- ff9mapkit/walkmesh_hotfixes.py +163 -0
- ff9mapkit/workspace/__init__.py +18 -0
- ff9mapkit/workspace/battledoc.py +985 -0
- ff9mapkit/workspace/builddoc.py +607 -0
- ff9mapkit/workspace/forms_qt.py +586 -0
- ff9mapkit/workspace/importdoc.py +665 -0
- ff9mapkit/workspace/mapview.py +131 -0
- ff9mapkit/workspace/palette.py +85 -0
- ff9mapkit/workspace/savedoc.py +664 -0
- ff9mapkit/workspace/shell.py +6907 -0
- ff9mapkit/workspace/style.py +105 -0
- ff9mapkit/workspace/tuningdialog.py +223 -0
- ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
- ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
- ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
- ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
- ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
- ff9mapkit-1.0.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
"""Ladder primitive -- a region the player climbs, replicating FF9's REAL ladder mechanism.
|
|
2
|
+
|
|
3
|
+
Decoded byte-for-byte from Treno/Residence (the real game; entry 15 = the ladder region, entry 19 =
|
|
4
|
+
the player):
|
|
5
|
+
|
|
6
|
+
- tread (tag 2): ``ifnot(usercontrol) return ; Bubble(1)`` -> the floating "!" prompt
|
|
7
|
+
- action (tag 3): ``ifnot(usercontrol) return ; DisableMove ;
|
|
8
|
+
RunScriptSync(2, 250, <climb_tag>) ; EnableMove`` -> run the PLAYER's climb
|
|
9
|
+
- the player's climb function (``climb_tag``): runs in the player's OWN context (UID 250), so its
|
|
10
|
+
moves move the PLAYER; ``RunScriptSync`` waits for it.
|
|
11
|
+
|
|
12
|
+
Why this shape (the hard-won truth): the controlled player's script loop is NOT stepped while
|
|
13
|
+
``usercontrol == 1``, so a region -> flag -> player-loop scheme can't drive a climb during free
|
|
14
|
+
walking. The region must call the player's climb DIRECTLY via ``RunScriptSync`` (which is exactly what
|
|
15
|
+
the real game does). The real climb is bespoke per-ladder jump arcs (hard-coded coords) -- not
|
|
16
|
+
generalizable -- so the kit's climb is a simple teleport to the destination (+ an optional climb
|
|
17
|
+
gesture). The TRIGGER is faithful; the climb body is simplified.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import struct
|
|
22
|
+
|
|
23
|
+
from ..eb import EbScript, edit, opcodes
|
|
24
|
+
from ..eb.disasm import iter_code
|
|
25
|
+
from . import region as _region
|
|
26
|
+
|
|
27
|
+
PLAYER_UID = 250 # the controlled player's object UID (standard across FF9 fields)
|
|
28
|
+
FIRST_CLIMB_TAG = 17 # the real Treno player ladder funcs start at tag 17; one tag per ladder
|
|
29
|
+
RUNSCRIPT_LEVEL = 2 # the script level arg the real ladder uses for RunScriptSync
|
|
30
|
+
WAIT = 0x22
|
|
31
|
+
STARTSEQ = 0x43 # RunSharedScript -- launches "entry arg0 of this field" as a concurrent Seq
|
|
32
|
+
SETUP_JUMP = 0xE2 # SetupJump(x, y, z, arc): the climb's per-rung jump arcs (absolute dest)
|
|
33
|
+
LADDER_FLAG = 4 # AddCharacterAttribute(4) = "on a ladder": don't floor-snap during a climb
|
|
34
|
+
ZONE_MARGIN = 150 # padding (world units) around the climb's span when auto-sizing a zone
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _s16(v: int) -> int:
|
|
38
|
+
return v - 65536 if v >= 32768 else v
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def climb_landings(climb_bytes: bytes) -> list:
|
|
42
|
+
"""Every ``SetupJump`` (X, Z) destination in a climb -- the absolute world points the player
|
|
43
|
+
lands on while climbing (top, bottom, and any intermediate rungs)."""
|
|
44
|
+
from ..eb.disasm import read_code
|
|
45
|
+
out, pos = [], 0
|
|
46
|
+
while pos < len(climb_bytes):
|
|
47
|
+
try:
|
|
48
|
+
ins, nxt = read_code(climb_bytes, pos)
|
|
49
|
+
except Exception:
|
|
50
|
+
break
|
|
51
|
+
if ins.op == SETUP_JUMP and len(ins.args) >= 3:
|
|
52
|
+
out.append((_s16(ins.args[0]), _s16(ins.args[2]))) # args = (jumpX, jumpY, jumpZ, steps)
|
|
53
|
+
pos = nxt
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def widen_zone_for_climb(zone, climb_bytes: bytes, margin: int = ZONE_MARGIN) -> list:
|
|
58
|
+
"""Return a 4-corner bbox quad covering BOTH the real entry zone AND every climb landing point.
|
|
59
|
+
|
|
60
|
+
An imported real ladder's ``SetRegion`` zone only covers the side the player normally approaches
|
|
61
|
+
from, so a FORK (where the player can end up at either end) gets no '!' prompt at the far end and
|
|
62
|
+
can't climb back. Unioning the zone with the climb's ``SetupJump`` destinations (+ margin) makes
|
|
63
|
+
the trigger span the whole ladder, so it's bidirectional. (Proven in-game: CPMP simple ladder.)"""
|
|
64
|
+
pts = [tuple(p) for p in (zone or [])] + climb_landings(climb_bytes)
|
|
65
|
+
if not pts:
|
|
66
|
+
return zone
|
|
67
|
+
xs = [p[0] for p in pts]
|
|
68
|
+
zs = [p[1] for p in pts]
|
|
69
|
+
x0, x1 = min(xs) - margin, max(xs) + margin
|
|
70
|
+
z0, z1 = min(zs) - margin, max(zs) + margin
|
|
71
|
+
return [[x0, z1], [x1, z1], [x1, z0], [x0, z0]]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_player_entry(eb: EbScript) -> int:
|
|
75
|
+
"""Index of the player entry -- the one running DefinePlayerCharacter (opcode 0x2C)."""
|
|
76
|
+
for e in eb.entries:
|
|
77
|
+
if e.empty:
|
|
78
|
+
continue
|
|
79
|
+
for f in e.funcs:
|
|
80
|
+
for ins in eb.instrs(f):
|
|
81
|
+
if ins.op == 0x2C:
|
|
82
|
+
return e.index
|
|
83
|
+
raise ValueError("no player entry (DefinePlayerCharacter) found -- can't attach a climb function")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def climb_body(dest, *, animation: int | None = None, anim_hold: int = 40) -> bytes:
|
|
87
|
+
"""The player's climb function body: an optional climb gesture, then teleport to ``dest``
|
|
88
|
+
``(x, z)`` or ``(x, z, y)`` + re-enable walkmesh pathing. Runs in the player's context (via
|
|
89
|
+
RunScriptSync), so ``MoveInstantXZY`` moves the player."""
|
|
90
|
+
x, z = int(dest[0]), int(dest[1])
|
|
91
|
+
y = int(dest[2]) if len(dest) > 2 else 0
|
|
92
|
+
body = b""
|
|
93
|
+
if animation is not None:
|
|
94
|
+
body += opcodes.run_animation(int(animation)) + opcodes.encode(WAIT, int(anim_hold))
|
|
95
|
+
body += opcodes.move_instant_xzy(x, z, y) + opcodes.set_pathing(1) + opcodes.RETURN
|
|
96
|
+
return body
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def climb_arc_body(arc_from, arc_to, *, rungs: int = 4, steps: int = 6) -> bytes:
|
|
100
|
+
"""DEPRECATED -- superseded by :func:`navigable_climb_body`. This auto-plays a fixed rung-hop
|
|
101
|
+
sequence end-to-end, which is NOT how FF9 ladders work (real ladders are navigable: you hold the
|
|
102
|
+
d-pad to climb up/down rung-by-rung). Kept only for back-compat; use the navigable climb instead.
|
|
103
|
+
|
|
104
|
+
An ANIMATED generic climb: interpolated `SetupJump`/`Jump` rung-hops from ``arc_from`` to
|
|
105
|
+
``arc_to``, each ``(x, z)`` or ``(x, z, height)`` (height defaults 0 = on the floor). Runs in the
|
|
106
|
+
player's context (RunScriptSync), so each rung moves the PLAYER; the engine projects every world
|
|
107
|
+
rung through the camera, so the climb traces the painted/borrowed ladder for free -- the faithful
|
|
108
|
+
jump-arc behavior, auto-generated from two endpoints (no hand-authored coords). Direction-agnostic:
|
|
109
|
+
pass (bottom, top) to ascend or (top, bottom) to descend. `rungs` = hops, `steps` = frames/hop.
|
|
110
|
+
Ends with `SetPathing(1)` to re-enable walkmesh collision at the destination."""
|
|
111
|
+
fx, fz = int(arc_from[0]), int(arc_from[1])
|
|
112
|
+
fy = int(arc_from[2]) if len(arc_from) > 2 else 0
|
|
113
|
+
tx, tz = int(arc_to[0]), int(arc_to[1])
|
|
114
|
+
ty = int(arc_to[2]) if len(arc_to) > 2 else 0
|
|
115
|
+
rungs = max(1, int(rungs))
|
|
116
|
+
body = opcodes.add_character_attribute(LADDER_FLAG) # ladder flag: don't snap to floor mid-climb
|
|
117
|
+
for i in range(1, rungs + 1):
|
|
118
|
+
f = i / rungs
|
|
119
|
+
x = round(fx + (tx - fx) * f)
|
|
120
|
+
z = round(fz + (tz - fz) * f)
|
|
121
|
+
y = round(fy + (ty - fy) * f)
|
|
122
|
+
body += opcodes.setup_jump(x, z, y, steps) + opcodes.jump()
|
|
123
|
+
if ty != 0: # ending ABOVE the (flat) walkmesh: hold here,
|
|
124
|
+
body += opcodes.set_pathing(0) # keep collision off so it isn't snapped down
|
|
125
|
+
else: # ending ON the floor: dismount to normal walking
|
|
126
|
+
body += opcodes.remove_character_attribute(LADDER_FLAG) + opcodes.set_pathing(1)
|
|
127
|
+
body += opcodes.RETURN
|
|
128
|
+
return body
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# The NAVIGABLE climb -- FF9's real ladder mechanism, recreated from two endpoints.
|
|
133
|
+
#
|
|
134
|
+
# Decoded byte-for-byte from field 706 (EVT_GIZ_TO_WORLD, the Gizamaluke vine; entry 14 = player,
|
|
135
|
+
# func tag 11). The real ladder is a per-frame state machine, NOT an auto-played sequence: mount onto
|
|
136
|
+
# the vine, then a loop that reads the HELD d-pad + the player's world-Y each frame, advances a scratch
|
|
137
|
+
# target +/-step, and snaps the player onto the 3D line between the two endpoints (MoveInstantXZY with
|
|
138
|
+
# X/Z linear in height); leaving the height band ends the loop and a height-keyed selector dismounts at
|
|
139
|
+
# whichever end you left from. The per-vine constants (the line equation, the band) are DERIVED here
|
|
140
|
+
# from the two world endpoints, so it reproduces 706's loop verbatim for 706's endpoints yet works for
|
|
141
|
+
# any new painted vine -- the truthful from-scratch ladder. See project memory + the Session 22 decode.
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
SELF = 255
|
|
144
|
+
F_Y = 1 # op78 field 1 = world-Y-up (= -pos.y); the climb tracks this
|
|
145
|
+
F_ANIMFRAME = 7
|
|
146
|
+
CLIMB_SCRATCH = 2 # MAP.I16[2]: the per-frame climb target (matches 706; transient per-field)
|
|
147
|
+
CLIMB_ANIM = 10539 # the per-frame climb-cycle animation (model-specific; Zidane in 706)
|
|
148
|
+
MOUNT_ANIM = 10687 # SetJumpAnimation for the mount arc
|
|
149
|
+
DISMOUNT_ANIM = 11453 # SetJumpAnimation for the dismount arc
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _const(v: int) -> bytes:
|
|
153
|
+
return bytes([_region.T_CONST]) + struct.pack("<h", int(v))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _selfv(field: int) -> bytes:
|
|
157
|
+
return _region.obj_var(SELF, field)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _scratch() -> bytes:
|
|
161
|
+
return _region._push_var(_region.MAP_INT16, CLIMB_SCRATCH)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _stmt(*toks: bytes) -> bytes:
|
|
165
|
+
"""A complete expression statement: ``05 <tokens> 7F``."""
|
|
166
|
+
return bytes([_region.EXPR_OP]) + b"".join(toks) + bytes([_region.T_END])
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _arg(*toks: bytes) -> bytes:
|
|
170
|
+
"""A bare expression operand (for an opcode arg with its arg_flags bit set): ``<tokens> 7F``."""
|
|
171
|
+
return b"".join(toks) + bytes([_region.T_END])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _Asm:
|
|
175
|
+
"""A tiny label assembler for the climb's mixed forward+backward jumps. Every jump's operand is
|
|
176
|
+
``target_off - (jmp_off + 3)`` as a signed i16 (verified: the engine does ``ip = A + 3 + offset``),
|
|
177
|
+
so one rule covers the forward if-skips AND the loop back-edge -- computed after layout."""
|
|
178
|
+
|
|
179
|
+
def __init__(self):
|
|
180
|
+
self._items = [] # ('raw', bytes) | ('lbl', name) | ('jmp', op, target)
|
|
181
|
+
|
|
182
|
+
def raw(self, b: bytes):
|
|
183
|
+
if b:
|
|
184
|
+
self._items.append(("raw", bytes(b)))
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def label(self, name: str):
|
|
188
|
+
self._items.append(("lbl", name))
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def jmp(self, op: int, target: str):
|
|
192
|
+
self._items.append(("jmp", op, target))
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def assemble(self) -> bytes:
|
|
196
|
+
labels, off = {}, 0
|
|
197
|
+
for it in self._items:
|
|
198
|
+
if it[0] == "lbl":
|
|
199
|
+
labels[it[1]] = off
|
|
200
|
+
elif it[0] == "raw":
|
|
201
|
+
off += len(it[1])
|
|
202
|
+
else:
|
|
203
|
+
off += 3
|
|
204
|
+
out, off = bytearray(), 0
|
|
205
|
+
for it in self._items:
|
|
206
|
+
if it[0] == "raw":
|
|
207
|
+
out += it[1]
|
|
208
|
+
off += len(it[1])
|
|
209
|
+
elif it[0] == "jmp":
|
|
210
|
+
_, op, tgt = it
|
|
211
|
+
out += bytes([op]) + struct.pack("<h", labels[tgt] - (off + 3))
|
|
212
|
+
off += 3
|
|
213
|
+
return bytes(out)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _dismount(anim: int, x: int, z: int, y: int = 0, steps: int = 6, frames=(2, 8)) -> bytes:
|
|
217
|
+
"""Jump off the vine onto the floor at height ``y``, re-enable walkmesh collision + clear the
|
|
218
|
+
ladder flag + restore the animation flags -- the clean dismount (706's floor walk-off, leaned out).
|
|
219
|
+
Real floors are often elevated (non-zero ``y``); ``frames`` = the jump anim's in/out window."""
|
|
220
|
+
return (opcodes.set_jump_animation(anim, frames[0], frames[1]) + opcodes.run_jump_animation()
|
|
221
|
+
+ opcodes.wait_animation() + opcodes.setup_jump(x, z, y, steps) + opcodes.jump()
|
|
222
|
+
+ opcodes.run_land_animation() + opcodes.wait_animation()
|
|
223
|
+
+ opcodes.set_pathing(1) + opcodes.remove_character_attribute(LADDER_FLAG)
|
|
224
|
+
+ opcodes.set_animation_flags(0, 0))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def navigable_climb_body(bottom, top, *, floor_landing=None, top_landing=None, step: int = 20,
|
|
228
|
+
up_mask: int = 0x10, down_mask: int = 0x40, right_alias: bool = False,
|
|
229
|
+
dirs=None, rungs=None,
|
|
230
|
+
climb_anim: int = CLIMB_ANIM, climb_frames: int = 12,
|
|
231
|
+
mount_anim: int = MOUNT_ANIM, dismount_anim: int = DISMOUNT_ANIM,
|
|
232
|
+
mount_steps: int = 4, dismount_steps: int = 6,
|
|
233
|
+
two_way_mount: bool = False, top_mount_anim=None, top_mount_steps=None,
|
|
234
|
+
face_angle: int | None = None, top_action: str = "floor",
|
|
235
|
+
top_field: int | None = None, top_entrance: int = 0,
|
|
236
|
+
top_worldmap: int | None = None) -> bytes:
|
|
237
|
+
"""Recreate FF9's NAVIGABLE ladder climb for a vine between two world endpoints.
|
|
238
|
+
|
|
239
|
+
``bottom`` / ``top`` = ``(x, z, y)`` world points (``y`` = up-positive height; they MUST differ in
|
|
240
|
+
``y``). The player mounts at ``bottom`` and climbs by holding the d-pad: each frame the loop reads
|
|
241
|
+
the held direction (``B_KEY``) + the player's world-Y, advances the scratch target +/- ``step``,
|
|
242
|
+
and snaps the player onto the 3D line between the endpoints (``MoveInstantXZY``, X/Z linear in
|
|
243
|
+
height). Leaving the band ends the loop; a height-keyed selector dismounts to the floor at the end
|
|
244
|
+
you left from (``floor_landing`` for the bottom end, ``top_landing`` for the top -- both default to
|
|
245
|
+
the vine's own x/z at floor level). Runs in the player's own context (the region RunScriptSync's
|
|
246
|
+
it), so its moves move the PLAYER.
|
|
247
|
+
|
|
248
|
+
The line equation + height band are DERIVED from the two endpoints, so passing 706's endpoints
|
|
249
|
+
reproduces 706's loop byte-for-byte, while any new painted vine just supplies its own two points
|
|
250
|
+
(read off the paint guide, same as walkmesh placement). ``up_mask`` / ``down_mask`` are the
|
|
251
|
+
``B_KEY`` button bits (vertical ladder default Up=0x10 / Down=0x40; pass ``right_alias`` for a
|
|
252
|
+
diagonal screen vine that also climbs on Right). Returns the climb function body.
|
|
253
|
+
|
|
254
|
+
``rungs`` = an optional list of >=2 world points (bottom..top) for a MULTI-RUNG (bent) vine -- a
|
|
255
|
+
piecewise-linear climb (the real Cleyra ladder shape): consecutive points form segments, each with
|
|
256
|
+
its own slope, and the snap picks the segment whose selfY band holds the target. When given it
|
|
257
|
+
overrides ``bottom``/``top`` (= rungs[0]/rungs[-1]); the band/mount/dismount use those endpoints."""
|
|
258
|
+
if rungs is not None:
|
|
259
|
+
rungs = [(int(p[0]), int(p[1]), int(p[2]) if len(p) > 2 else 0) for p in rungs]
|
|
260
|
+
if len(rungs) < 2:
|
|
261
|
+
raise ValueError("navigable ladder rungs need >= 2 points (bottom..top)")
|
|
262
|
+
bottom, top = rungs[0], rungs[-1]
|
|
263
|
+
bx, bz = int(bottom[0]), int(bottom[1])
|
|
264
|
+
by = int(bottom[2]) if len(bottom) > 2 else 0
|
|
265
|
+
tx, tz = int(top[0]), int(top[1])
|
|
266
|
+
ty = int(top[2]) if len(top) > 2 else 0
|
|
267
|
+
if ty == by:
|
|
268
|
+
raise ValueError("navigable ladder: top and bottom must differ in height (y)")
|
|
269
|
+
# selfY = -worldY (op78 field 1). Band = [lo, hi] in selfY space; the line is anchored at bottom.
|
|
270
|
+
sy_bottom, sy_top = -by, -ty
|
|
271
|
+
lo, hi = min(sy_bottom, sy_top), max(sy_bottom, sy_top)
|
|
272
|
+
exit_threshold = (lo + hi) // 2
|
|
273
|
+
anchor, slope_den = sy_bottom, sy_top - sy_bottom
|
|
274
|
+
x_slope, z_slope = tx - bx, tz - bz
|
|
275
|
+
# MULTI-RUNG: one (bx, bz, x_slope, z_slope, anchor, slope_den, top_sy) per segment (bottom->top).
|
|
276
|
+
segments = None
|
|
277
|
+
if rungs is not None:
|
|
278
|
+
segments = [(px, pz, qx - px, qz - pz, -py, (-qy) - (-py), -qy)
|
|
279
|
+
for (px, pz, py), (qx, qz, qy) in zip(rungs, rungs[1:])]
|
|
280
|
+
fl = floor_landing if floor_landing else (bx, bz)
|
|
281
|
+
flx, flz = int(fl[0]), int(fl[1]); fly = int(fl[2]) if len(fl) > 2 else 0
|
|
282
|
+
tl = top_landing if top_landing else (tx, tz)
|
|
283
|
+
tlx, tlz = int(tl[0]), int(tl[1]); tly = int(tl[2]) if len(tl) > 2 else 0
|
|
284
|
+
if top_action == "field" and top_field is None:
|
|
285
|
+
raise ValueError('navigable ladder top_action="field" needs top_field')
|
|
286
|
+
if top_action == "worldmap" and top_worldmap is None:
|
|
287
|
+
raise ValueError('navigable ladder top_action="worldmap" needs top_worldmap')
|
|
288
|
+
|
|
289
|
+
def line(base, slope, anc=None, den=None): # base + (target - anc) * slope / den (706 verbatim)
|
|
290
|
+
anc = anchor if anc is None else anc
|
|
291
|
+
den = slope_den if den is None else den
|
|
292
|
+
return _arg(_const(base), _const(slope), _scratch(), _const(anc),
|
|
293
|
+
bytes([_region.T_MINUS]), bytes([_region.T_MULT]),
|
|
294
|
+
_const(den), bytes([_region.T_DIV]), bytes([_region.T_PLUS]))
|
|
295
|
+
|
|
296
|
+
def band(): # (selfY <= hi) && (selfY >= lo) -- still on the vine
|
|
297
|
+
return _stmt(_selfv(F_Y), _const(hi), bytes([_region.T_LE]),
|
|
298
|
+
_selfv(F_Y), _const(lo), bytes([_region.T_GE]), bytes([_region.T_ANDAND]))
|
|
299
|
+
|
|
300
|
+
def anim_window(advance): # SetAnimationInOut((animFrame+advance)%N, ...): step the climb anim
|
|
301
|
+
# a ONE-frame window = the climb's clock. advance=1 plays it FORWARD (ascending), advance=N-1
|
|
302
|
+
# (= -1 mod N) plays it BACKWARD (descending) so the hands match the down motion (real GZML).
|
|
303
|
+
w = _arg(_selfv(F_ANIMFRAME), _const(advance), bytes([_region.T_PLUS]),
|
|
304
|
+
_const(climb_frames), bytes([_region.T_MOD]))
|
|
305
|
+
return opcodes.encode(0x3D, w, w, arg_flags=0b11)
|
|
306
|
+
|
|
307
|
+
def set_target(sign): # MAP.I16[2] = selfY (+/- step); sign=None just holds (= selfY, no move)
|
|
308
|
+
if sign is None:
|
|
309
|
+
return _stmt(_scratch(), _selfv(F_Y), bytes([_region.T_ASSIGN]))
|
|
310
|
+
return _stmt(_scratch(), _selfv(F_Y), _const(step), bytes([sign]), bytes([_region.T_ASSIGN]))
|
|
311
|
+
|
|
312
|
+
a = _Asm()
|
|
313
|
+
|
|
314
|
+
def _pair(v): # a per-END value: scalar -> (v, v); a [bottom, top] list/tuple -> as given
|
|
315
|
+
return (int(v[0]), int(v[1])) if isinstance(v, (list, tuple)) else (int(v), int(v))
|
|
316
|
+
da_b, da_t = _pair(dismount_anim) # per-end dismount ANIM (bottom, top) -- e.g. CPMP 11453 / 10685
|
|
317
|
+
ds_b, ds_t = _pair(dismount_steps) # per-end dismount STEPS -- e.g. CPMP 6 / 8
|
|
318
|
+
tm_anim = mount_anim if top_mount_anim is None else int(top_mount_anim)
|
|
319
|
+
tm_steps = mount_steps if top_mount_steps is None else int(top_mount_steps)
|
|
320
|
+
|
|
321
|
+
def mount_arc(m_anim, dx, dz, dy, m_steps): # face + jump-on anim + jump ONTO a vine end + grip
|
|
322
|
+
return ((opcodes.turn_instant(int(face_angle)) if face_angle is not None else b"")
|
|
323
|
+
+ opcodes.set_jump_animation(int(m_anim), 2, 6) + opcodes.run_jump_animation()
|
|
324
|
+
+ opcodes.wait_animation() + opcodes.add_character_attribute(LADDER_FLAG)
|
|
325
|
+
+ opcodes.setup_jump(int(dx), int(dz), int(dy), int(m_steps)) + opcodes.jump()
|
|
326
|
+
+ opcodes.run_land_animation() + opcodes.wait_animation()
|
|
327
|
+
+ opcodes.set_pathing(0) + opcodes.set_animation_flags(1, 0)
|
|
328
|
+
+ opcodes.set_animation_in_out(0, 0))
|
|
329
|
+
# MOUNT. selfY > the mid-band threshold => arriving near the BOTTOM end.
|
|
330
|
+
a.raw(_stmt(_selfv(F_Y), _const(exit_threshold), bytes([_region.T_GT])))
|
|
331
|
+
if two_way_mount:
|
|
332
|
+
# TWO-WAY (CPMP/TRNO): mount the END you approached -- bottom-arc if low, top-arc if high. Each end
|
|
333
|
+
# has its own jump anim + dest + steps. Needs a region at BOTH floors (inject_navigable_ladder
|
|
334
|
+
# top_zone) so you can stand at either end to press action.
|
|
335
|
+
a.jmp(_region.JMP_FALSE, "TOPMOUNT")
|
|
336
|
+
a.raw(mount_arc(mount_anim, bx, bz, by, mount_steps))
|
|
337
|
+
a.jmp(0x01, "LOOP")
|
|
338
|
+
a.label("TOPMOUNT")
|
|
339
|
+
a.raw(mount_arc(tm_anim, tx, tz, ty, tm_steps))
|
|
340
|
+
else:
|
|
341
|
+
# single bottom-mount; high on the vine (RE-ENTRY) -> skip the mount (the player-init already
|
|
342
|
+
# placed you with the ladder flag + SetPathing(0)) -> drop into the loop. Faithful to 706.
|
|
343
|
+
a.jmp(_region.JMP_FALSE, "LOOP")
|
|
344
|
+
a.raw(mount_arc(mount_anim, bx, bz, by, mount_steps))
|
|
345
|
+
# NAVIGATE LOOP
|
|
346
|
+
a.label("LOOP")
|
|
347
|
+
# input: a FIRST-MATCH-WINS else-if chain (like the real GZML loop). Each held direction advances
|
|
348
|
+
# the climb anim one frame, sets the target +/- step, and JUMPS to the snap (skipping the rest) --
|
|
349
|
+
# so an up-diagonal climbs UP even though the down mask (Down|Left) can overlap it. No input -> HOLD
|
|
350
|
+
# (target = selfY; the anim window is NOT advanced, so it freezes on a grip pose rather than looping).
|
|
351
|
+
# input bindings: an explicit `dirs` list of (mask, "up"|"down") -- first-match-wins -- subsumes the
|
|
352
|
+
# up_mask/down_mask/right_alias shorthand and expresses real multi-key fields (TRNO/UDFT bind both
|
|
353
|
+
# Up AND Left to climb up: dirs=[[0x10,"up"],[0x80,"up"],[0x60,"down"]]). Default = up=0x10/down=0x40.
|
|
354
|
+
if dirs is not None:
|
|
355
|
+
dir_list = [(int(m), _region.T_MINUS if str(d).lower() == "up" else _region.T_PLUS) for m, d in dirs]
|
|
356
|
+
else:
|
|
357
|
+
dir_list = [(up_mask, _region.T_MINUS), (down_mask, _region.T_PLUS)]
|
|
358
|
+
if right_alias:
|
|
359
|
+
dir_list.append((0x20, _region.T_MINUS)) # Right = a second 'up' binding
|
|
360
|
+
for i, (mask, sign) in enumerate(dir_list):
|
|
361
|
+
adv = 1 if sign == _region.T_MINUS else climb_frames - 1 # up = forward, down = backward
|
|
362
|
+
a.raw(_stmt(_const(mask), bytes([_region.T_KEY]))) # if (mask held)
|
|
363
|
+
a.jmp(_region.JMP_FALSE, f"DIR{i}")
|
|
364
|
+
a.raw(anim_window(adv) + set_target(sign))
|
|
365
|
+
a.jmp(0x01, "SNAP")
|
|
366
|
+
a.label(f"DIR{i}")
|
|
367
|
+
a.raw(set_target(None)) # HOLD: no direction held
|
|
368
|
+
a.label("SNAP")
|
|
369
|
+
# snap the player onto the vine line for the new height (X/Z exprs; middle arg = bare target)
|
|
370
|
+
if segments:
|
|
371
|
+
# MULTI-RUNG (bent vine): pick the segment whose selfY band holds the target, snap to ITS line.
|
|
372
|
+
# Checked bottom->top by each segment's top-of-band (selfY GE); the last segment is the fallback.
|
|
373
|
+
for i, (sbx, sbz, sxs, szs, sanc, sden, stop) in enumerate(segments[:-1]):
|
|
374
|
+
a.raw(_stmt(_scratch(), _const(stop), bytes([_region.T_GE]))) # target at/below seg i's top?
|
|
375
|
+
a.jmp(_region.JMP_FALSE, f"SEG{i + 1}")
|
|
376
|
+
a.raw(opcodes.encode(0xA1, line(sbx, sxs, sanc, sden), _arg(_scratch()),
|
|
377
|
+
line(sbz, szs, sanc, sden), arg_flags=0b111))
|
|
378
|
+
a.jmp(0x01, "SNAP_DONE")
|
|
379
|
+
a.label(f"SEG{i + 1}")
|
|
380
|
+
lbx, lbz, lxs, lzs, lanc, lden, _stop = segments[-1]
|
|
381
|
+
a.raw(opcodes.encode(0xA1, line(lbx, lxs, lanc, lden), _arg(_scratch()),
|
|
382
|
+
line(lbz, lzs, lanc, lden), arg_flags=0b111))
|
|
383
|
+
a.label("SNAP_DONE")
|
|
384
|
+
else:
|
|
385
|
+
a.raw(opcodes.encode(0xA1, line(bx, x_slope), _arg(_scratch()), line(bz, z_slope),
|
|
386
|
+
arg_flags=0b111))
|
|
387
|
+
# climb-cycle anim while on the vine, else a 1-frame wait
|
|
388
|
+
a.raw(band())
|
|
389
|
+
a.jmp(_region.JMP_FALSE, "OFFVINE")
|
|
390
|
+
a.raw(opcodes.run_animation(climb_anim) + opcodes.wait_animation())
|
|
391
|
+
a.jmp(0x01, "ANIMDONE")
|
|
392
|
+
a.label("OFFVINE")
|
|
393
|
+
a.raw(opcodes.wait(1))
|
|
394
|
+
a.label("ANIMDONE")
|
|
395
|
+
# loop while still on the vine
|
|
396
|
+
a.raw(band())
|
|
397
|
+
a.jmp(_region.JMP_TRUE, "LOOP")
|
|
398
|
+
# EXIT: selfY > midpoint -> BOTTOM (floor) dismount; else -> the TOP end (per top_action)
|
|
399
|
+
a.raw(_stmt(_selfv(F_Y), _const(exit_threshold), bytes([_region.T_GT])))
|
|
400
|
+
a.jmp(_region.JMP_FALSE, "TOP_END")
|
|
401
|
+
a.raw(_dismount(da_b, flx, flz, fly, ds_b)) # bottom -> floor dismount (per-end anim)
|
|
402
|
+
a.jmp(0x01, "END")
|
|
403
|
+
a.label("TOP_END")
|
|
404
|
+
if top_action == "field": # top -> a Field() gateway
|
|
405
|
+
# The engine's field transition: fade out, WAIT for the fade to finish, set the arrival
|
|
406
|
+
# entrance, then Field(). We do NOT emit PreloadField -- it is opcode 0xFD (HINT), "ignored in
|
|
407
|
+
# the non-PSX versions" (a no-op on Steam); and crucially it must NOT be confused with 0x2A =
|
|
408
|
+
# Battle (emitting 0x2A here literally fired a battle using the field id as the scene). Move/menu
|
|
409
|
+
# are already disabled by the region that RunScriptSync'd this climb.
|
|
410
|
+
a.raw(opcodes.fade_filter(6, 24, 0, 255, 255, 255) + opcodes.wait(25)
|
|
411
|
+
+ _region.set_field_entrance(int(top_entrance))
|
|
412
|
+
+ opcodes.field(int(top_field)) + opcodes.terminate_entry(255))
|
|
413
|
+
elif top_action == "worldmap": # top -> the world map
|
|
414
|
+
a.raw(opcodes.fade_filter(6, 24, 0, 255, 255, 255) + opcodes.wait(25)
|
|
415
|
+
+ _region.set_field_entrance(int(top_entrance))
|
|
416
|
+
+ opcodes.world_map(int(top_worldmap)) + opcodes.terminate_entry(255))
|
|
417
|
+
else: # "floor": dismount onto a top floor
|
|
418
|
+
a.raw(_dismount(da_t, tlx, tlz, tly, ds_t)) # top -> floor dismount (per-end anim)
|
|
419
|
+
a.label("END")
|
|
420
|
+
a.raw(opcodes.RETURN)
|
|
421
|
+
return a.assemble()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def ladder_region(zone, climb_tag: int, *, player_uid: int = PLAYER_UID) -> bytes:
|
|
425
|
+
"""A type-1 region entry: Init ``SetRegion(zone)`` / tread ``Bubble(1)`` / action ``DisableMove;
|
|
426
|
+
RunScriptSync(player climb); EnableMove`` -- the real FF9 ladder trigger."""
|
|
427
|
+
init = _region.set_region(zone) + opcodes.RETURN
|
|
428
|
+
tread = _region.MOVEMENT_GATE + opcodes.bubble(1) + opcodes.RETURN
|
|
429
|
+
action = (_region.MOVEMENT_GATE + opcodes.DISABLE_MOVE
|
|
430
|
+
+ opcodes.run_script_sync(RUNSCRIPT_LEVEL, player_uid, climb_tag)
|
|
431
|
+
+ opcodes.ENABLE_MOVE + opcodes.RETURN)
|
|
432
|
+
funcs = [(0, init), (_region.RANGE_TAG, tread), (_region.INTERACT_TAG, action)]
|
|
433
|
+
table = b""
|
|
434
|
+
pos = len(funcs) * 4
|
|
435
|
+
for tag, body in funcs:
|
|
436
|
+
table += struct.pack("<HH", tag, pos)
|
|
437
|
+
pos += len(body)
|
|
438
|
+
return bytes([_region.REGION_ENTRY_TYPE, len(funcs)]) + table + b"".join(b for _, b in funcs)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def inject_ladder(data, zone, dest=None, *, climb_bytes: bytes | None = None,
|
|
442
|
+
arc_from=None, arc_to=None, rungs: int = 4, steps: int = 6,
|
|
443
|
+
sequences: dict | None = None, climb_tag: int = FIRST_CLIMB_TAG,
|
|
444
|
+
player_uid: int = PLAYER_UID, animation: int | None = None, activate: bool = True):
|
|
445
|
+
"""Inject a ladder: add a climb function (``climb_tag``) to the player entry + a ladder region
|
|
446
|
+
(tread "!" prompt + action -> RunScriptSync the climb), and arm the region. Returns
|
|
447
|
+
``(new_bytes, region_slot)``. For multiple ladders pass a distinct ``climb_tag`` each.
|
|
448
|
+
|
|
449
|
+
The climb is either FAITHFUL or EMULATED:
|
|
450
|
+
* ``climb_bytes`` -- a real ladder's climb function extracted verbatim by
|
|
451
|
+
``eventscan.scan_ladders`` (exact jump arcs, perspective-correct). Grafted as-is; its internal
|
|
452
|
+
jumps are function-relative so they survive the move. This is what ``import`` emits for a fork.
|
|
453
|
+
* ``dest`` -- ``(x, z[, y])``; ``climb_body`` builds a teleport (+ optional gesture). The simple
|
|
454
|
+
generic climb when you have no real ladder to copy.
|
|
455
|
+
|
|
456
|
+
``sequences`` (``{original_entry_index: entry_bytes}``, from ``scan_ladders``) are the concurrent
|
|
457
|
+
helper entries the climb launches via STARTSEQ (e.g. the SetPitchAngle forward-lean). Each is
|
|
458
|
+
appended at a free slot and the climb's STARTSEQ entry-args are remapped to those slots (a
|
|
459
|
+
same-length 1-byte patch -- the climb stays byte-for-byte otherwise). Empty for simple ladders."""
|
|
460
|
+
animated = arc_from is not None and arc_to is not None
|
|
461
|
+
if climb_bytes is None and dest is None and not animated:
|
|
462
|
+
raise ValueError("inject_ladder needs climb_bytes (faithful), arc_from+arc_to (animated arc), or dest (teleport)")
|
|
463
|
+
if animated:
|
|
464
|
+
body = bytearray(climb_arc_body(arc_from, arc_to, rungs=rungs, steps=steps))
|
|
465
|
+
elif climb_bytes is not None:
|
|
466
|
+
body = bytearray(climb_bytes)
|
|
467
|
+
else:
|
|
468
|
+
body = bytearray(climb_body(dest, animation=animation))
|
|
469
|
+
if sequences: # graft the STARTSEQ helper entries + remap
|
|
470
|
+
ei2slot = {}
|
|
471
|
+
for ei in sorted(sequences):
|
|
472
|
+
slot = EbScript.from_bytes(data).first_free_slot()
|
|
473
|
+
data = edit.append_entry(data, slot, sequences[ei])
|
|
474
|
+
ei2slot[ei] = slot
|
|
475
|
+
for ins in iter_code(bytes(body), 0, len(body)):
|
|
476
|
+
if ins.op == STARTSEQ and ins.args and ins.args[0] in ei2slot:
|
|
477
|
+
body[ins.off + 2] = ei2slot[ins.args[0]] # STARTSEQ = 0x43, argflag, entry-arg
|
|
478
|
+
body = bytes(body)
|
|
479
|
+
eb = EbScript.from_bytes(data)
|
|
480
|
+
pe = find_player_entry(eb)
|
|
481
|
+
data = edit.add_function(data, pe, climb_tag, body)
|
|
482
|
+
eb = EbScript.from_bytes(data)
|
|
483
|
+
slot = eb.first_free_slot()
|
|
484
|
+
data = edit.append_entry(data, slot, ladder_region([tuple(p) for p in zone], climb_tag,
|
|
485
|
+
player_uid=player_uid))
|
|
486
|
+
if activate:
|
|
487
|
+
data = edit.activate(data, opcodes.init_region(slot, 0))
|
|
488
|
+
return data, slot
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def square_zone(center, radius: int = 150) -> list:
|
|
492
|
+
"""A 5-point IsInQuad-safe square trigger zone (side 2*radius) centred on ``(x, z)``."""
|
|
493
|
+
cx, cz = int(center[0]), int(center[1])
|
|
494
|
+
r = int(radius)
|
|
495
|
+
c = [[cx - r, cz + r], [cx + r, cz + r], [cx + r, cz - r], [cx - r, cz - r]]
|
|
496
|
+
return c + [c[-1]] # double last vertex (IsInQuad fan safety)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def inject_bidirectional_ladder(data, top, bottom, *, radius: int = 150, rungs: int = 4,
|
|
500
|
+
steps: int = 6, animation: int | None = None,
|
|
501
|
+
first_tag: int = FIRST_CLIMB_TAG):
|
|
502
|
+
"""A from-scratch BIDIRECTIONAL ladder with no real climb to copy: a trigger zone at EACH end, the
|
|
503
|
+
player's location picks the direction (top zone -> down to ``bottom``, bottom zone -> up to
|
|
504
|
+
``top``), so it climbs both ways WITHOUT reading runtime position. ``top``/``bottom`` are the
|
|
505
|
+
trigger-zone centre + landing point for each end.
|
|
506
|
+
|
|
507
|
+
If EITHER endpoint carries a height (``(x, z, y)``, y>0) the climb is ANIMATED -- interpolated
|
|
508
|
+
`SetupJump`/`Jump` rung-hops that the engine projects so they trace the painted/borrowed ladder
|
|
509
|
+
(the faithful behavior, auto-generated). If both are flat ``(x, z)`` it falls back to an instant
|
|
510
|
+
teleport (the zero-info generic). Returns ``(new_bytes, next_tag)`` (consumes two climb tags)."""
|
|
511
|
+
animated = len(top) > 2 or len(bottom) > 2
|
|
512
|
+
if animated: # arc DOWN from the top zone, arc UP from the bottom
|
|
513
|
+
data, _ = inject_ladder(data, square_zone(top, radius), arc_from=top, arc_to=bottom,
|
|
514
|
+
rungs=rungs, steps=steps, climb_tag=first_tag)
|
|
515
|
+
data, _ = inject_ladder(data, square_zone(bottom, radius), arc_from=bottom, arc_to=top,
|
|
516
|
+
rungs=rungs, steps=steps, climb_tag=first_tag + 1)
|
|
517
|
+
else: # flat endpoints -> instant teleport fallback
|
|
518
|
+
data, _ = inject_ladder(data, square_zone(top, radius), dest=bottom,
|
|
519
|
+
climb_tag=first_tag, animation=animation)
|
|
520
|
+
data, _ = inject_ladder(data, square_zone(bottom, radius), dest=top,
|
|
521
|
+
climb_tag=first_tag + 1, animation=animation)
|
|
522
|
+
return data, first_tag + 2
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def inject_navigable_ladder(data, bottom, top, *, floor_landing=None, top_landing=None, zone=None,
|
|
526
|
+
top_zone=None, radius: int = 200, climb_tag: int = FIRST_CLIMB_TAG,
|
|
527
|
+
player_uid: int = PLAYER_UID, activate: bool = True, **climb_kw):
|
|
528
|
+
"""A from-scratch NAVIGABLE ladder between two world endpoints -- FF9's REAL ladder mechanism,
|
|
529
|
+
recreated (NOT the deprecated auto-hop): ONE trigger zone at the vine base -> press action -> hold
|
|
530
|
+
the d-pad to climb up/down, snapped onto the painted vine, dismount at either end.
|
|
531
|
+
|
|
532
|
+
``bottom`` / ``top`` = ``(x, z, y)`` world points (``y`` = up-positive height). The trigger ``zone``
|
|
533
|
+
defaults to a square at the floor step-off point (``floor_landing`` or the bottom's x/z) -- where
|
|
534
|
+
the player stands to mount. Extra climb params (``step``, ``up_mask`` / ``down_mask``,
|
|
535
|
+
``right_alias``, ``climb_anim`` / ``mount_anim`` / ``dismount_anim``, ``face_angle`` ...) pass
|
|
536
|
+
through to :func:`navigable_climb_body`. The generated body is grafted exactly like a faithful
|
|
537
|
+
climb (it IS a climb body), so it reuses the proven trigger/region machinery. Returns
|
|
538
|
+
``(new_bytes, region_slot)``. One climb function => one ladder; pass a distinct ``climb_tag`` each."""
|
|
539
|
+
body = navigable_climb_body(bottom, top, floor_landing=floor_landing, top_landing=top_landing,
|
|
540
|
+
**climb_kw)
|
|
541
|
+
if zone is None:
|
|
542
|
+
base = floor_landing if floor_landing is not None else (int(bottom[0]), int(bottom[1]))
|
|
543
|
+
zone = square_zone(base, radius)
|
|
544
|
+
data, slot = inject_ladder(data, zone, climb_bytes=body, climb_tag=climb_tag,
|
|
545
|
+
player_uid=player_uid, activate=activate)
|
|
546
|
+
if top_zone is not None: # TWO-WAY mount: a SECOND trigger at the TOP floor invoking the SAME
|
|
547
|
+
tz = (square_zone(top_zone, radius) # climb (its height selector picks
|
|
548
|
+
if isinstance(top_zone[0], (int, float)) # the top arc); accept a centre or a
|
|
549
|
+
else [tuple(p) for p in top_zone]) # full quad, like `zone`.
|
|
550
|
+
eb = EbScript.from_bytes(data)
|
|
551
|
+
slot2 = eb.first_free_slot()
|
|
552
|
+
data = edit.append_entry(data, slot2, ladder_region(tz, climb_tag, player_uid=player_uid))
|
|
553
|
+
if activate:
|
|
554
|
+
data = edit.activate(data, opcodes.init_region(slot2, 0))
|
|
555
|
+
return data, slot
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def reentry_spawn_block(x: int, z: int, y: int, *, face: int = 0,
|
|
559
|
+
climb_anim: int = CLIMB_ANIM) -> bytes:
|
|
560
|
+
"""The on-vine RE-ENTRY placement (no RETURN -- meant to be inlined as an ``if`` body in the
|
|
561
|
+
player-init): place the player ON THE VINE at world ``(x, z)`` height ``y``, gripping (ladder flag
|
|
562
|
+
+ detached from the walkmesh + the climb idle pose) and facing ``face``. So when you return to the
|
|
563
|
+
field from the ladder's top gateway you appear high on the vine and climb DOWN to get off (706's
|
|
564
|
+
re-entry pattern). ``y`` is the up-positive height; the encoder negates it like the climb does."""
|
|
565
|
+
return (opcodes.add_character_attribute(LADDER_FLAG)
|
|
566
|
+
+ opcodes.move_instant_xzy(int(x), int(z), int(y))
|
|
567
|
+
+ opcodes.turn_instant(int(face) & 0xFF)
|
|
568
|
+
+ opcodes.set_pathing(0)
|
|
569
|
+
+ opcodes.set_animation_flags(1, 0) + opcodes.set_animation_in_out(0, 0)
|
|
570
|
+
+ opcodes.run_animation(int(climb_anim)))
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
REENTRY_WARMUP = 2 # frames the CLIMB-RUN code entry waits so it runs after the Init has placed you
|
|
574
|
+
# (the PLACEMENT itself is in the Init -> frame 0, pre-render, NO base flash)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def inject_reentry_spawn(data, entrance: int, x: int, z: int, y: int, *, climb_tag: int = FIRST_CLIMB_TAG,
|
|
578
|
+
face: int = 0, climb_anim: int = CLIMB_ANIM,
|
|
579
|
+
player_uid: int = PLAYER_UID, activate: bool = True):
|
|
580
|
+
"""Make the field spawn the player ON THE VINE *already climbing* when entered via ``entrance`` --
|
|
581
|
+
the return from a ladder-top ``Field()`` gateway, so you hold Down to climb off (706's re-entry).
|
|
582
|
+
|
|
583
|
+
Faithful to field 706 (``EVT_GIZ_TO_WORLD``), which uses NO warm-up timer -- it does two things:
|
|
584
|
+
- its **player Init** places you on the vine for the re-entry entrance (``if (entrance==9999)``:
|
|
585
|
+
AddCharacterAttribute + MoveInstantXZY + SetPathing(0) + climb anim) -- so you are positioned
|
|
586
|
+
as the player object is created, *before the first render*, and never flash the base spawn;
|
|
587
|
+
- its **Main_Loop** runs ``RunScriptSync(player, climb)`` so you are in the climb loop from spawn
|
|
588
|
+
(then re-enables control once you dismount).
|
|
589
|
+
|
|
590
|
+
We replicate BOTH halves. The placement is SPLICED INTO the player Init right after
|
|
591
|
+
``DefinePlayerCharacter`` (jump-safe -- matches 706's position; the Init's tail jumps + their RETURN
|
|
592
|
+
target shift together), and the climb is run from a code entry armed at field load (706 uses
|
|
593
|
+
Main_Loop)::
|
|
594
|
+
|
|
595
|
+
# spliced into the player Init, after DefinePlayerCharacter:
|
|
596
|
+
if (D8:2 == entrance) { <on-vine placement> }
|
|
597
|
+
# code entry:
|
|
598
|
+
if (D8:2 == entrance) { Wait; DisableMove; RunScriptSync(player, climb); EnableMove }
|
|
599
|
+
|
|
600
|
+
The earlier base-flash bug was placing you from a *post-Init* code entry (the Init had already
|
|
601
|
+
spawned you at the base, so any wait showed it). Placing in the Init kills the flash with no timer;
|
|
602
|
+
the small climb-run Wait is invisible (you are already on the vine). The climb's mount-gate sees you
|
|
603
|
+
high on the vine and skips the jump-on mount, dropping into the loop -> hold Down to climb off.
|
|
604
|
+
Returns ``(new_bytes, code_slot)``. ``entrance`` must match what the return gateway sets (D8:2);
|
|
605
|
+
``climb_tag`` is the player's climb function for this ladder."""
|
|
606
|
+
eb = EbScript.from_bytes(data)
|
|
607
|
+
pe = find_player_entry(eb)
|
|
608
|
+
init = eb.entry(pe).func_by_tag(0)
|
|
609
|
+
if init is None:
|
|
610
|
+
raise ValueError("player entry has no Init (tag 0)")
|
|
611
|
+
dpc = next((i for i in eb.instrs(init) if i.op == 0x2C), None) # DefinePlayerCharacter
|
|
612
|
+
if dpc is None:
|
|
613
|
+
raise ValueError("player Init has no DefinePlayerCharacter (0x2C); cannot place re-entry spawn")
|
|
614
|
+
rel = dpc.end - init.abs_start # right after DefinePlayerCharacter
|
|
615
|
+
placement = _region.if_block(
|
|
616
|
+
_region.cond_eq(_region.GLOB_INT16, _region.FIELD_ENTRANCE_IDX, int(entrance)),
|
|
617
|
+
reentry_spawn_block(int(x), int(z), int(y), face=face, climb_anim=climb_anim)) # no RETURN
|
|
618
|
+
data = edit.insert_in_function(data, pe, 0, rel, placement)
|
|
619
|
+
# run the climb from a code entry (706 runs it from Main_Loop): post-Init -> you are already placed
|
|
620
|
+
body = (_region.if_block(
|
|
621
|
+
_region.cond_eq(_region.GLOB_INT16, _region.FIELD_ENTRANCE_IDX, int(entrance)),
|
|
622
|
+
opcodes.wait(REENTRY_WARMUP)
|
|
623
|
+
+ opcodes.disable_move()
|
|
624
|
+
+ opcodes.run_script_sync(RUNSCRIPT_LEVEL, player_uid, int(climb_tag)) # run the climb loop
|
|
625
|
+
+ opcodes.enable_move())
|
|
626
|
+
+ opcodes.RETURN)
|
|
627
|
+
code_entry = bytes([0, 1]) + struct.pack("<HH", 0, 4) + body # type-0 entry, 1 func (tag 0)
|
|
628
|
+
eb = EbScript.from_bytes(data)
|
|
629
|
+
slot = eb.first_free_slot()
|
|
630
|
+
data = edit.append_entry(data, slot, code_entry)
|
|
631
|
+
if activate:
|
|
632
|
+
data = edit.activate(data, opcodes.init_code(slot, 0))
|
|
633
|
+
return data, slot
|