madlysimple 0.1.0__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.
- madlysimple/__init__.py +23 -0
- madlysimple/actorparams.py +367 -0
- madlysimple/cameradata.py +213 -0
- madlysimple/classes.py +24 -0
- madlysimple/cprotoactor.py +172 -0
- madlysimple/extended_stream.py +5 -0
- madlysimple/levelhub.py +160 -0
- madlysimple/prefabs/__init__.py +5 -0
- madlysimple/prefabs/prefab.py +383 -0
- madlysimple-0.1.0.dist-info/METADATA +9 -0
- madlysimple-0.1.0.dist-info/RECORD +12 -0
- madlysimple-0.1.0.dist-info/WHEEL +4 -0
madlysimple/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .extended_stream import LevelStream
|
|
2
|
+
|
|
3
|
+
from .classes import RWClass
|
|
4
|
+
from .actorparams import ActorParamBlock, CProtoActorParams, SharedActorParams
|
|
5
|
+
from .cprotoactor import CProtoActor
|
|
6
|
+
from .cameradata import CameraData, CameraDataOffset
|
|
7
|
+
from .levelhub import LevelHub
|
|
8
|
+
|
|
9
|
+
from .prefabs import Prefab
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RWClass",
|
|
13
|
+
"ActorParamBlock",
|
|
14
|
+
"CProtoActorParams",
|
|
15
|
+
"SharedActorParams",
|
|
16
|
+
"CProtoActor",
|
|
17
|
+
"CameraData",
|
|
18
|
+
"CameraDataOffset",
|
|
19
|
+
"LevelHub",
|
|
20
|
+
"Prefab",
|
|
21
|
+
# --
|
|
22
|
+
"LevelStream", # LevelStream.load(fp)
|
|
23
|
+
]
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""The actor parameter record, as write-through attribute views.
|
|
2
|
+
|
|
3
|
+
`CProtoActor` attribute 1 is one 272-byte blob that the engine copies
|
|
4
|
+
straight into the live actor object. It is really two records glued
|
|
5
|
+
together:
|
|
6
|
+
|
|
7
|
+
+0x00..0x3f CProtoActor-only prefix -> CProtoActorParams
|
|
8
|
+
+0x40..0x10f shared 208-byte record -> SharedActorParams
|
|
9
|
+
|
|
10
|
+
The second half is byte-for-byte the same record `CTFBModel` attribute 1
|
|
11
|
+
carries on its own (there at +0x00), which is why it is its own class --
|
|
12
|
+
point `SharedActorParams` at a model with ``base=0`` and the same field
|
|
13
|
+
names work.
|
|
14
|
+
|
|
15
|
+
Both classes read and write through to the entity, exactly like the
|
|
16
|
+
CameraData properties do: nothing is cached, so an edit lands on the very
|
|
17
|
+
entity the stream holds.
|
|
18
|
+
|
|
19
|
+
actor.params.activationRange = 300.0
|
|
20
|
+
actor.params.noShadow = True
|
|
21
|
+
actor.sharedParams.update(health=100, weight=20.0)
|
|
22
|
+
|
|
23
|
+
Field names and offsets come from the engine's own ``::actor`` script
|
|
24
|
+
descriptor table -- see the `madagascar-actor-param-block` memory note.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import struct
|
|
28
|
+
from typing import Any, ClassVar, overload, override
|
|
29
|
+
|
|
30
|
+
from madformats.streamfuncs import RW_sf_CreateEntity
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Field:
|
|
34
|
+
"""One scalar at a fixed offset inside a parameter block.
|
|
35
|
+
|
|
36
|
+
A descriptor rather than a property so the whole layout reads as a
|
|
37
|
+
table -- offset, name and doc on one line each.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_fmt: ClassVar[str] = "<I"
|
|
41
|
+
|
|
42
|
+
def __init__(self, offset: int, doc: str = "", readOnly: bool = False) -> None:
|
|
43
|
+
self.offset = offset
|
|
44
|
+
self.readOnly = readOnly
|
|
45
|
+
self.name = ""
|
|
46
|
+
self.__doc__ = doc
|
|
47
|
+
|
|
48
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
49
|
+
self.name = name
|
|
50
|
+
|
|
51
|
+
def _coerce(self, value: Any) -> Any:
|
|
52
|
+
return int(value)
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def __get__(self, instance: None, owner: type | None = None) -> "_Field": ...
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
def __get__(
|
|
59
|
+
self, instance: "ActorParamBlock", owner: type | None = None
|
|
60
|
+
) -> Any: ...
|
|
61
|
+
|
|
62
|
+
def __get__(self, instance: "ActorParamBlock | None", owner: type | None = None):
|
|
63
|
+
if instance is None:
|
|
64
|
+
return self
|
|
65
|
+
return instance.readValue(self.offset, self._fmt)
|
|
66
|
+
|
|
67
|
+
def __set__(self, instance: "ActorParamBlock", value: Any) -> None:
|
|
68
|
+
if self.readOnly:
|
|
69
|
+
raise AttributeError(
|
|
70
|
+
f"{type(instance).__name__}.{self.name} is read-only "
|
|
71
|
+
"(the engine never reads it back, changing it only risks "
|
|
72
|
+
"breaking the record)"
|
|
73
|
+
)
|
|
74
|
+
instance.writeValue(self.offset, self._fmt, self._coerce(value))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _FloatField(_Field):
|
|
78
|
+
_fmt: ClassVar[str] = "<f"
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def _coerce(self, value: Any) -> float:
|
|
82
|
+
return float(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _UIntField(_Field):
|
|
86
|
+
_fmt: ClassVar[str] = "<I"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _IntField(_Field):
|
|
90
|
+
_fmt: ClassVar[str] = "<i"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _FlagField(_Field):
|
|
94
|
+
"""One bit of the flags bitmask, exposed as a bool."""
|
|
95
|
+
|
|
96
|
+
_fmt: ClassVar[str] = "<I"
|
|
97
|
+
|
|
98
|
+
def __init__(self, offset: int, bit: int, doc: str = "") -> None:
|
|
99
|
+
super().__init__(offset, doc)
|
|
100
|
+
self.bit = bit
|
|
101
|
+
|
|
102
|
+
@overload
|
|
103
|
+
def __get__(self, instance: None, owner: type | None = None) -> "_FlagField": ...
|
|
104
|
+
|
|
105
|
+
@overload
|
|
106
|
+
def __get__(
|
|
107
|
+
self, instance: "ActorParamBlock", owner: type | None = None
|
|
108
|
+
) -> bool: ...
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
def __get__(self, instance: "ActorParamBlock | None", owner: type | None = None):
|
|
112
|
+
if instance is None:
|
|
113
|
+
return self
|
|
114
|
+
return bool((instance.readValue(self.offset, self._fmt) >> self.bit) & 1)
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
def __set__(self, instance: "ActorParamBlock", value: Any) -> None:
|
|
118
|
+
flags: int = instance.readValue(self.offset, self._fmt)
|
|
119
|
+
if value:
|
|
120
|
+
flags |= 1 << self.bit
|
|
121
|
+
else:
|
|
122
|
+
flags &= ~(1 << self.bit) & 0xFFFFFFFF
|
|
123
|
+
instance.writeValue(self.offset, self._fmt, flags)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ActorParamBlock:
|
|
127
|
+
"""Shared machinery: a fixed slice of one entity attribute.
|
|
128
|
+
|
|
129
|
+
Subclasses declare their fields as `_Field` descriptors with offsets
|
|
130
|
+
relative to `base`, so the same field table can be pointed at the same
|
|
131
|
+
record wherever it is embedded.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
__slots__ = ("_base", "_command", "_className", "_owner")
|
|
135
|
+
|
|
136
|
+
SIZE: ClassVar[int] = 0
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
owner: RW_sf_CreateEntity,
|
|
141
|
+
className: str = "CProtoActor",
|
|
142
|
+
command: int = 1,
|
|
143
|
+
base: int = 0,
|
|
144
|
+
) -> None:
|
|
145
|
+
self._owner = owner
|
|
146
|
+
self._className = className
|
|
147
|
+
self._command = command
|
|
148
|
+
self._base = base
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def base(self) -> int:
|
|
152
|
+
"""Byte offset of this record inside the attribute payload."""
|
|
153
|
+
return self._base
|
|
154
|
+
|
|
155
|
+
def asBytes(self) -> bytes:
|
|
156
|
+
"""Detached copy of just this record's bytes."""
|
|
157
|
+
blob = self._owner.getAttribute(self._className, self._command).data
|
|
158
|
+
return blob[self._base : self._base + self.SIZE]
|
|
159
|
+
|
|
160
|
+
def readValue(self, offset: int, fmt: str) -> Any:
|
|
161
|
+
"""One struct-format value at `offset` inside this record."""
|
|
162
|
+
blob = self._owner.getAttribute(self._className, self._command).data
|
|
163
|
+
at = self._base + offset
|
|
164
|
+
if at + struct.calcsize(fmt) > len(blob):
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"{self._className} attribute {self._command} is only "
|
|
167
|
+
f"{len(blob)} bytes, too short to hold offset 0x{at:x}"
|
|
168
|
+
)
|
|
169
|
+
return struct.unpack_from(fmt, blob, at)[0]
|
|
170
|
+
|
|
171
|
+
def writeValue(self, offset: int, fmt: str, value: Any) -> None:
|
|
172
|
+
"""Overwrite one struct-format value at `offset`, leaving the rest alone."""
|
|
173
|
+
attr = self._owner.getAttribute(self._className, self._command)
|
|
174
|
+
at = self._base + offset
|
|
175
|
+
if at + struct.calcsize(fmt) > len(attr.data):
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"{self._className} attribute {self._command} is only "
|
|
178
|
+
f"{len(attr.data)} bytes, too short to hold offset 0x{at:x}"
|
|
179
|
+
)
|
|
180
|
+
data = bytearray(attr.data)
|
|
181
|
+
struct.pack_into(fmt, data, at, value)
|
|
182
|
+
attr.data = bytes(data)
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def fields(cls) -> dict[str, _Field]:
|
|
186
|
+
"""Every declared field, in layout order."""
|
|
187
|
+
found: dict[str, _Field] = {}
|
|
188
|
+
for klass in reversed(cls.__mro__):
|
|
189
|
+
for name, value in vars(klass).items():
|
|
190
|
+
if isinstance(value, _Field):
|
|
191
|
+
found[name] = value
|
|
192
|
+
return found
|
|
193
|
+
|
|
194
|
+
def asDict(self) -> dict[str, Any]:
|
|
195
|
+
return {name: getattr(self, name) for name in self.fields()}
|
|
196
|
+
|
|
197
|
+
def update(self, **values: Any) -> None:
|
|
198
|
+
"""Set several fields at once, rejecting names that don't exist."""
|
|
199
|
+
known = self.fields()
|
|
200
|
+
for name, value in values.items():
|
|
201
|
+
if name not in known:
|
|
202
|
+
raise AttributeError(f"{type(self).__name__} has no field {name!r}")
|
|
203
|
+
setattr(self, name, value)
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
def __repr__(self) -> str:
|
|
207
|
+
body = ",\n".join(f" {n}={v!r}" for n, v in self.asDict().items())
|
|
208
|
+
return f"{type(self).__name__}(\n{body}\n)"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class CProtoActorParams(ActorParamBlock):
|
|
212
|
+
"""The CProtoActor-only prefix -- blob +0x00..0x3f, actor +0x1ac.
|
|
213
|
+
|
|
214
|
+
Everything here is about *being an actor placed in a level*: when the
|
|
215
|
+
actor wakes up, and how the editor/radar draws it. The stats live next
|
|
216
|
+
door in `SharedActorParams`.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
SIZE: ClassVar[int] = 0x40
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
owner: RW_sf_CreateEntity,
|
|
224
|
+
className: str = "CProtoActor",
|
|
225
|
+
command: int = 1,
|
|
226
|
+
base: int = 0x00,
|
|
227
|
+
) -> None:
|
|
228
|
+
super().__init__(owner, className, command, base)
|
|
229
|
+
|
|
230
|
+
version = _UIntField(0x00, "Record format version -- 2 everywhere.", readOnly=True)
|
|
231
|
+
flags = _UIntField(0x04, "Raw flags bitmask; see the bool fields below.")
|
|
232
|
+
activationRange = _FloatField(
|
|
233
|
+
0x08, "Distance at which the actor wakes up and starts ticking. 0 = always on."
|
|
234
|
+
)
|
|
235
|
+
deactivationRange = _FloatField(
|
|
236
|
+
0x0C, "Distance at which it sleeps again. 9999/10000 = never deactivate."
|
|
237
|
+
)
|
|
238
|
+
coneColor = _UIntField(0x10, "Palette index (0/3/4/8) for the debug vision cone.")
|
|
239
|
+
unknown0x14 = _UIntField(0x14, "1 on 5727/5730 actors. No reader found.")
|
|
240
|
+
blipColor = _UIntField(0x18, "Palette index (0/2) for the radar dot.")
|
|
241
|
+
activationRangeFadePercent = _UIntField(
|
|
242
|
+
0x1C, "Fade-in fraction of the activation range. 0 in all shipped data."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
solidCollision = _FlagField(0x04, 0, "Actor blocks other actors.")
|
|
246
|
+
wallClimber = _FlagField(0x04, 1, "Actor can climb walls.")
|
|
247
|
+
wallCollision = _FlagField(0x04, 2, "Actor collides with walls.")
|
|
248
|
+
noShadow = _FlagField(0x04, 3, "Shadow renderer skips this actor.")
|
|
249
|
+
visibleOnRadar = _FlagField(0x04, 4, "Drawn on the radar using blipColor.")
|
|
250
|
+
cutSceneIgnore = _FlagField(
|
|
251
|
+
0x04, 5, "Excluded from normal cut-scene processing outside game mode 3."
|
|
252
|
+
)
|
|
253
|
+
ignoresGroundColor = _FlagField(
|
|
254
|
+
0x04, 6, "Skips ground-tint lighting. Never set in shipped data."
|
|
255
|
+
)
|
|
256
|
+
tiltsWithGround = _FlagField(0x04, 7, "Pitches/rolls to match the ground normal.")
|
|
257
|
+
meshCollider = _FlagField(
|
|
258
|
+
0x04, 8, "Collide against the mesh instead of the box (needs actor+0x2c8)."
|
|
259
|
+
)
|
|
260
|
+
trianglesCollide = _FlagField(
|
|
261
|
+
0x04, 9, "Per-triangle collision (needs actor+0x2c8)."
|
|
262
|
+
)
|
|
263
|
+
unknownBit10 = _FlagField(0x04, 10, "Set on 16 actors; no accessor, no reader.")
|
|
264
|
+
rwClumpFlag0x1000 = _FlagField(
|
|
265
|
+
0x04, 11, "Model-instance ctor applies RW clump flag 0x1000 when set."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class SharedActorParams(ActorParamBlock):
|
|
270
|
+
"""The 208-byte record shared with CTFBModel -- the actor's stats.
|
|
271
|
+
|
|
272
|
+
On a `CProtoActor` this sits at blob +0x40 (the default); on a
|
|
273
|
+
`CTFBModel` it is the whole of attribute 1, so build it as
|
|
274
|
+
``SharedActorParams(model, "CTFBModel", 1, 0)``.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
SIZE: ClassVar[int] = 0xD0
|
|
278
|
+
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
owner: RW_sf_CreateEntity,
|
|
282
|
+
className: str = "CProtoActor",
|
|
283
|
+
command: int = 1,
|
|
284
|
+
base: int = 0x40,
|
|
285
|
+
) -> None:
|
|
286
|
+
super().__init__(owner, className, command, base)
|
|
287
|
+
|
|
288
|
+
version = _UIntField(0x00, "Record format version -- 2 everywhere.", readOnly=True)
|
|
289
|
+
|
|
290
|
+
# --- jumping / gravity -------------------------------------------------
|
|
291
|
+
jumpCoastFrames = _UIntField(
|
|
292
|
+
0x04, "Frames the jump floats after takeoff before gravity takes over."
|
|
293
|
+
)
|
|
294
|
+
jumpDelay = _UIntField(0x08, "Windup frames between the jump input and takeoff.")
|
|
295
|
+
initialJumpVelocity = _FloatField(0x2C, "Launch speed applied at takeoff.")
|
|
296
|
+
upTopSpeed = _FloatField(0x30, "Terminal upward speed.")
|
|
297
|
+
upAcceleration = _FloatField(0x34, "Upward acceleration while rising.")
|
|
298
|
+
downAcceleration = _FloatField(0x38, "Gravity -- downward acceleration.")
|
|
299
|
+
downTopSpeed = _FloatField(0x3C, "Terminal falling speed.")
|
|
300
|
+
bounceRestitution = _FloatField(0x40, "Elasticity on landing. 0 on nearly all.")
|
|
301
|
+
|
|
302
|
+
# --- core stats --------------------------------------------------------
|
|
303
|
+
health = _UIntField(0x0C, "Hit points. 30 on most actors. Script get/set.")
|
|
304
|
+
moveType = _UIntField(0x10, "Locomotion mode enum (0/1/2/4).")
|
|
305
|
+
weight = _FloatField(0x44, "Mass for physics / knockback / push resolution.")
|
|
306
|
+
id = _IntField(0x98, "Designer-assigned numeric id; -1 = unset. Script get/set.")
|
|
307
|
+
|
|
308
|
+
# --- ground movement ---------------------------------------------------
|
|
309
|
+
groundTopSpeed = _FloatField(
|
|
310
|
+
0x14, "Max ground speed, and the upper edge of the fast-move anim band."
|
|
311
|
+
)
|
|
312
|
+
groundTurnRate = _FloatField(
|
|
313
|
+
0x18, "Max ground turn rate, deg/s; also drives the lean anims."
|
|
314
|
+
)
|
|
315
|
+
groundAcceleration = _FloatField(0x48, "How fast it reaches groundTopSpeed.")
|
|
316
|
+
groundDeceleration = _FloatField(0x4C, "How fast it stops on the ground.")
|
|
317
|
+
|
|
318
|
+
# --- air movement ------------------------------------------------------
|
|
319
|
+
airTopSpeed = _FloatField(0x50, "Horizontal speed cap while airborne.")
|
|
320
|
+
airAcceleration = _FloatField(0x54, "Horizontal air control, accelerating.")
|
|
321
|
+
airDeceleration = _FloatField(0x58, "Horizontal air control, slowing.")
|
|
322
|
+
airTurnRate = _FloatField(0x74, "Turn rate while airborne, deg/s.")
|
|
323
|
+
|
|
324
|
+
# --- vision cone -------------------------------------------------------
|
|
325
|
+
coneAngle = _FloatField(0x1C, "Half-angle of the vision / attention cone.")
|
|
326
|
+
coneLength = _FloatField(0x20, "Range of that cone.")
|
|
327
|
+
coneSweepOffset = _FloatField(0x24, "Angular offset while the cone sweeps.")
|
|
328
|
+
unknown0x28 = _FloatField(0x28, "10 or 0, sits with the cone fields. No reader.")
|
|
329
|
+
|
|
330
|
+
# --- collision box (feet; the engine scales by 0.5 * 12) ---------------
|
|
331
|
+
hExtent = _FloatField(0x5C, "Collision-box height, in feet.")
|
|
332
|
+
wExtent = _FloatField(0x60, "Collision-box width, in feet.")
|
|
333
|
+
lExtent = _FloatField(0x64, "Collision-box length, in feet.")
|
|
334
|
+
xOffset = _FloatField(0x68, "Box centre offset.")
|
|
335
|
+
yOffset = _FloatField(0x6C, "Box centre offset.")
|
|
336
|
+
zOffset = _FloatField(
|
|
337
|
+
0x70, "Vertical box offset -- usually hExtent/2, so the base sits on the origin."
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# --- locomotion animation rate bands -----------------------------------
|
|
341
|
+
# FUN_004235b0 picks a band from ground speed, then lerps min->max
|
|
342
|
+
# across it and divides by 100 to get the clip playback rate.
|
|
343
|
+
slowestToSlowSpeed = _FloatField(
|
|
344
|
+
0x78, "Speed at which slowest-move gives way to slow-move."
|
|
345
|
+
)
|
|
346
|
+
slowToFastSpeed = _FloatField(
|
|
347
|
+
0x7C, "Speed at which slow-move gives way to fast-move."
|
|
348
|
+
)
|
|
349
|
+
slowestMoveRateMin = _IntField(0x80, "Slowest-move playback rate x100, band bottom.")
|
|
350
|
+
slowestMoveRateMax = _IntField(0x84, "Slowest-move playback rate x100, band top.")
|
|
351
|
+
slowMoveRateMin = _IntField(0x88, "Slow-move playback rate x100, band bottom.")
|
|
352
|
+
slowMoveRateMax = _IntField(0x8C, "Slow-move playback rate x100, band top.")
|
|
353
|
+
fastMoveRateMin = _IntField(0x90, "Fast-move playback rate x100, band bottom.")
|
|
354
|
+
fastMoveRateMax = _IntField(0x94, "Fast-move playback rate x100, band top.")
|
|
355
|
+
|
|
356
|
+
# --- dead editor defaults ---------------------------------------------
|
|
357
|
+
unused50 = _FloatField(0xB0, "50 on every actor and model. No reader.", readOnly=True)
|
|
358
|
+
unused100 = _FloatField(0xB4, "100 on every actor and model. No reader.", readOnly=True)
|
|
359
|
+
unused150 = _FloatField(0xB8, "150 on every actor and model. No reader.", readOnly=True)
|
|
360
|
+
|
|
361
|
+
def checkAlignment(self) -> bool:
|
|
362
|
+
"""True if the dead 50/100/150 triple is where it should be.
|
|
363
|
+
|
|
364
|
+
Nothing reads those three floats, which is exactly what makes them a
|
|
365
|
+
cheap sanity check that `base` points at a real shared record.
|
|
366
|
+
"""
|
|
367
|
+
return (self.unused50, self.unused100, self.unused150) == (50.0, 100.0, 150.0)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import cast, overload, override
|
|
4
|
+
|
|
5
|
+
from madformats.lib.parser import Parser
|
|
6
|
+
from madformats.streamfuncs import RW_sf_CreateEntity
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _packVector3(value: Sequence[float]) -> bytes:
|
|
11
|
+
values = tuple(value)
|
|
12
|
+
if len(values) != 3:
|
|
13
|
+
raise ValueError(f"Expected 3 offsets, got {len(values)}")
|
|
14
|
+
return struct.pack("<3f", *values)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CameraDataOffset(Sequence[float]):
|
|
18
|
+
"""Write-through view of a 3-float CameraData offset attribute.
|
|
19
|
+
|
|
20
|
+
Reads like the tuple the getters used to return -- unpacking, indexing,
|
|
21
|
+
iteration and comparison against a tuple all still work -- but assigning to
|
|
22
|
+
an element rewrites the attribute on the entity:
|
|
23
|
+
|
|
24
|
+
cam.lookFromOffset[1] = 12.0
|
|
25
|
+
cam.lookAtOffset.heading = 90.0
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__slots__ = ("_command", "_owner")
|
|
29
|
+
|
|
30
|
+
def __init__(self, owner: "CameraData", command: int) -> None:
|
|
31
|
+
self._owner = owner
|
|
32
|
+
self._command = command
|
|
33
|
+
|
|
34
|
+
def _values(self) -> tuple[float, float, float]:
|
|
35
|
+
cmdp = Parser(self._owner.getAttribute("CameraData", self._command).data)
|
|
36
|
+
return (cmdp.readFloat(), cmdp.readFloat(), cmdp.readFloat())
|
|
37
|
+
|
|
38
|
+
def asTuple(self) -> tuple[float, float, float]:
|
|
39
|
+
"""Detached copy, for when you want a value that will not follow edits."""
|
|
40
|
+
return self._values()
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def __len__(self) -> int:
|
|
44
|
+
return 3
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
def __getitem__(self, index: int) -> float: ...
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def __getitem__(self, index: slice) -> tuple[float, ...]: ...
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
def __getitem__(self, index: int | slice) -> float | tuple[float, ...]:
|
|
54
|
+
return self._values()[index]
|
|
55
|
+
|
|
56
|
+
def __setitem__(self, index: int, value: float) -> None:
|
|
57
|
+
values = list(self._values())
|
|
58
|
+
values[index] = float(value) # IndexError on out of range, as with a list
|
|
59
|
+
self._owner.setAttribute("CameraData", self._command, _packVector3(values))
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def horizontal(self) -> float:
|
|
63
|
+
return self._values()[0]
|
|
64
|
+
|
|
65
|
+
@horizontal.setter
|
|
66
|
+
def horizontal(self, value: float) -> None:
|
|
67
|
+
self[0] = value
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def vertical(self) -> float:
|
|
71
|
+
return self._values()[1]
|
|
72
|
+
|
|
73
|
+
@vertical.setter
|
|
74
|
+
def vertical(self, value: float) -> None:
|
|
75
|
+
self[1] = value
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def heading(self) -> float:
|
|
79
|
+
return self._values()[2]
|
|
80
|
+
|
|
81
|
+
@heading.setter
|
|
82
|
+
def heading(self, value: float) -> None:
|
|
83
|
+
self[2] = value
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
def __eq__(self, other: object) -> bool:
|
|
87
|
+
if isinstance(other, CameraDataOffset):
|
|
88
|
+
return self._values() == other._values()
|
|
89
|
+
if isinstance(other, (tuple, list)):
|
|
90
|
+
values = cast(Sequence[object], other)
|
|
91
|
+
return len(values) == 3 and self._values() == tuple(values)
|
|
92
|
+
return NotImplemented
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return repr(self._values())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CameraDataOffsetAttribute:
|
|
100
|
+
"""One 3-float offset command, exposed as an attribute of CameraData.
|
|
101
|
+
|
|
102
|
+
A descriptor rather than a property because reads and writes have different
|
|
103
|
+
types: reading hands back a write-through CameraDataOffset, writing accepts
|
|
104
|
+
any 3-element sequence.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, command: int, doc: str) -> None:
|
|
108
|
+
self._command = command
|
|
109
|
+
self.__doc__ = doc
|
|
110
|
+
|
|
111
|
+
@overload
|
|
112
|
+
def __get__(
|
|
113
|
+
self, instance: None, owner: type | None = None
|
|
114
|
+
) -> "CameraDataOffsetAttribute": ...
|
|
115
|
+
|
|
116
|
+
@overload
|
|
117
|
+
def __get__(
|
|
118
|
+
self, instance: "CameraData", owner: type | None = None
|
|
119
|
+
) -> CameraDataOffset: ...
|
|
120
|
+
|
|
121
|
+
def __get__(
|
|
122
|
+
self, instance: "CameraData | None", owner: type | None = None
|
|
123
|
+
) -> "CameraDataOffset | CameraDataOffsetAttribute":
|
|
124
|
+
if instance is None:
|
|
125
|
+
return self
|
|
126
|
+
return CameraDataOffset(instance, self._command)
|
|
127
|
+
|
|
128
|
+
def __set__(self, instance: "CameraData", value: Sequence[float]) -> None:
|
|
129
|
+
instance.setAttribute("CameraData", self._command, _packVector3(value))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class CameraData(RW_sf_CreateEntity):
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_entity(cls, entity: RW_sf_CreateEntity) -> "CameraData":
|
|
136
|
+
# Re-tag in place instead of copying, so writes through the properties
|
|
137
|
+
# land on the very entity the stream holds.
|
|
138
|
+
entity.__class__ = cls
|
|
139
|
+
camd = cast(CameraData, entity)
|
|
140
|
+
return camd
|
|
141
|
+
|
|
142
|
+
def _setString(self, command: int, value: str) -> None:
|
|
143
|
+
self.getAttribute("CameraData", command).setString(value)
|
|
144
|
+
|
|
145
|
+
def _setFloat(self, command: int, value: float) -> None:
|
|
146
|
+
self.setAttribute("CameraData", command, struct.pack("<f", value))
|
|
147
|
+
|
|
148
|
+
def _setBool(self, command: int, value: bool) -> None:
|
|
149
|
+
self.setAttribute("CameraData", command, struct.pack("<i", int(value)))
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def cameraDataName(self) -> str:
|
|
153
|
+
return self.getAttribute("CameraData", 0).asString()
|
|
154
|
+
|
|
155
|
+
@cameraDataName.setter
|
|
156
|
+
def cameraDataName(self, value: str) -> None:
|
|
157
|
+
self._setString(0, value)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def lookFromTarget(self) -> str:
|
|
161
|
+
return self.getAttribute("CameraData", 1).asString()
|
|
162
|
+
|
|
163
|
+
@lookFromTarget.setter
|
|
164
|
+
def lookFromTarget(self, value: str) -> None:
|
|
165
|
+
self._setString(1, value)
|
|
166
|
+
|
|
167
|
+
lookFromOffset = CameraDataOffsetAttribute(2, "(horizontal,vertical,heading) offsets")
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def lookFromHeadingRelative(self) -> bool:
|
|
171
|
+
cmdp = Parser(self.getAttribute("CameraData", 3).data)
|
|
172
|
+
return cmdp.readBool()
|
|
173
|
+
|
|
174
|
+
@lookFromHeadingRelative.setter
|
|
175
|
+
def lookFromHeadingRelative(self, value: bool) -> None:
|
|
176
|
+
self._setBool(3, value)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def lookAtTarget(self) -> str:
|
|
180
|
+
return self.getAttribute("CameraData", 4).asString()
|
|
181
|
+
|
|
182
|
+
@lookAtTarget.setter
|
|
183
|
+
def lookAtTarget(self, value: str) -> None:
|
|
184
|
+
self._setString(4, value)
|
|
185
|
+
|
|
186
|
+
lookAtOffset = CameraDataOffsetAttribute(5, "(horizontal,vertical,heading) offsets")
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def lookAtHeadingRelative(self) -> bool:
|
|
190
|
+
cmdp = Parser(self.getAttribute("CameraData", 6).data)
|
|
191
|
+
return cmdp.readBool()
|
|
192
|
+
|
|
193
|
+
@lookAtHeadingRelative.setter
|
|
194
|
+
def lookAtHeadingRelative(self, value: bool) -> None:
|
|
195
|
+
self._setBool(6, value)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def fov(self) -> float:
|
|
199
|
+
cmdp = Parser(self.getAttribute("CameraData", 7).data)
|
|
200
|
+
return cmdp.readFloat()
|
|
201
|
+
|
|
202
|
+
@fov.setter
|
|
203
|
+
def fov(self, value: float) -> None:
|
|
204
|
+
self._setFloat(7, value)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def laziness(self) -> bool:
|
|
208
|
+
cmdp = Parser(self.getAttribute("CameraData", 8).data)
|
|
209
|
+
return cmdp.readBool()
|
|
210
|
+
|
|
211
|
+
@laziness.setter
|
|
212
|
+
def laziness(self, value: bool) -> None:
|
|
213
|
+
self._setBool(8, value)
|
madlysimple/classes.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RWClass(str, Enum):
|
|
5
|
+
CSystemCommands = "CSystemCommands"
|
|
6
|
+
CTFBCommand = "CTFBCommand"
|
|
7
|
+
CProtoActor = "CProtoActor"
|
|
8
|
+
CAttributeHandler = "CAttributeHandler"
|
|
9
|
+
LevelHub = "LevelHub"
|
|
10
|
+
CTFBWorld = "CTFBWorld"
|
|
11
|
+
CTFBModel = "CTFBModel"
|
|
12
|
+
SpriteManager = "SpriteManager"
|
|
13
|
+
SpriteObject = "SpriteObject"
|
|
14
|
+
AssetHub = "AssetHub"
|
|
15
|
+
CTFBSound = "CTFBSound"
|
|
16
|
+
CameraData = "CameraData"
|
|
17
|
+
CFXPartSpray = "CFXPartSpray"
|
|
18
|
+
CFXColorLight = "CFXColorLight"
|
|
19
|
+
GameCamera = "GameCamera"
|
|
20
|
+
CDirectorsCamera = "CDirectorsCamera"
|
|
21
|
+
CFXMotionBlur = "CFXMotionBlur"
|
|
22
|
+
RadarRender = "RadarRender"
|
|
23
|
+
TFBShadowCamera = "TFBShadowCamera"
|
|
24
|
+
ShadowRender = "ShadowRender"
|