exp-runtime-cod 0.5.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.
- exp_runtime/cod/__init__.py +1 -0
- exp_runtime/cod/assets.py +76 -0
- exp_runtime/cod/audio.py +6 -0
- exp_runtime/cod/content.py +7 -0
- exp_runtime/cod/profile.py +36 -0
- exp_runtime/cod/services.py +56 -0
- exp_runtime_cod-0.5.0.dist-info/METADATA +47 -0
- exp_runtime_cod-0.5.0.dist-info/RECORD +11 -0
- exp_runtime_cod-0.5.0.dist-info/WHEEL +5 -0
- exp_runtime_cod-0.5.0.dist-info/licenses/LICENSE +21 -0
- exp_runtime_cod-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cause of Death native game contracts."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Verified Cause of Death IPA UI roles. Script-visible IDs remain exact.
|
|
2
|
+
|
|
3
|
+
CoD retains SHS's layout bank, but its title/menu fonts and relationship art
|
|
4
|
+
have different numeric IDs. There is no fallback into an SHS library.
|
|
5
|
+
"""
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
|
|
8
|
+
from ..content import ContentError, MAX_PAYLOAD
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
UI_IDS = {528: 292, 529: 293, 530: 294, 531: 295, 532: 296, 533: 297}
|
|
12
|
+
# Host string roles use the existing SHS renderer's indices. These are
|
|
13
|
+
# inspected semantic equivalents in CoD's own resource 13, never SHS text.
|
|
14
|
+
STRING_IDS = {
|
|
15
|
+
9: 8, 10: 9, 11: 10, 21: 20, 23: 22, 25: 24, 26: 25, 128: 120,
|
|
16
|
+
29: 26, 30: 27, 31: 28, 35: 32, 36: 33, 37: 34, 39: 36,
|
|
17
|
+
82: 79, 91: 82, 125: 117, 126: 118, 127: 119, 164: 154,
|
|
18
|
+
238: 213, 252: 227, 258: 232, 260: 234, 302: 80,
|
|
19
|
+
**{role: role - 8 for role in range(118, 123)},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ui_resource(library, role):
|
|
24
|
+
# Shared panel/art IDs retain their native values. Absent assets fail in
|
|
25
|
+
# this library; a coincident SHS role must not acquire unrelated CoD data.
|
|
26
|
+
if role in (290, 446, 496, 499, 502, 540, 541) or 508 <= role < 528 or 700 <= role <= 723:
|
|
27
|
+
raise ContentError(f'Cause of Death has no verified UI resource for role {role}')
|
|
28
|
+
return library.read_asset(UI_IDS.get(role, role))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_assets(read):
|
|
32
|
+
from PIL import Image, ImageFont
|
|
33
|
+
from ..atlas import AtlasFont
|
|
34
|
+
from ..content import ExpArchive
|
|
35
|
+
from ..menu_assets import MenuFont, MenuStrings
|
|
36
|
+
from ..ui_assets import ImagePack, LayoutBank, Rect
|
|
37
|
+
|
|
38
|
+
def check(resource, parser):
|
|
39
|
+
try:
|
|
40
|
+
return parser(read(resource))
|
|
41
|
+
except (ValueError, OSError, IndexError, Image.DecompressionBombError) as error:
|
|
42
|
+
raise ContentError(f'Incompatible or missing CoD asset {resource}: {error}') from error
|
|
43
|
+
|
|
44
|
+
episode = check(12, ExpArchive)
|
|
45
|
+
episode.metadata()
|
|
46
|
+
if 25001 not in episode.programs():
|
|
47
|
+
raise ContentError('CoD asset 12 is missing the bundled episode entry script')
|
|
48
|
+
if len(check(13, MenuStrings.parse).strings) < 257:
|
|
49
|
+
raise ContentError('CoD string bank 13 is missing required UI strings')
|
|
50
|
+
bank = check(14, LayoutBank.parse)
|
|
51
|
+
if len(bank.layouts) < 80:
|
|
52
|
+
raise ContentError('CoD layout bank 14 is missing required UI layouts')
|
|
53
|
+
for index, last in ((7, 9), (8, 9), (17, 87), (22, 18), (46, 46), (65, 4)):
|
|
54
|
+
bank.rectangle(index, last)
|
|
55
|
+
for index in range(len(bank.layouts)):
|
|
56
|
+
tuple(bank.walk(index))
|
|
57
|
+
for resource, count in ((6, 1), (7, 1), (8, 1), (9, 1), (16, 108), (126, 60),
|
|
58
|
+
(204, 14), (220, 14), (236, 14), (252, 14), (268, 1), (272, 16)):
|
|
59
|
+
pack = check(resource, ImagePack.parse)
|
|
60
|
+
mode = 'A' if resource == 268 else 'RGBA'
|
|
61
|
+
if len(pack.images) < count or any(p.mode != mode for p in pack.images):
|
|
62
|
+
raise ContentError(f'CoD image pack {resource} has incompatible frames or pixel mode')
|
|
63
|
+
if resource == 268 and (pack.images[0].width, pack.images[0].height) != (128, 150):
|
|
64
|
+
raise ContentError('CoD portrait mask 268 must be 128 by 150')
|
|
65
|
+
check(291, lambda data: ImageFont.truetype(BytesIO(data), 24))
|
|
66
|
+
def png(data):
|
|
67
|
+
with Image.open(BytesIO(data)) as image:
|
|
68
|
+
if image.format != 'PNG' or image.width * image.height * 4 > MAX_PAYLOAD:
|
|
69
|
+
raise ContentError('Unsupported or oversized CoD UI image')
|
|
70
|
+
image.load()
|
|
71
|
+
return Rect(0, 0, image.width, image.height)
|
|
72
|
+
for resource in (292, 294):
|
|
73
|
+
rect = check(resource, png)
|
|
74
|
+
check(resource + 1, lambda data: AtlasFont.parse(data, rect))
|
|
75
|
+
for resource in (296, 297):
|
|
76
|
+
check(resource, MenuFont.parse)
|
exp_runtime/cod/audio.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Cause of Death's bundled Volume One is itself an EXP at base resource 12."""
|
|
2
|
+
BUILTIN = 'volume-one'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def extract_builtin(package, asset_root, *, ios):
|
|
6
|
+
from ..content import _zip_read
|
|
7
|
+
return _zip_read(package, asset_root + '12'), 'Volume_One.exp', dict(builtin=BUILTIN)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Cause of Death content and host-service profile (iOS 1.3.4 reference)."""
|
|
2
|
+
from .audio import music_cue as MUSIC_CUE
|
|
3
|
+
from .content import BUILTIN, extract_builtin
|
|
4
|
+
from .services import dispatch as DISPATCH
|
|
5
|
+
|
|
6
|
+
STATE_TYPES = {}
|
|
7
|
+
|
|
8
|
+
KEY = 'cod'
|
|
9
|
+
TITLE = 'Cause of Death'
|
|
10
|
+
SOURCES = 'Cause of Death IPA'
|
|
11
|
+
CATALOG_FILENAME = 'cod_options.sav'
|
|
12
|
+
# Native loadOptions 00019098 / saveOptions 000195b4: five flag bytes and
|
|
13
|
+
# two s32 values precede the current entry; EOF follows the rename table.
|
|
14
|
+
CATALOG_LAYOUTS = {17: ((33, 0),)}
|
|
15
|
+
PROFILES = {'cod-ios-assets-v1': 'ipa'}
|
|
16
|
+
BUNDLE_PATTERN = r'com\.ea\.causeofdeath(?:\.[A-Za-z0-9-]+)*'
|
|
17
|
+
NAME_FONT = ('VerdanaBoldItalic', 291)
|
|
18
|
+
# SHSEngine::initFonts 000234da..000237ee; normal/gray/default dialogue
|
|
19
|
+
# uses black body text and dark-red names. Theme 2 retains its rose color.
|
|
20
|
+
DIALOGUE_COLORS = {1: (0, 0, 0), 2: (180, 78, 78), 3: (0, 0, 0), -1: (0, 0, 0)}
|
|
21
|
+
NAME_COLORS = {'PajamaHip24': (41, 104, 221), 'PajamaHip26': (139, 0, 0),
|
|
22
|
+
'PajamaHip266': (180, 78, 78), 'PajamaHipG26': (139, 0, 0),
|
|
23
|
+
'PajamaHipY26': (139, 0, 0), 'PajamaHipS26': (254, 53, 0)}
|
|
24
|
+
MUSIC_RANGE = range(8201, 8210)
|
|
25
|
+
# CoD doSoundManagerTransition (00022b04), splashScreenFlewIn (00014c0c).
|
|
26
|
+
MENU_MUSIC = 8209
|
|
27
|
+
DEFAULT_EPISODE = (101, 1)
|
|
28
|
+
RELATIONSHIP_ASSETS = (3000, 3001, 3002)
|
|
29
|
+
RELATIONSHIP_FLASH = {3000: 3003, 3001: 3005, 3002: 3007}
|
|
30
|
+
RELATIONSHIP_LOSS = {3000: 3004, 3001: 3006, 3002: 3008}
|
|
31
|
+
# SHSEngine::buttonPressed 000249d0 and SHSWidgetMenu::buttonPressed 0003fd60.
|
|
32
|
+
CLICK_SOUND = 8005
|
|
33
|
+
# SHSEngine::buttonPressed pause branch (00024d86).
|
|
34
|
+
PAUSE_SOUND = 8006
|
|
35
|
+
# SHS answer-feedback resource IDs do not establish semantics for CoD assets.
|
|
36
|
+
CHOICE_FEEDBACK_SOUNDS = {}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Cause of Death host-service differences, recovered from its iOS dispatcher."""
|
|
2
|
+
from ..vm import VMError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def dispatch(engine, vm, *, resource_exists=None):
|
|
6
|
+
from ..engine import EngineAction
|
|
7
|
+
request = vm.pending
|
|
8
|
+
service, args = request.yield_id, request.args
|
|
9
|
+
|
|
10
|
+
def complete(name, result=0, **details):
|
|
11
|
+
vm.resume(result)
|
|
12
|
+
return EngineAction(name, request, True, dict(result=result, **details))
|
|
13
|
+
|
|
14
|
+
if service == 15:
|
|
15
|
+
# SHSScript::syscall 0003354c, case 0x0f: named dialogue on panel 3.
|
|
16
|
+
# Only the first two words are read, even in the four-word EXP form.
|
|
17
|
+
if len(args) < 2:
|
|
18
|
+
raise VMError(f'yield 15 at pc {request.pc}: needs at least 2 arguments')
|
|
19
|
+
speaker, text = (engine.read_text(vm, ref) for ref in args[:2])
|
|
20
|
+
return EngineAction('dialogue', request, False,
|
|
21
|
+
engine.present_dialogue(-1, 0, text, 0, speaker=speaker))
|
|
22
|
+
if service == 70:
|
|
23
|
+
# 000335d6 reads stack[SP - argc], including retained backing for a
|
|
24
|
+
# zero-word call. Extra frame words are ignored, then popped normally.
|
|
25
|
+
selector = args[0] if args else vm.read_word(vm.stack_base + vm.sp)
|
|
26
|
+
if selector == 6:
|
|
27
|
+
# 000341fe writes CFBundleVersion to the first dynamic string.
|
|
28
|
+
# Bare EXP tracing has no native bundle; do not invent a version.
|
|
29
|
+
if engine.app_version is None:
|
|
30
|
+
return EngineAction('unhandled_yield', request, False, dict(
|
|
31
|
+
selector=selector, reason='CoD version query requires CFBundleVersion from its IPA'))
|
|
32
|
+
engine.dynamic_strings[0] = engine.app_version
|
|
33
|
+
# 000341bc: selector 10 returns 1 when MTX_IsStoreAvailable is false.
|
|
34
|
+
# The offline desktop has no MTX store. This is its unavailable branch,
|
|
35
|
+
# not a claim that every native iOS device returned a constant.
|
|
36
|
+
result = {2: 2, 3: 6, 6: 0x7ff5, 9: 1, 10: 1}.get(selector, 0)
|
|
37
|
+
return complete('query_build', result, selector=selector)
|
|
38
|
+
if service in (94, 96, 100):
|
|
39
|
+
# 94/96 target 00033778 through the table; 100 reaches it through the
|
|
40
|
+
# unsigned >99 branch at 000335cc, without reading any frame words.
|
|
41
|
+
# This reproduces the supplied CoD 1.3.4 default, including callers
|
|
42
|
+
# with promotional URLs. It does not implement a later URL launcher.
|
|
43
|
+
# The completion tail at 00034b74 narrows the internal sentinel to R=0
|
|
44
|
+
# and pops the frame.
|
|
45
|
+
# No minigame, score write or random draw occurs in this native build.
|
|
46
|
+
return complete('native_noop')
|
|
47
|
+
if service == 99:
|
|
48
|
+
if len(args) != 1:
|
|
49
|
+
raise VMError(f'yield 99 at pc {request.pc}: expected 1 arguments, got {len(args)}')
|
|
50
|
+
# Offline desktop adaptation of the native no-ad completion path.
|
|
51
|
+
# No ad SDK or network request is made; resume the original continuation.
|
|
52
|
+
return complete('offline_ad')
|
|
53
|
+
if service in (9, 91, 95):
|
|
54
|
+
return EngineAction('unhandled_yield', request, False,
|
|
55
|
+
dict(reason='This service is not implemented for Cause of Death'))
|
|
56
|
+
return None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: exp-runtime-cod
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Cause of Death adapter for EXP Runtime
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 Josh Mcdaniel
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: exp-runtime-engine[fonts]==0.5.0
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# EXP Runtime — cod
|
|
34
|
+
|
|
35
|
+
Cause of Death adapter for EXP Runtime. This is a component of the single
|
|
36
|
+
[EXP Runtime project](https://github.com/joshrmcdaniel/exp-runtime), maintained
|
|
37
|
+
and released together with its other wheels. Python imports use the shared
|
|
38
|
+
`exp_runtime` namespace.
|
|
39
|
+
|
|
40
|
+
Install `exp-runtime[tools]` for file tools, `exp-runtime[engine,shs]` or
|
|
41
|
+
`exp-runtime[engine,cod]` for a headless game, or `exp-runtime[player]` for
|
|
42
|
+
the desktop player with both games. Individual component wheels also support
|
|
43
|
+
selective installs. See the project's `docs/SDK.md` for APIs and examples.
|
|
44
|
+
|
|
45
|
+
No original game content is included. Users supply their own game files.
|
|
46
|
+
The inherited MIT license and attribution are included in LICENSE; see
|
|
47
|
+
[provenance](https://github.com/joshrmcdaniel/exp-runtime/blob/main/docs/PROVENANCE.md).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
exp_runtime/cod/__init__.py,sha256=H-v3gHEytdACCAVPnnjcKqqoHyE1ELUdsToUafuDSgw,44
|
|
2
|
+
exp_runtime/cod/assets.py,sha256=80Qyv0vKFa9D_TroOmZx1TLBnjsPSJWF3opo2N20-tc,3651
|
|
3
|
+
exp_runtime/cod/audio.py,sha256=K7hax3eKyV8_YzvGNdgGM6ypbUyC8kpwmsAqaNySz4o,168
|
|
4
|
+
exp_runtime/cod/content.py,sha256=sdobY9K59YGFqlSM4XCvtMGEeh3CAy1afCTMqejZ914,281
|
|
5
|
+
exp_runtime/cod/profile.py,sha256=bTiT2PoB41sQqzbButq5dbLZya7HrZZQR9lXozc9qCg,1688
|
|
6
|
+
exp_runtime/cod/services.py,sha256=_qZljJZma8ZcqIskXOirXQ9kJumRZnUHFS413fsdJhE,3126
|
|
7
|
+
exp_runtime_cod-0.5.0.dist-info/licenses/LICENSE,sha256=y2xxdaMMD9iKqLFR2Lp8sM_0b3qZWxU4_vW0zgufh2k,1070
|
|
8
|
+
exp_runtime_cod-0.5.0.dist-info/METADATA,sha256=ClLy41xsEws6HgcFX-0l__QD-l68aNrVq3Ic8Byq9kE,2326
|
|
9
|
+
exp_runtime_cod-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
10
|
+
exp_runtime_cod-0.5.0.dist-info/top_level.txt,sha256=1uRgkxqLAgYpITLluWh9qs6TpAD83zFoh0E5lbic4RA,12
|
|
11
|
+
exp_runtime_cod-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Josh Mcdaniel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exp_runtime
|