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,138 @@
|
|
|
1
|
+
"""Auto-pathing: route a blocked cutscene walk AROUND obstacles into clear straight legs.
|
|
2
|
+
|
|
3
|
+
A FF9 field walk is straight-line + synchronous, so it can't round a corner (off the walkmesh) or pass
|
|
4
|
+
a standing character (its collision box) on its own -- it presses into the obstacle and stalls. This
|
|
5
|
+
finds a route over the walkmesh that avoids both, then string-pulls it down to a few waypoints (which
|
|
6
|
+
the builder emits as a ``path``).
|
|
7
|
+
|
|
8
|
+
Grid A* over the walkmesh bounds: a cell is FREE if its centre is on the walkmesh, at least
|
|
9
|
+
``clearance`` from any wall (the player's controller radius), and at least ``obstacle_r`` from every
|
|
10
|
+
other character's centre (= the collision distance, so the actor never enters a box). Pure stdlib;
|
|
11
|
+
operates on a :class:`ff9mapkit.scene.bgi.BgiWalkmesh`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import heapq
|
|
17
|
+
|
|
18
|
+
from ..scene import cam
|
|
19
|
+
|
|
20
|
+
_NEIGHBORS = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _free(wmesh, x, z, obstacles, clearance, obstacle_r) -> bool:
|
|
24
|
+
if wmesh.point_on_walkmesh(int(round(x)), int(round(z))) is None:
|
|
25
|
+
return False
|
|
26
|
+
if clearance > 0:
|
|
27
|
+
d = wmesh.distance_to_boundary(int(round(x)), int(round(z)))
|
|
28
|
+
if d is not None and d < clearance:
|
|
29
|
+
return False
|
|
30
|
+
r2 = obstacle_r * obstacle_r
|
|
31
|
+
for ox, oz in obstacles:
|
|
32
|
+
if (x - ox) ** 2 + (z - oz) ** 2 < r2:
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _clear(wmesh, a, b, obstacles, clearance, obstacle_r) -> bool:
|
|
38
|
+
"""Is the straight leg a->b fully free (sampled ~every clearance)?"""
|
|
39
|
+
dx, dz = b[0] - a[0], b[1] - a[1]
|
|
40
|
+
dist = (dx * dx + dz * dz) ** 0.5
|
|
41
|
+
n = max(1, int(dist / max(8.0, clearance)))
|
|
42
|
+
for k in range(n + 1):
|
|
43
|
+
t = k / n
|
|
44
|
+
if not _free(wmesh, a[0] + dx * t, a[1] + dz * t, obstacles, clearance, obstacle_r):
|
|
45
|
+
return False
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _simplify(wmesh, pts, obstacles, clearance, obstacle_r) -> list:
|
|
50
|
+
"""String-pull a dense point list to a few waypoints (drop a point when you can see past it).
|
|
51
|
+
Returns the waypoints AFTER the start, always ending at the exact goal (pts[-1])."""
|
|
52
|
+
out, i = [], 0
|
|
53
|
+
while i < len(pts) - 1:
|
|
54
|
+
j = len(pts) - 1
|
|
55
|
+
while j > i + 1 and not _clear(wmesh, pts[i], pts[j], obstacles, clearance, obstacle_r):
|
|
56
|
+
j -= 1
|
|
57
|
+
out.append(pts[j])
|
|
58
|
+
i = j
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _nearest_free_cell(free_cell, gi, gj, span=6):
|
|
63
|
+
"""A free grid cell near (gi, gj) (the exact goal may sit within a margin of a wall/obstacle)."""
|
|
64
|
+
if free_cell(gi, gj):
|
|
65
|
+
return (gi, gj)
|
|
66
|
+
for ring in range(1, span + 1):
|
|
67
|
+
best = None
|
|
68
|
+
for di in range(-ring, ring + 1):
|
|
69
|
+
for dj in range(-ring, ring + 1):
|
|
70
|
+
if max(abs(di), abs(dj)) != ring:
|
|
71
|
+
continue
|
|
72
|
+
if free_cell(gi + di, gj + dj):
|
|
73
|
+
d = di * di + dj * dj
|
|
74
|
+
if best is None or d < best[0]:
|
|
75
|
+
best = (d, (gi + di, gj + dj))
|
|
76
|
+
if best:
|
|
77
|
+
return best[1]
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def route(wmesh, start, goal, obstacles=(), *, cell=64.0, clearance=None, obstacle_r=None,
|
|
82
|
+
max_expand=20000):
|
|
83
|
+
"""Waypoints routing ``start``->``goal`` around walls + obstacles, or ``None`` if unreachable.
|
|
84
|
+
|
|
85
|
+
Returns the interior waypoints + the exact goal (EXCLUDING start), suitable as a ``path``. Stays on
|
|
86
|
+
the walkmesh, >= ``clearance`` from walls, >= ``obstacle_r`` from each obstacle centre. ``obstacles``
|
|
87
|
+
is a list of (x, z) character centres."""
|
|
88
|
+
clearance = cam.COLLISION_RADIUS_W if clearance is None else clearance
|
|
89
|
+
obstacle_r = 2 * cam.OBJECT_COLLISION_W if obstacle_r is None else obstacle_r
|
|
90
|
+
sx, sz = float(start[0]), float(start[1])
|
|
91
|
+
gx, gz = float(goal[0]), float(goal[1])
|
|
92
|
+
|
|
93
|
+
def cell_xz(i, j):
|
|
94
|
+
return (sx + i * cell, sz + j * cell)
|
|
95
|
+
|
|
96
|
+
def free_cell(i, j):
|
|
97
|
+
x, z = cell_xz(i, j)
|
|
98
|
+
return _free(wmesh, x, z, obstacles, clearance, obstacle_r)
|
|
99
|
+
|
|
100
|
+
start_c = (0, 0)
|
|
101
|
+
goal_c = _nearest_free_cell(free_cell, round((gx - sx) / cell), round((gz - sz) / cell))
|
|
102
|
+
if goal_c is None:
|
|
103
|
+
return None
|
|
104
|
+
if start_c == goal_c:
|
|
105
|
+
return [(int(round(gx)), int(round(gz)))]
|
|
106
|
+
|
|
107
|
+
open_h = [(0.0, start_c)]
|
|
108
|
+
came = {}
|
|
109
|
+
g = {start_c: 0.0}
|
|
110
|
+
expand = 0
|
|
111
|
+
while open_h:
|
|
112
|
+
_, c = heapq.heappop(open_h)
|
|
113
|
+
if c == goal_c:
|
|
114
|
+
break
|
|
115
|
+
expand += 1
|
|
116
|
+
if expand > max_expand:
|
|
117
|
+
return None
|
|
118
|
+
for di, dj in _NEIGHBORS:
|
|
119
|
+
n = (c[0] + di, c[1] + dj)
|
|
120
|
+
if n != goal_c and not free_cell(*n): # start/goal cells are taken as given
|
|
121
|
+
continue
|
|
122
|
+
step = cell * (1.41421356 if di and dj else 1.0)
|
|
123
|
+
ng = g[c] + step
|
|
124
|
+
if ng < g.get(n, 1e18):
|
|
125
|
+
g[n] = ng
|
|
126
|
+
came[n] = c
|
|
127
|
+
h = ((n[0] - goal_c[0]) ** 2 + (n[1] - goal_c[1]) ** 2) ** 0.5 * cell
|
|
128
|
+
heapq.heappush(open_h, (ng + h, n))
|
|
129
|
+
if goal_c not in came:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
chain = [goal_c]
|
|
133
|
+
while chain[-1] in came:
|
|
134
|
+
chain.append(came[chain[-1]])
|
|
135
|
+
chain.reverse() # start_c .. goal_c
|
|
136
|
+
pts = [(sx, sz)] + [cell_xz(i, j) for (i, j) in chain[1:-1]] + [(gx, gz)] # exact start..exact goal
|
|
137
|
+
wps = _simplify(wmesh, pts, obstacles, clearance, obstacle_r)
|
|
138
|
+
return [(int(round(x)), int(round(z))) for (x, z) in wps]
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Carry-platform primitive -- a rideable lift/elevator that physically CARRIES the player within one
|
|
2
|
+
field (no Field() re-entry warp), recreating FF9's Pandemonium-elevator mechanism.
|
|
3
|
+
|
|
4
|
+
Decoded from Pandemonium/Elevator (fields 2712 `pd_elv` / 2713 `pd_evd`). The real ride is a fully
|
|
5
|
+
SCRIPTED, control-locked carry -- NOT an engine attach (there is ZERO MoveTileLoop/AttachTile/SIM in
|
|
6
|
+
either field). The boarding region disables control and `RunScriptSync`s a function grafted onto the
|
|
7
|
+
PLAYER object (UID 250); that function moves the player frame-by-frame with `MoveInstantXZY` until he
|
|
8
|
+
reaches the destination height, then control returns:
|
|
9
|
+
|
|
10
|
+
- region (tag 2 tread "!" / tag 3 action): ``DisableMove ; RunScriptSync(2, 250, ride_tag) ; EnableMove``
|
|
11
|
+
- the ride func (``ride_tag``, on the PLAYER): a per-frame loop
|
|
12
|
+
``{ scratch = selfY +/- step ; MoveInstantXZY(line_x, scratch, line_z) ; Wait(1) ;
|
|
13
|
+
while (selfY hasn't reached arrive) }`` then SetPathing(1) (or the optional fade+Field tail).
|
|
14
|
+
|
|
15
|
+
This is the kit's navigable ladder climb (:func:`ladder.navigable_climb_body`) MINUS the d-pad input
|
|
16
|
+
and the mount/dismount arcs: instead of reading the held direction each frame, the carry advances a
|
|
17
|
+
FIXED step toward the destination, so it is auto-driven and always terminates (a linear ride of
|
|
18
|
+
~``duration`` frames). Every byte is emitted by the same proven primitives the climb uses (the ``_Asm``
|
|
19
|
+
label assembler, the ``0xA1`` expression-arg snap, the ``line()`` interpolation, the ladder flag).
|
|
20
|
+
|
|
21
|
+
selfY = -worldY (op78 field 1): a HIGHER physical position is a MORE-NEGATIVE selfY, so a lift going UP
|
|
22
|
+
(arrive above board) advances selfY in the NEGATIVE direction. The carry derives the step sign + the
|
|
23
|
+
arrival test from ``arrive.y`` vs ``board.y`` (they MUST differ -- a zero-height carry never moves).
|
|
24
|
+
|
|
25
|
+
v1 emits the CARRY only; the visible platform is the human's (paint a ride surface, or drive a placed
|
|
26
|
+
GEO model in lockstep -- a follow-up). docs: project memory `project-ff9-moving-platforms-elevators`.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import math
|
|
31
|
+
import struct
|
|
32
|
+
|
|
33
|
+
from ..eb import EbScript, edit, opcodes
|
|
34
|
+
from . import region as _region
|
|
35
|
+
from . import cutscene as _cutscene
|
|
36
|
+
from .ladder import _Asm, _arg, _const, _selfv, _stmt, F_Y, LADDER_FLAG, find_player_entry, square_zone
|
|
37
|
+
|
|
38
|
+
PLAYER_UID = 250 # the controlled player's runtime UID (standard across FF9 fields)
|
|
39
|
+
FIRST_PLATFORM_TAG = 56 # player ride funcs start here -- clear of ladder (17+) / jump (40+) climb tags,
|
|
40
|
+
# below the object-carry player band (64+); one tag per platform
|
|
41
|
+
RUNSCRIPT_LEVEL = 2 # the script level RunScriptSync uses (matches the real ladder/jump triggers)
|
|
42
|
+
PLATFORM_SCRATCH = 3 # MAP.I16[3]: this frame's stepped selfY target (transient per-field)
|
|
43
|
+
PLATFORM_START = 4 # MAP.I16[4]: the captured boarding selfY (the height the player rides FROM)
|
|
44
|
+
PLATFORM_START_X = 5 # MAP.I16[5]: the captured boarding world-X
|
|
45
|
+
PLATFORM_START_Z = 6 # MAP.I16[6]: the captured boarding world-Z
|
|
46
|
+
DEFAULT_DURATION = 32 # ride frames (for the relative `rise` mode -- linear, always terminates)
|
|
47
|
+
DEFAULT_SPEED = 30 # world-units/frame for the absolute `land` mode (ride duration = distance/speed)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _scratch() -> bytes:
|
|
51
|
+
"""This frame's stepped selfY target (MAP.I16[PLATFORM_SCRATCH]); transient per-field."""
|
|
52
|
+
return _region._push_var(_region.MAP_INT16, PLATFORM_SCRATCH)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _scratch_start() -> bytes:
|
|
56
|
+
"""The boarding selfY var (MAP.I16[PLATFORM_START]); captured from the player at ride start."""
|
|
57
|
+
return _region._push_var(_region.MAP_INT16, PLATFORM_START)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _scratch_x() -> bytes:
|
|
61
|
+
"""The boarding world-X var (MAP.I16[PLATFORM_START_X]); captured at ride start (`land` mode)."""
|
|
62
|
+
return _region._push_var(_region.MAP_INT16, PLATFORM_START_X)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _scratch_z() -> bytes:
|
|
66
|
+
"""The boarding world-Z var (MAP.I16[PLATFORM_START_Z]); captured at ride start (`land` mode)."""
|
|
67
|
+
return _region._push_var(_region.MAP_INT16, PLATFORM_START_Z)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _carry_land_body(land, *, speed: int, animation: int | None,
|
|
71
|
+
warp_to: int | None, warp_entrance: int) -> bytes:
|
|
72
|
+
"""Ride the player from WHEREVER he boards to the absolute landing point ``land`` = ``(x, z, y)``, at
|
|
73
|
+
``speed`` world-units/frame, then re-attach to the walkmesh -- landing cleanly on the floor AT
|
|
74
|
+
``land`` (no end-of-ride floor-snap warp). RELATIVE start (captures the boarding position, no
|
|
75
|
+
teleport-in) + ABSOLUTE end (the landing is a real floor). ``land`` must be ABOVE the boarding point
|
|
76
|
+
(the elevator rides UP -> selfY decreases). Used for inter-field-style lifts where you board at the
|
|
77
|
+
bottom and step off onto a higher floor elsewhere in the room."""
|
|
78
|
+
lx, lz = int(land[0]), int(land[1])
|
|
79
|
+
ly = int(land[2]) if len(land) > 2 else 0
|
|
80
|
+
lsy = -ly # landing selfY (= -worldY)
|
|
81
|
+
speed = max(1, int(speed))
|
|
82
|
+
|
|
83
|
+
def selfx(): return _selfv(0)
|
|
84
|
+
def selfz(): return _selfv(2)
|
|
85
|
+
def selfy(): return _selfv(F_Y)
|
|
86
|
+
|
|
87
|
+
def interp(c_start: bytes, target: int) -> bytes:
|
|
88
|
+
# c_start + (target - c_start) * (cur - csy) / (lsy - csy) -- linear, parameterised by selfY
|
|
89
|
+
return _arg(c_start,
|
|
90
|
+
_const(target), c_start, bytes([_region.T_MINUS]), # (target - c_start)
|
|
91
|
+
_scratch(), _scratch_start(), bytes([_region.T_MINUS]), # (cur - csy)
|
|
92
|
+
bytes([_region.T_MULT]),
|
|
93
|
+
_const(lsy), _scratch_start(), bytes([_region.T_MINUS]), # (lsy - csy)
|
|
94
|
+
bytes([_region.T_DIV]), bytes([_region.T_PLUS]))
|
|
95
|
+
|
|
96
|
+
a = _Asm()
|
|
97
|
+
a.raw(opcodes.add_character_attribute(LADDER_FLAG) + opcodes.set_pathing(0)) # grip + detach; NO teleport
|
|
98
|
+
if animation is not None:
|
|
99
|
+
a.raw(opcodes.run_animation(int(animation)))
|
|
100
|
+
# capture the boarding position (x, z, selfY) -- the ride interpolates FROM here
|
|
101
|
+
a.raw(_stmt(_scratch_x(), selfx(), bytes([_region.T_ASSIGN])))
|
|
102
|
+
a.raw(_stmt(_scratch_z(), selfz(), bytes([_region.T_ASSIGN])))
|
|
103
|
+
a.raw(_stmt(_scratch_start(), selfy(), bytes([_region.T_ASSIGN])))
|
|
104
|
+
a.label("LOOP")
|
|
105
|
+
a.raw(_stmt(_scratch(), selfy(), _const(speed), bytes([_region.T_MINUS]), bytes([_region.T_ASSIGN]))) # step UP
|
|
106
|
+
a.raw(opcodes.encode(0xA1, interp(_scratch_x(), lx), _arg(_scratch()), interp(_scratch_z(), lz), arg_flags=0b111))
|
|
107
|
+
a.raw(opcodes.wait(1))
|
|
108
|
+
a.raw(_stmt(selfy(), _const(lsy), bytes([_region.T_GT]))) # selfY still above the landing?
|
|
109
|
+
a.jmp(_region.JMP_TRUE, "LOOP")
|
|
110
|
+
a.raw(opcodes.encode(0xA1, _arg(_const(lx)), _arg(_const(lsy)), _arg(_const(lz)), arg_flags=0b111)) # exact landing
|
|
111
|
+
if warp_to is not None:
|
|
112
|
+
a.raw(opcodes.fade_filter(6, 24, 0, 255, 255, 255) + opcodes.wait(25)
|
|
113
|
+
+ _region.set_field_entrance(int(warp_entrance))
|
|
114
|
+
+ opcodes.field(int(warp_to)) + opcodes.terminate_entry(255))
|
|
115
|
+
else:
|
|
116
|
+
a.raw(opcodes.remove_character_attribute(LADDER_FLAG) + opcodes.set_pathing(1))
|
|
117
|
+
a.raw(opcodes.RETURN)
|
|
118
|
+
return a.assemble()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _assemble_entry(funcs) -> bytes:
|
|
122
|
+
"""Assemble a type-1 (region) entry from ``[(tag, body), ...]`` (the ladder/jump region layout):
|
|
123
|
+
the func table (``<tag:u16><fpos:u16>`` x N) then the concatenated bodies."""
|
|
124
|
+
table = b""
|
|
125
|
+
pos = len(funcs) * 4
|
|
126
|
+
for tag, body in funcs:
|
|
127
|
+
table += struct.pack("<HH", tag, pos)
|
|
128
|
+
pos += len(body)
|
|
129
|
+
return bytes([_region.REGION_ENTRY_TYPE, len(funcs)]) + table + b"".join(b for _, b in funcs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def carry_body(*, rise: int | None = None, land=None, speed: int = DEFAULT_SPEED,
|
|
133
|
+
duration: int = DEFAULT_DURATION, animation: int | None = None,
|
|
134
|
+
warp_to: int | None = None, warp_entrance: int = 0) -> bytes:
|
|
135
|
+
"""The player's ride function, run in the player's context (the region RunScriptSync's it) so the
|
|
136
|
+
moves move the PLAYER. Two modes:
|
|
137
|
+
|
|
138
|
+
* ``land = (x, z, y)`` -- ride from WHEREVER he boards to that absolute landing point (a real floor),
|
|
139
|
+
so he steps off cleanly (no end-of-ride floor-snap). For inter-field-style lifts: board at the
|
|
140
|
+
bottom, ride up, let off on a higher floor elsewhere. ``land`` must be ABOVE the boarding point.
|
|
141
|
+
* ``rise = <units>`` -- lift him ``rise`` world-units vertically (positive = up) from his current
|
|
142
|
+
height, keeping x/z. A pure in-place vertical lift (needs a real floor at the top, else he
|
|
143
|
+
floor-snaps back down when collision re-enables).
|
|
144
|
+
|
|
145
|
+
Both are RELATIVE at the start (capture the boarding position, no teleport-in -- an earlier absolute
|
|
146
|
+
board-snap warped him under a platform model). VISIBILITY is governed by the FIELD CAMERA, not the
|
|
147
|
+
carry: a vertical rise stays rendered only when the camera's depthOffset + shallow pitch map it into
|
|
148
|
+
screen-Y (not depth -- the [100,3996] psxDepth cull) and its vrp band is tall enough to scroll up;
|
|
149
|
+
fork an elevator-style scene+camera (e.g. Pandemonium 2713). With ``warp_to`` the ride ENDS in a
|
|
150
|
+
fade + ``Field()`` re-entry (an inter-floor elevator)."""
|
|
151
|
+
if land is not None:
|
|
152
|
+
return _carry_land_body(land, speed=speed, animation=animation,
|
|
153
|
+
warp_to=warp_to, warp_entrance=warp_entrance)
|
|
154
|
+
if rise is None:
|
|
155
|
+
raise ValueError("carry_body needs land=[x,z,y] (ride to a floor) or rise=<units> (vertical lift)")
|
|
156
|
+
rise = int(rise)
|
|
157
|
+
if rise == 0:
|
|
158
|
+
raise ValueError("carry_body: rise must be non-zero (positive = up) -- a zero ride never moves")
|
|
159
|
+
duration = max(1, int(duration))
|
|
160
|
+
smag = max(1, math.ceil(abs(rise) / duration)) # per-frame selfY step (terminates in ~duration frames)
|
|
161
|
+
up = rise > 0 # UP => selfY (= -worldY) DECREASES
|
|
162
|
+
step_tok = _region.T_MINUS if up else _region.T_PLUS
|
|
163
|
+
test_tok = _region.T_GT if up else _region.T_LT # loop while selfY hasn't reached the target
|
|
164
|
+
|
|
165
|
+
def selfx(): return _selfv(0) # obj field 0 = world X
|
|
166
|
+
def selfz(): return _selfv(2) # obj field 2 = world Z
|
|
167
|
+
def selfy(): return _selfv(F_Y) # obj field 1 = worldY-up (= -pos.y)
|
|
168
|
+
|
|
169
|
+
a = _Asm()
|
|
170
|
+
a.raw(opcodes.add_character_attribute(LADDER_FLAG) + opcodes.set_pathing(0)) # grip + detach; NO teleport
|
|
171
|
+
if animation is not None:
|
|
172
|
+
a.raw(opcodes.run_animation(int(animation))) # optional ride gesture (cosmetic; off by default)
|
|
173
|
+
# capture the boarding selfY, then the destination = start - rise (UP decreases selfY). Ride from there.
|
|
174
|
+
a.raw(_stmt(_scratch_start(), selfy(), bytes([_region.T_ASSIGN])))
|
|
175
|
+
a.raw(_stmt(_scratch(), _scratch_start(), _const(rise), bytes([_region.T_MINUS]), bytes([_region.T_ASSIGN])))
|
|
176
|
+
a.label("LOOP")
|
|
177
|
+
# step selfY one notch toward the target, keeping the player's current x/z (read live, written back)
|
|
178
|
+
a.raw(opcodes.encode(0xA1, _arg(selfx()),
|
|
179
|
+
_arg(selfy(), _const(smag), bytes([step_tok])),
|
|
180
|
+
_arg(selfz()), arg_flags=0b111))
|
|
181
|
+
a.raw(opcodes.wait(1)) # one ride frame (deterministic timing)
|
|
182
|
+
a.raw(_stmt(selfy(), _scratch(), bytes([test_tok]))) # selfY not yet at the target?
|
|
183
|
+
a.jmp(_region.JMP_TRUE, "LOOP")
|
|
184
|
+
a.raw(opcodes.encode(0xA1, _arg(selfx()), _arg(_scratch()), _arg(selfz()), arg_flags=0b111)) # exact final snap
|
|
185
|
+
if warp_to is not None: # ELEVATOR: ride then re-enter the destination floor
|
|
186
|
+
a.raw(opcodes.fade_filter(6, 24, 0, 255, 255, 255) + opcodes.wait(25)
|
|
187
|
+
+ _region.set_field_entrance(int(warp_entrance))
|
|
188
|
+
+ opcodes.field(int(warp_to)) + opcodes.terminate_entry(255))
|
|
189
|
+
else: # in-screen ride: land + hand control back
|
|
190
|
+
a.raw(opcodes.remove_character_attribute(LADDER_FLAG) + opcodes.set_pathing(1))
|
|
191
|
+
a.raw(opcodes.RETURN)
|
|
192
|
+
return a.assemble()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def platform_region(zone, ride_tag: int, *, trigger: str = "action", bubble: bool = True,
|
|
196
|
+
player_uid: int = PLAYER_UID) -> bytes:
|
|
197
|
+
"""A type-1 region entry that boards the player onto the ride (func ``ride_tag`` on the player).
|
|
198
|
+
|
|
199
|
+
``trigger="action"`` (default): Init ``SetRegion`` / tread ``Bubble(1)`` (if ``bubble``) / action
|
|
200
|
+
``DisableMove; RunScriptSync(player, ride_tag); EnableMove`` -- press to board. ``trigger="tread"``:
|
|
201
|
+
the dispatch is on the tread func (auto-board on walk-in). Control is held for the whole ride
|
|
202
|
+
(synchronous ``RunScriptSync``) -- the same proven shape as the ladder/jump trigger."""
|
|
203
|
+
init = _region.set_region(zone) + opcodes.RETURN
|
|
204
|
+
dispatch = (opcodes.DISABLE_MOVE
|
|
205
|
+
+ opcodes.run_script_sync(RUNSCRIPT_LEVEL, player_uid, ride_tag)
|
|
206
|
+
+ opcodes.ENABLE_MOVE + opcodes.RETURN)
|
|
207
|
+
if trigger == "tread":
|
|
208
|
+
body = _region.MOVEMENT_GATE + (opcodes.bubble(1) if bubble else b"") + dispatch
|
|
209
|
+
funcs = [(0, init), (_region.RANGE_TAG, body)]
|
|
210
|
+
else: # "action" -- press-to-board (+ "!" prompt)
|
|
211
|
+
tread = _region.MOVEMENT_GATE + (opcodes.bubble(1) if bubble else b"") + opcodes.RETURN
|
|
212
|
+
action = _region.MOVEMENT_GATE + dispatch
|
|
213
|
+
funcs = [(0, init), (_region.RANGE_TAG, tread), (_region.INTERACT_TAG, action)]
|
|
214
|
+
return _assemble_entry(funcs)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def inject_platform(data, zone, *, rise: int | None = None, land=None, speed: int = DEFAULT_SPEED,
|
|
218
|
+
ride_tag: int = FIRST_PLATFORM_TAG,
|
|
219
|
+
duration: int = DEFAULT_DURATION, animation: int | None = None,
|
|
220
|
+
trigger: str = "action", bubble: bool = True, warp_to: int | None = None,
|
|
221
|
+
warp_entrance: int = 0, player_uid: int = PLAYER_UID, activate: bool = True):
|
|
222
|
+
"""Inject one carry platform: graft the ride function (``ride_tag``) onto the player entry, append a
|
|
223
|
+
boarding region that fires it, and arm the region. Pass ``land=[x,z,y]`` (ride from the boarding spot
|
|
224
|
+
to that landing floor) or ``rise=<units>`` (vertical lift). Returns ``(new_bytes, region_slot)``. For
|
|
225
|
+
multiple platforms pass a distinct ``ride_tag`` each (start at :data:`FIRST_PLATFORM_TAG`)."""
|
|
226
|
+
body = carry_body(rise=rise, land=land, speed=speed, duration=duration, animation=animation,
|
|
227
|
+
warp_to=warp_to, warp_entrance=warp_entrance)
|
|
228
|
+
eb = EbScript.from_bytes(data)
|
|
229
|
+
pe = find_player_entry(eb)
|
|
230
|
+
data = edit.add_function(data, pe, ride_tag, body)
|
|
231
|
+
eb = EbScript.from_bytes(data)
|
|
232
|
+
slot = eb.first_free_slot()
|
|
233
|
+
data = edit.append_entry(data, slot, platform_region([tuple(p) for p in zone], ride_tag,
|
|
234
|
+
trigger=trigger, bubble=bubble, player_uid=player_uid))
|
|
235
|
+
if activate:
|
|
236
|
+
data = edit.activate(data, opcodes.init_region(slot, 0))
|
|
237
|
+
return data, slot
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
RIDE_WARMUP = 2 # tiny warm-up after control is handed; the rise then OVERLAPS the fade-in so the
|
|
241
|
+
# camera pans up AS the map fades in (the player is occluded down the hole until
|
|
242
|
+
# he emerges near the top -- the floor occlusion, not the fade, hides him)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def entry_rise_body(*, land, rise: int, duration: int = DEFAULT_DURATION, animation: int | None = None) -> bytes:
|
|
246
|
+
"""The on-arrival elevator ride, ENTIRELY in the player's post-fade ride function (RunScriptSync
|
|
247
|
+
controls the player reliably here -- a drop spliced into the player Init does NOT stick, the engine
|
|
248
|
+
re-places him on the floor). One self-contained function:
|
|
249
|
+
|
|
250
|
+
detach -> MoveInstantXZY to the EXACT hole bottom (lx, -ly+rise, lz) -> per-frame rise loop that
|
|
251
|
+
pins x/z to (lx, lz) CONSTANTS and only decreases selfY toward the floor -> exact floor snap ->
|
|
252
|
+
re-attach (land flush).
|
|
253
|
+
|
|
254
|
+
Everything is ABSOLUTE (no stale-selfY capture -> no overshoot) and the rise is a plain decrement (NO
|
|
255
|
+
division -> the land-mode divide-by-(land-start) garbage that flung the player sideways cannot happen).
|
|
256
|
+
x/z are re-asserted every frame so he can never drift left/right."""
|
|
257
|
+
lx, lz = int(land[0]), int(land[1])
|
|
258
|
+
ly = int(land[2]) if len(land) > 2 else 0
|
|
259
|
+
rise = abs(int(rise))
|
|
260
|
+
floor_selfy = -ly # the let-off floor selfY (= -worldY)
|
|
261
|
+
bottom_selfy = -ly + rise # the hole bottom, `rise` below the floor
|
|
262
|
+
duration = max(1, int(duration))
|
|
263
|
+
smag = max(1, math.ceil(rise / duration)) # per-frame UP step (selfY decreases)
|
|
264
|
+
|
|
265
|
+
def selfy(): return _selfv(F_Y)
|
|
266
|
+
|
|
267
|
+
a = _Asm()
|
|
268
|
+
a.raw(opcodes.add_character_attribute(LADDER_FLAG) + opcodes.set_pathing(0)) # detach; don't floor-snap
|
|
269
|
+
a.raw(opcodes.encode(0xA1, _arg(_const(lx)), _arg(_const(bottom_selfy)), _arg(_const(lz)),
|
|
270
|
+
arg_flags=0b111)) # drop to the hole bottom (abs)
|
|
271
|
+
if animation is not None:
|
|
272
|
+
a.raw(opcodes.run_animation(int(animation)))
|
|
273
|
+
a.label("LOOP")
|
|
274
|
+
a.raw(opcodes.encode(0xA1, _arg(_const(lx)), # x pinned (no drift)
|
|
275
|
+
_arg(selfy(), _const(smag), bytes([_region.T_MINUS])), # selfY - step (rise up)
|
|
276
|
+
_arg(_const(lz)), arg_flags=0b111)) # z pinned
|
|
277
|
+
a.raw(opcodes.wait(1))
|
|
278
|
+
a.raw(_stmt(selfy(), _const(floor_selfy), bytes([_region.T_GT]))) # still below the floor?
|
|
279
|
+
a.jmp(_region.JMP_TRUE, "LOOP")
|
|
280
|
+
a.raw(opcodes.encode(0xA1, _arg(_const(lx)), _arg(_const(floor_selfy)), _arg(_const(lz)),
|
|
281
|
+
arg_flags=0b111)) # exact floor (abs)
|
|
282
|
+
a.raw(opcodes.remove_character_attribute(LADDER_FLAG) + opcodes.set_pathing(1)) # land flush on the floor
|
|
283
|
+
a.raw(opcodes.RETURN)
|
|
284
|
+
return a.assemble()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def inject_entry_rise(data, *, land, rise: int, ride_tag: int = FIRST_PLATFORM_TAG,
|
|
288
|
+
duration: int = DEFAULT_DURATION, animation: int | None = None,
|
|
289
|
+
player_uid: int = PLAYER_UID):
|
|
290
|
+
"""The on-ARRIVAL elevator: graft the self-contained drop+rise (:func:`entry_rise_body`) onto the
|
|
291
|
+
player and arm a post-control ``InitCode`` trigger that spins until ``usercontrol == 1`` then runs it
|
|
292
|
+
synchronously. NO Init-drop splice (it doesn't stick) and NO dividing land-mode (it flung the player
|
|
293
|
+
sideways) -- the whole ride is the absolute, division-free, x/z-pinned function. Returns new bytes."""
|
|
294
|
+
lx, lz = int(land[0]), int(land[1])
|
|
295
|
+
ly = int(land[2]) if len(land) > 2 else 0
|
|
296
|
+
out = data if isinstance(data, (bytes, bytearray)) else data.to_bytes()
|
|
297
|
+
pe = find_player_entry(EbScript.from_bytes(out))
|
|
298
|
+
out = edit.add_function(out, pe, ride_tag,
|
|
299
|
+
entry_rise_body(land=(lx, lz, ly), rise=rise, duration=duration, animation=animation))
|
|
300
|
+
# post-control trigger: spin until usercontrol==1, a tiny warm-up, then run the ride synchronously
|
|
301
|
+
a = _Asm()
|
|
302
|
+
a.label("WAITCTL")
|
|
303
|
+
a.raw(opcodes.wait(1))
|
|
304
|
+
a.raw(_region.cond_sysvar_eq(2, 0)) # usercontrol still 0 (no control yet)?
|
|
305
|
+
a.jmp(_region.JMP_TRUE, "WAITCTL") # yes -> keep spinning (op_03 = backward-safe)
|
|
306
|
+
a.raw(opcodes.wait(RIDE_WARMUP)
|
|
307
|
+
+ opcodes.DISABLE_MOVE
|
|
308
|
+
+ opcodes.run_script_sync(RUNSCRIPT_LEVEL, player_uid, ride_tag)
|
|
309
|
+
+ opcodes.ENABLE_MOVE + opcodes.RETURN)
|
|
310
|
+
entry = bytes([0x00, 0x01]) + struct.pack("<HH", 0, 4) + a.assemble()
|
|
311
|
+
slot = EbScript.from_bytes(out).first_free_slot()
|
|
312
|
+
out = edit.append_entry(out, slot, entry)
|
|
313
|
+
out = edit.activate(out, opcodes.init_code(slot, 0))
|
|
314
|
+
return out
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Player-function graft -- carry the donor field's player functions onto the fork player.
|
|
2
|
+
|
|
3
|
+
The next step after object carry (:mod:`content.object`): when a carried object's interactive function
|
|
4
|
+
``RunScript``s a PLAYER tag >= 2 (e.g. the field-122 cask's tag-2 does ``RunScript(player, 24)`` -> the
|
|
5
|
+
player turns toward the cask), that tag doesn't exist on the blank fork player (tags ``[0, 1]`` only), so
|
|
6
|
+
the object graft drops it to ``init_only``. This module grafts those donor player functions onto the fork
|
|
7
|
+
player at a fresh tag band, the object's ``RunScript`` tag is remapped to the new tag (in
|
|
8
|
+
:func:`content.object.remap_entry_refs`), and -- the load-bearing catch -- the donor Init's animation-pack
|
|
9
|
+
loads are spliced so the grafted gestures' clips are actually loaded.
|
|
10
|
+
|
|
11
|
+
It GENERALIZES the one-function jump/ladder graft (:func:`content.jump.inject_jump` /
|
|
12
|
+
:func:`content.ladder.inject_ladder` each add ONE player function via :func:`eb.edit.add_function`) to N
|
|
13
|
+
functions, with:
|
|
14
|
+
|
|
15
|
+
* :class:`PlayerTagAllocator` -- a single next-free allocator across the ladder (17+), jump (40+) and
|
|
16
|
+
object (64+) bands, so the bands never collide regardless of count.
|
|
17
|
+
* :func:`remap_player_tag_calls` -- the intra-graft player->player tag remap (depth-0 in practice).
|
|
18
|
+
* :func:`ensure_player_anim_packs` -- the donor Init's ``RunModelCode`` pack-loads spliced into the fork
|
|
19
|
+
player Init.
|
|
20
|
+
|
|
21
|
+
Specs come from :func:`ff9mapkit.eventscan.scan_player_funcs` (only ``safety == "clean"`` funcs are
|
|
22
|
+
grafted; the rest leave their seeding object ``init_only``). Full recipe: ``docs/PLAYER_GRAFT.md``.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from .. import eventscan
|
|
27
|
+
from ..eb import EbScript, edit, opcodes
|
|
28
|
+
from ..eb.disasm import iter_code
|
|
29
|
+
from .jump import FIRST_JUMP_TAG
|
|
30
|
+
from .ladder import FIRST_CLIMB_TAG, find_player_entry
|
|
31
|
+
from .object import _arg_byte_offset
|
|
32
|
+
|
|
33
|
+
FIRST_OBJECT_PLAYER_TAG = 64 # the object-referenced player-func band (clear of ladder 17+, jump 40+)
|
|
34
|
+
DEFINE_PC_OP = 0x2C # DefinePlayerCharacter -- the splice point for the anim-pack prologue
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlayerTagAllocator:
|
|
38
|
+
"""Hands out fresh fork-player function tags, never colliding across the three graft bands
|
|
39
|
+
(ladder 17+ / jump 40+ / object 64+). Built from the fork player's existing tags (``{0, 1}`` on a
|
|
40
|
+
blank fork); :meth:`take` starts at the band floor and slides past any prior band's overflow, so a
|
|
41
|
+
field with > 24 jumps (which would push the jump band into 64) can't alias the object band. For every
|
|
42
|
+
in-budget field this returns exactly the fixed-counter tags the jump/ladder grafts use today (so their
|
|
43
|
+
in-game proofs + the hut golden are preserved); it only changes the previously-broken overflow case."""
|
|
44
|
+
|
|
45
|
+
FLOORS = {"ladder": FIRST_CLIMB_TAG, "jump": FIRST_JUMP_TAG, "object": FIRST_OBJECT_PLAYER_TAG}
|
|
46
|
+
|
|
47
|
+
def __init__(self, data):
|
|
48
|
+
eb = data if isinstance(data, EbScript) else EbScript.from_bytes(data)
|
|
49
|
+
self._used = {f.tag for f in eb.entry(find_player_entry(eb)).funcs}
|
|
50
|
+
|
|
51
|
+
def take(self, kind, n=1):
|
|
52
|
+
"""``n`` fresh tags in ``kind``'s band ('ladder'|'jump'|'object'), none colliding with prior takes."""
|
|
53
|
+
t, out = self.FLOORS[kind], []
|
|
54
|
+
for _ in range(int(n)):
|
|
55
|
+
while t in self._used:
|
|
56
|
+
t += 1
|
|
57
|
+
self._used.add(t)
|
|
58
|
+
out.append(t)
|
|
59
|
+
t += 1
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def remap_player_tag_calls(body, tagmap) -> bytes:
|
|
64
|
+
"""Site (b): within a grafted player function body, remap an intra-graft player->player ``RunScript``'s
|
|
65
|
+
tag arg (arg2) to its fork tag. Depth-0 in practice (the census found no object-path player func calls
|
|
66
|
+
another player tag), so this is a defensive same-length pass; function-relative jumps survive untouched."""
|
|
67
|
+
if not tagmap:
|
|
68
|
+
return bytes(body)
|
|
69
|
+
b = bytearray(body)
|
|
70
|
+
for ins in iter_code(bytes(b), 0, len(b)):
|
|
71
|
+
if ins.op in eventscan.RUNSCRIPT_OPS:
|
|
72
|
+
uid, tag = ins.imm(1), ins.imm(2)
|
|
73
|
+
if uid in (eventscan.UID_PLAYER, eventscan.UID_SELF) and tag in tagmap:
|
|
74
|
+
bo = _arg_byte_offset(ins, 2)
|
|
75
|
+
if bo is not None:
|
|
76
|
+
b[ins.off + bo] = tagmap[tag] & 0xFF
|
|
77
|
+
return bytes(b)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def ensure_player_anim_packs(data, packs) -> bytes:
|
|
81
|
+
"""Splice the donor player Init's ``RunModelCode`` anim-pack loads into the fork player Init (after
|
|
82
|
+
``DefinePlayerCharacter``), so a grafted gesture's clip is actually loaded -- the fork player otherwise
|
|
83
|
+
loads only the blank-field default pack, leaving a clip from another pack SILENTLY unloaded
|
|
84
|
+
(docs/PLAYER_GRAFT.md S4). ``packs`` are decoded ``RunModelCode`` arg tuples (from
|
|
85
|
+
:func:`ff9mapkit.eventscan.scan_player_funcs`), re-encoded byte-exact. De-duped + idempotent (skips
|
|
86
|
+
packs the fork Init already loads). Generalizes :func:`content.jump.ensure_jump_animation`."""
|
|
87
|
+
if not packs:
|
|
88
|
+
return data
|
|
89
|
+
eb = EbScript.from_bytes(data)
|
|
90
|
+
pe = find_player_entry(eb)
|
|
91
|
+
init = eb.entry(pe).func_by_tag(0)
|
|
92
|
+
if init is None:
|
|
93
|
+
return data
|
|
94
|
+
have = {tuple(ins.args) for ins in eb.instrs(init)
|
|
95
|
+
if ins.op == eventscan.RUN_MODEL_CODE_OP and not any(ins.arg_is_expr)}
|
|
96
|
+
block = b"".join(opcodes.encode(eventscan.RUN_MODEL_CODE_OP, *p)
|
|
97
|
+
for p in packs if tuple(p) not in have)
|
|
98
|
+
if not block:
|
|
99
|
+
return data
|
|
100
|
+
dpc = next((i for i in eb.instrs(init) if i.op == DEFINE_PC_OP), None)
|
|
101
|
+
rel = (dpc.end - init.abs_start) if dpc is not None else 0
|
|
102
|
+
return edit.insert_in_function(data, pe, 0, rel, block)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def graft_player_funcs(data, specs, tagmap, *, load=None, graftable_safeties=("clean",)) -> bytes:
|
|
106
|
+
"""Graft each graftable player function verbatim onto the fork player at its fork tag
|
|
107
|
+
(``tagmap[donor_tag]``) via :func:`eb.edit.add_function` -- the N-function generalization of the
|
|
108
|
+
one-func jump/ladder graft. Splices the donor Init's anim packs first (so the gestures' clips load).
|
|
109
|
+
Non-graftable funcs are skipped (their seeding object stays ``init_only``). ``specs`` come from
|
|
110
|
+
:func:`ff9mapkit.eventscan.scan_player_funcs`; bodies are inline (``body``) or loaded via
|
|
111
|
+
``load(spec["bin"])``. Returns the new ``.eb`` bytes (unchanged when nothing is graftable).
|
|
112
|
+
|
|
113
|
+
``graftable_safeties`` (default ``("clean",)``) selects which safety classes are grafted; the
|
|
114
|
+
text-carry path passes ``("clean", "text")`` so a ``text`` player func (one whose window TXID the carry
|
|
115
|
+
is about to remap + ship) is also grafted -- its bytes are graft-safe once its text is carried. Default
|
|
116
|
+
is byte-identical to before (only ``clean`` funcs)."""
|
|
117
|
+
ok = set(graftable_safeties)
|
|
118
|
+
clean = [s for s in specs if s.get("safety") in ok and int(s["donor_tag"]) in tagmap]
|
|
119
|
+
if not clean:
|
|
120
|
+
return data
|
|
121
|
+
packs = []
|
|
122
|
+
for s in clean:
|
|
123
|
+
for p in s.get("donor_init_packs") or []:
|
|
124
|
+
if tuple(p) not in packs:
|
|
125
|
+
packs.append(tuple(p))
|
|
126
|
+
data = ensure_player_anim_packs(data, packs)
|
|
127
|
+
pe = find_player_entry(EbScript.from_bytes(data))
|
|
128
|
+
for s in clean:
|
|
129
|
+
body = s.get("body")
|
|
130
|
+
if body is None:
|
|
131
|
+
if load is None:
|
|
132
|
+
raise ValueError(f"player_func spec (tag {s.get('donor_tag')}) has no body and no loader")
|
|
133
|
+
body = load(s["bin"])
|
|
134
|
+
body = remap_player_tag_calls(bytes(body), tagmap)
|
|
135
|
+
data = edit.add_function(data, pe, tagmap[int(s["donor_tag"])], body)
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def remap_player_func_siblings(data, tagmap, slot_map) -> bytes:
|
|
140
|
+
"""Post-graft pass: in each grafted player function, remap a CARRIED-sibling uid ref to that sibling's
|
|
141
|
+
fork slot. A grafted func may ``TurnTowardObject`` a carried sibling -- the save Moogle's funcs 13/14/15
|
|
142
|
+
each ``TurnTowardObject(<Moogle donor slot>)``; once the object graft has placed the Moogle at its fork
|
|
143
|
+
slot (``slot_map``: donor_idx -> fork_slot), rewrite the uid. Same-length 1-byte patch, exactly like
|
|
144
|
+
:func:`content.object.remap_entry_refs`. Only uids that are carried-object slots (in ``slot_map``) are
|
|
145
|
+
touched -- player/self/party uids are never in ``slot_map``. Runs AFTER both grafts so the map exists; a
|
|
146
|
+
no-op (byte-identical) without carried siblings (``slot_map`` empty / no matching ref)."""
|
|
147
|
+
if not slot_map or not tagmap:
|
|
148
|
+
return data
|
|
149
|
+
eb = EbScript.from_bytes(data)
|
|
150
|
+
pe = find_player_entry(eb)
|
|
151
|
+
fork_tags = set(tagmap.values())
|
|
152
|
+
b = bytearray(data)
|
|
153
|
+
for f in eb.entry(pe).funcs:
|
|
154
|
+
if f.tag not in fork_tags:
|
|
155
|
+
continue
|
|
156
|
+
for ins in eb.instrs(f):
|
|
157
|
+
spec = eventscan.REF_OPS.get(ins.op)
|
|
158
|
+
if not spec:
|
|
159
|
+
continue
|
|
160
|
+
for ai in spec.get("uid", ()):
|
|
161
|
+
if ai >= len(ins.arg_is_expr) or ins.arg_is_expr[ai]:
|
|
162
|
+
continue
|
|
163
|
+
v = ins.imm(ai)
|
|
164
|
+
if v is not None and int(v) in slot_map:
|
|
165
|
+
bo = _arg_byte_offset(ins, ai)
|
|
166
|
+
if bo is not None:
|
|
167
|
+
b[ins.off + bo] = slot_map[int(v)] & 0xFF
|
|
168
|
+
return bytes(b)
|