gmdbuilder 0.1.0__tar.gz
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.
- gmdbuilder-0.1.0/LICENSE +21 -0
- gmdbuilder-0.1.0/PKG-INFO +101 -0
- gmdbuilder-0.1.0/README.md +85 -0
- gmdbuilder-0.1.0/pyproject.toml +51 -0
- gmdbuilder-0.1.0/setup.cfg +4 -0
- gmdbuilder-0.1.0/src/gmdbuilder/core.py +158 -0
- gmdbuilder-0.1.0/src/gmdbuilder/fields.py +222 -0
- gmdbuilder-0.1.0/src/gmdbuilder/level.py +413 -0
- gmdbuilder-0.1.0/src/gmdbuilder/mappings/color_id.py +18 -0
- gmdbuilder-0.1.0/src/gmdbuilder/mappings/color_prop.py +19 -0
- gmdbuilder-0.1.0/src/gmdbuilder/mappings/lvl_prop.py +127 -0
- gmdbuilder-0.1.0/src/gmdbuilder/mappings/obj_enum.py +340 -0
- gmdbuilder-0.1.0/src/gmdbuilder/mappings/obj_id.py +223 -0
- gmdbuilder-0.1.0/src/gmdbuilder/mappings/obj_prop.py +1278 -0
- gmdbuilder-0.1.0/src/gmdbuilder/mappings/smart_template.py +15 -0
- gmdbuilder-0.1.0/src/gmdbuilder/object_types.py +1104 -0
- gmdbuilder-0.1.0/src/gmdbuilder/validation.py +115 -0
- gmdbuilder-0.1.0/src/gmdbuilder.egg-info/PKG-INFO +101 -0
- gmdbuilder-0.1.0/src/gmdbuilder.egg-info/SOURCES.txt +20 -0
- gmdbuilder-0.1.0/src/gmdbuilder.egg-info/dependency_links.txt +1 -0
- gmdbuilder-0.1.0/src/gmdbuilder.egg-info/requires.txt +7 -0
- gmdbuilder-0.1.0/src/gmdbuilder.egg-info/top_level.txt +1 -0
gmdbuilder-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xtreme
|
|
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,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gmdbuilder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An unopinionated General-Purpose Geometry Dash framework for safe and easy level editing and scripting
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: gmdkit>=0.4.0
|
|
10
|
+
Requires-Dist: questionary>=2.1.1
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: basedpyright; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+

|
|
19
|
+

|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
# gmdbuilder
|
|
23
|
+
A type-safe general-purpose Python framework for pragmatic Geometry Dash level editing and scripting.
|
|
24
|
+
|
|
25
|
+
gmdbuilder lets you:
|
|
26
|
+
- Read & write Geometry Dash levels
|
|
27
|
+
- Automatically scan and protect against bugs (property types/ranges, spawn limit, etc.)
|
|
28
|
+
- Work directly with triggers, groups, and objects - and choose your own abstractions
|
|
29
|
+
- Use pre-built systems and templates to accelerate development
|
|
30
|
+
|
|
31
|
+
**gmdbuilder** is developed in collaboration with HDanke, the creator of **gmdkit** (a dependency of this framework) and his unofficial **GD Editor Docs**.
|
|
32
|
+
|
|
33
|
+
*(No overengineered language was made in the making of this project)*
|
|
34
|
+
|
|
35
|
+
## Why Python?
|
|
36
|
+
|
|
37
|
+
Python fits surprisingly well as a language for GD scripting:
|
|
38
|
+
- Exceptionally good at building/verifying dictionaries (which all GD objects are)
|
|
39
|
+
- Operator overloading for counters and other special logic
|
|
40
|
+
- Any programming paradigm that you want is well supported
|
|
41
|
+
- Reliable type system with good debugger/type-checker tooling
|
|
42
|
+
- Huge package ecosystem
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
Install the latest release from PyPI (i didnt set this up yet):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install gmdkit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Install the latest development version from GitHub:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install git+https://github.com/UHDanke/gmdkit.git
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Getting Started
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from gmdbuilder import level
|
|
61
|
+
|
|
62
|
+
# This group gets deleted at level-load and automatically added to new objects at level-export
|
|
63
|
+
level.tag_group = 9999 # Set to 9999 by default
|
|
64
|
+
|
|
65
|
+
# From .gmd file, supports full object editing/deleting
|
|
66
|
+
level.from_file("example.gmd")
|
|
67
|
+
|
|
68
|
+
# From WSLiveEditor, only supports adidng objects
|
|
69
|
+
level.from_live_editor()
|
|
70
|
+
|
|
71
|
+
obj_list = level.objects # mutations are validated
|
|
72
|
+
|
|
73
|
+
# Object properties are in the form { "a<key number>": value }
|
|
74
|
+
repr(obj_list[1])
|
|
75
|
+
|
|
76
|
+
# Object ID and Property enums (all values are Literal)
|
|
77
|
+
from gmdbuilder.mappings.obj_prop import ObjProp
|
|
78
|
+
from gmdbuilder.mappings.obj_id import ObjId
|
|
79
|
+
|
|
80
|
+
for obj in obj_list:
|
|
81
|
+
if obj[ObjProp.ID] == ObjID.Trigger.MOVE:
|
|
82
|
+
obj[ObjProp.GROUPS] = {}
|
|
83
|
+
elif obj[ObjProp.ID] == ObjID.Trigger.COUNT:
|
|
84
|
+
obj_list.remove(obj)
|
|
85
|
+
|
|
86
|
+
# Translates to { a1: 1 }
|
|
87
|
+
block = from_raw_object({1: 1})
|
|
88
|
+
|
|
89
|
+
obj_list.delete_where(block)
|
|
90
|
+
obj_list.delete_where(lambda obj: obj[ObjProp.ID] == 1)
|
|
91
|
+
|
|
92
|
+
# Translates to { a1: 1611, a2: 50, a3: 45 }
|
|
93
|
+
object = from_object_string("1,1611,2,50,3,45;", obj_type=CountType)
|
|
94
|
+
object[ObjProp.Trigger.Count.ACTIVATE_GROUP] = True
|
|
95
|
+
|
|
96
|
+
# Export object edits, deletions and additions
|
|
97
|
+
level.export_to_file(file_path="example_updated.gmd")
|
|
98
|
+
|
|
99
|
+
# Export added objects to WSLiveEditor
|
|
100
|
+
level.export_to_live_editor()
|
|
101
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
# gmdbuilder
|
|
7
|
+
A type-safe general-purpose Python framework for pragmatic Geometry Dash level editing and scripting.
|
|
8
|
+
|
|
9
|
+
gmdbuilder lets you:
|
|
10
|
+
- Read & write Geometry Dash levels
|
|
11
|
+
- Automatically scan and protect against bugs (property types/ranges, spawn limit, etc.)
|
|
12
|
+
- Work directly with triggers, groups, and objects - and choose your own abstractions
|
|
13
|
+
- Use pre-built systems and templates to accelerate development
|
|
14
|
+
|
|
15
|
+
**gmdbuilder** is developed in collaboration with HDanke, the creator of **gmdkit** (a dependency of this framework) and his unofficial **GD Editor Docs**.
|
|
16
|
+
|
|
17
|
+
*(No overengineered language was made in the making of this project)*
|
|
18
|
+
|
|
19
|
+
## Why Python?
|
|
20
|
+
|
|
21
|
+
Python fits surprisingly well as a language for GD scripting:
|
|
22
|
+
- Exceptionally good at building/verifying dictionaries (which all GD objects are)
|
|
23
|
+
- Operator overloading for counters and other special logic
|
|
24
|
+
- Any programming paradigm that you want is well supported
|
|
25
|
+
- Reliable type system with good debugger/type-checker tooling
|
|
26
|
+
- Huge package ecosystem
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
Install the latest release from PyPI (i didnt set this up yet):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install gmdkit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Install the latest development version from GitHub:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install git+https://github.com/UHDanke/gmdkit.git
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Getting Started
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from gmdbuilder import level
|
|
45
|
+
|
|
46
|
+
# This group gets deleted at level-load and automatically added to new objects at level-export
|
|
47
|
+
level.tag_group = 9999 # Set to 9999 by default
|
|
48
|
+
|
|
49
|
+
# From .gmd file, supports full object editing/deleting
|
|
50
|
+
level.from_file("example.gmd")
|
|
51
|
+
|
|
52
|
+
# From WSLiveEditor, only supports adidng objects
|
|
53
|
+
level.from_live_editor()
|
|
54
|
+
|
|
55
|
+
obj_list = level.objects # mutations are validated
|
|
56
|
+
|
|
57
|
+
# Object properties are in the form { "a<key number>": value }
|
|
58
|
+
repr(obj_list[1])
|
|
59
|
+
|
|
60
|
+
# Object ID and Property enums (all values are Literal)
|
|
61
|
+
from gmdbuilder.mappings.obj_prop import ObjProp
|
|
62
|
+
from gmdbuilder.mappings.obj_id import ObjId
|
|
63
|
+
|
|
64
|
+
for obj in obj_list:
|
|
65
|
+
if obj[ObjProp.ID] == ObjID.Trigger.MOVE:
|
|
66
|
+
obj[ObjProp.GROUPS] = {}
|
|
67
|
+
elif obj[ObjProp.ID] == ObjID.Trigger.COUNT:
|
|
68
|
+
obj_list.remove(obj)
|
|
69
|
+
|
|
70
|
+
# Translates to { a1: 1 }
|
|
71
|
+
block = from_raw_object({1: 1})
|
|
72
|
+
|
|
73
|
+
obj_list.delete_where(block)
|
|
74
|
+
obj_list.delete_where(lambda obj: obj[ObjProp.ID] == 1)
|
|
75
|
+
|
|
76
|
+
# Translates to { a1: 1611, a2: 50, a3: 45 }
|
|
77
|
+
object = from_object_string("1,1611,2,50,3,45;", obj_type=CountType)
|
|
78
|
+
object[ObjProp.Trigger.Count.ACTIVATE_GROUP] = True
|
|
79
|
+
|
|
80
|
+
# Export object edits, deletions and additions
|
|
81
|
+
level.export_to_file(file_path="example_updated.gmd")
|
|
82
|
+
|
|
83
|
+
# Export added objects to WSLiveEditor
|
|
84
|
+
level.export_to_live_editor()
|
|
85
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gmdbuilder"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An unopinionated General-Purpose Geometry Dash framework for safe and easy level editing and scripting"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"gmdkit>=0.4.0",
|
|
14
|
+
"questionary>=2.1.1"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# Optional dependencies for development
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"basedpyright",
|
|
21
|
+
"ruff",
|
|
22
|
+
"pytest>=7.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.basedpyright]
|
|
29
|
+
typeCheckingMode = "strict"
|
|
30
|
+
reportAny = "none"
|
|
31
|
+
reportExplicitAny = "none"
|
|
32
|
+
reportMissingTypeStubs = "none"
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
ignore = [
|
|
36
|
+
"E701", # Multiple statements on one line (colon)
|
|
37
|
+
"E702", # Multiple statements on one line (semicolon)
|
|
38
|
+
"E731", # Do not assign a lambda expression, use a def
|
|
39
|
+
"E741", # Ambiguous variable name (allows single letter vars like l, o, I)
|
|
40
|
+
# "E402", # Import not at top line
|
|
41
|
+
# "F401", # Imported but unused (useful for __init__.py files)
|
|
42
|
+
# "F403", # Star imports (from module import *)
|
|
43
|
+
# "B008", # Do not perform function call in argument defaults
|
|
44
|
+
# "B904", # Use raise from to specify exception cause
|
|
45
|
+
# "N802", # Function name should be lowercase
|
|
46
|
+
# "N803", # Argument name should be lowercase
|
|
47
|
+
# "N806", # Variable in function should be lowercase
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
addopts = "-qv"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Core utilities for working with ObjectType dicts."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Any, Literal, TypeVar, overload
|
|
5
|
+
from gmdkit.models.object import Object as KitObject
|
|
6
|
+
from gmdkit.models.prop.list import IDList, RemapList
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from gmdbuilder.mappings import obj_prop
|
|
10
|
+
from gmdbuilder.validation import validate
|
|
11
|
+
import gmdbuilder.object_types as td
|
|
12
|
+
|
|
13
|
+
ObjectType = td.ObjectType
|
|
14
|
+
|
|
15
|
+
T = TypeVar('T', bound=ObjectType)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Object(dict[str, Any]):
|
|
19
|
+
"""
|
|
20
|
+
Note: Not for users to call directly
|
|
21
|
+
|
|
22
|
+
The actual dict implementation hidden behind the ObjectType TypedDict
|
|
23
|
+
|
|
24
|
+
This is to intercept & validate mutations of objects and add new helpers.
|
|
25
|
+
"""
|
|
26
|
+
__slots__ = ("_obj_id",)
|
|
27
|
+
|
|
28
|
+
def __init__(self, obj_id: int):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._obj_id = int(obj_id)
|
|
31
|
+
super().__setitem__("a1", self._obj_id)
|
|
32
|
+
|
|
33
|
+
def __setitem__(self, k: str, v: Any):
|
|
34
|
+
validate(self._obj_id, k, v)
|
|
35
|
+
if k == obj_prop.ID:
|
|
36
|
+
self._obj_id = int(v)
|
|
37
|
+
super().__setitem__(k, v)
|
|
38
|
+
|
|
39
|
+
def update(self, *args: Any, **kwargs: Any):
|
|
40
|
+
# Construct items dict from args and kwargs
|
|
41
|
+
items: dict[str, Any]
|
|
42
|
+
if args:
|
|
43
|
+
if len(args) != 1:
|
|
44
|
+
raise TypeError(f"update() takes at most 1 positional argument ({len(args)} given)")
|
|
45
|
+
__m = args[0]
|
|
46
|
+
items = dict(__m)
|
|
47
|
+
items.update(kwargs)
|
|
48
|
+
else:
|
|
49
|
+
items = dict(kwargs)
|
|
50
|
+
|
|
51
|
+
for k, v in items.items():
|
|
52
|
+
validate(self._obj_id, k, v)
|
|
53
|
+
if obj_prop.ID in items:
|
|
54
|
+
self._obj_id = int(items[obj_prop.ID])
|
|
55
|
+
super().update(items)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@lru_cache(maxsize=1024)
|
|
60
|
+
def _to_raw_key_cached(key: str) -> int | str:
|
|
61
|
+
if key.startswith('k'):
|
|
62
|
+
return key
|
|
63
|
+
if key.startswith('a'):
|
|
64
|
+
tail = key[1:]
|
|
65
|
+
if tail.isdigit():
|
|
66
|
+
return int(tail)
|
|
67
|
+
raise ValueError()
|
|
68
|
+
|
|
69
|
+
def to_kit_object(obj: ObjectType) -> KitObject:
|
|
70
|
+
"""
|
|
71
|
+
Convert ObjectType to a new gmdkit int-keyed dict for gmdkit or debugging.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
{'a1': 900, 'a2': 50, 'a57': {2}} → {1: 900, 2: 50, 57: IDList([2])}
|
|
75
|
+
"""
|
|
76
|
+
raw: dict[int|str, Any] = {}
|
|
77
|
+
for k, v in obj.items():
|
|
78
|
+
match k:
|
|
79
|
+
case obj_prop.GROUPS:
|
|
80
|
+
raw[_to_raw_key_cached(k)] = IDList(v)
|
|
81
|
+
case obj_prop.PARENT_GROUPS:
|
|
82
|
+
raw[_to_raw_key_cached(k)] = IDList(v)
|
|
83
|
+
case obj_prop.Trigger.Spawn.REMAPS:
|
|
84
|
+
raw[_to_raw_key_cached(k)] = RemapList.from_dict(v) # type: ignore
|
|
85
|
+
case _:
|
|
86
|
+
try:
|
|
87
|
+
raw[_to_raw_key_cached(k)] = v
|
|
88
|
+
except ValueError as e:
|
|
89
|
+
raise ValueError(f"Object has bad/unsupported key {k!r}:\n{obj=}") from e
|
|
90
|
+
return KitObject(raw)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@lru_cache(maxsize=1024)
|
|
94
|
+
def _from_raw_key_cached(key: object) -> str:
|
|
95
|
+
if isinstance(key, int):
|
|
96
|
+
return f"a{key}"
|
|
97
|
+
if isinstance(key, str) and (key.startswith("a") or key.startswith("k")):
|
|
98
|
+
return key
|
|
99
|
+
raise ValueError()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def from_kit_object(obj: dict[int|str, Any]) -> ObjectType:
|
|
103
|
+
"""
|
|
104
|
+
Convert gmdkit object dict to object typeddict.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
{1: 900, 2: 50, 57: IDList([2])} → {a1: 900, a2: 50, a57: {2}}
|
|
108
|
+
"""
|
|
109
|
+
new = {}
|
|
110
|
+
for k, v in obj.items():
|
|
111
|
+
match k:
|
|
112
|
+
case 57:
|
|
113
|
+
new[obj_prop.GROUPS] = set(v) if v else set()
|
|
114
|
+
case 274:
|
|
115
|
+
new[obj_prop.PARENT_GROUPS] = set(v) if v else set()
|
|
116
|
+
case 442:
|
|
117
|
+
new[obj_prop.Trigger.Spawn.REMAPS] = v.to_dict()
|
|
118
|
+
case 52:
|
|
119
|
+
new[obj_prop.Trigger.Pulse.TARGET_TYPE] = bool(v)
|
|
120
|
+
case _:
|
|
121
|
+
try:
|
|
122
|
+
new[_from_raw_key_cached(k)] = v
|
|
123
|
+
except ValueError as e:
|
|
124
|
+
raise ValueError(f"Object has bad/unsupported key {k!r}: \n{obj=}") from e
|
|
125
|
+
return new # type: ignore
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@overload
|
|
129
|
+
def from_object_string(obj_string: str) -> ObjectType: ...
|
|
130
|
+
@overload
|
|
131
|
+
def from_object_string(obj_string: str, *, obj_type: type[T]) -> T: ...
|
|
132
|
+
def from_object_string(obj_string: str, *, obj_type: type[ObjectType] | None = None) -> ObjectType:
|
|
133
|
+
"""
|
|
134
|
+
Convert GD level object string to ObjectType.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
"1,1,2,50,3,45;" → {'a1': 1, 'a2': 50, 'a3': 45}
|
|
138
|
+
"""
|
|
139
|
+
return from_kit_object(KitObject.from_string(obj_string)) # type: ignore
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@overload
|
|
143
|
+
def new_object(object_id: Literal[3016]) -> td.AdvFollowType: ...
|
|
144
|
+
@overload
|
|
145
|
+
def new_object(object_id: Literal[1346]) -> td.RotateType: ...
|
|
146
|
+
@overload
|
|
147
|
+
def new_object(object_id: Literal[901]) -> td.MoveType: ...
|
|
148
|
+
@overload
|
|
149
|
+
def new_object(object_id: int) -> ObjectType: ...
|
|
150
|
+
def new_object(object_id: int) -> ObjectType:
|
|
151
|
+
"""
|
|
152
|
+
Create a new Object with defaults from gmdkit.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
ObjectType dict with default properties (using 'a<num>' keys)
|
|
156
|
+
"""
|
|
157
|
+
# Convert from gmdkit's {1: val, 2: val} to our {'a1': val, 'a2': val}
|
|
158
|
+
return from_kit_object(KitObject.default(object_id)) # type: ignore
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any, Callable, Literal, Union, get_args, get_origin, Required
|
|
3
|
+
from gmdbuilder.mappings import obj_id, obj_prop
|
|
4
|
+
from gmdbuilder import object_types as td
|
|
5
|
+
|
|
6
|
+
ObjectType = td.ObjectType
|
|
7
|
+
tid = obj_id.Trigger
|
|
8
|
+
cid = obj_id.Collectible
|
|
9
|
+
ID_TO_TYPEDDICT: dict[int, type[ObjectType]] = {
|
|
10
|
+
tid.ALPHA: td.AlphaType,
|
|
11
|
+
tid.ADV_FOLLOW: td.AdvFollowType,
|
|
12
|
+
tid.ADV_RANDOM: td.AdvRandomType,
|
|
13
|
+
tid.ANIMATE: td.AnimateType,
|
|
14
|
+
tid.ANIMATE_KEYFRAME: td.AnimateKeyframeType,
|
|
15
|
+
tid.ARROW: td.ArrowType,
|
|
16
|
+
tid.BG_EFFECT_ENABLE: td.TriggerType,
|
|
17
|
+
tid.BG_EFFECT_DISABLE: td.TriggerType,
|
|
18
|
+
tid.BPM: td.BpmType,
|
|
19
|
+
tid.CAMERA_EDGE: td.CameraEdgeType,
|
|
20
|
+
tid.CAMERA_GUIDE: td.CameraGuideType,
|
|
21
|
+
tid.CAMERA_MODE: td.CameraModeType,
|
|
22
|
+
tid.CHANGE_BG: td.ChangeBgType,
|
|
23
|
+
tid.CHANGE_GR: td.ChangeGrType,
|
|
24
|
+
tid.CHANGE_MG: td.ChangeMgType,
|
|
25
|
+
tid.CHECKPOINT: td.CheckpointType,
|
|
26
|
+
tid.COUNT: td.CountType,
|
|
27
|
+
tid.COLOR: td.ColorType,
|
|
28
|
+
tid.COLLISION: td.CollisionType,
|
|
29
|
+
tid.COLLISION_BLOCK: td.CollisionBlockType,
|
|
30
|
+
tid.EDIT_ADV_FOLLOW: td.EditAdvFollowType,
|
|
31
|
+
tid.EDIT_MG: td.MgEditType,
|
|
32
|
+
tid.EVENT: td.EventType,
|
|
33
|
+
tid.EDIT_SFX: td.SfxType,
|
|
34
|
+
tid.EDIT_SONG: td.SongType,
|
|
35
|
+
tid.END: td.EndType,
|
|
36
|
+
tid.FOLLOW: td.FollowType,
|
|
37
|
+
tid.FOLLOW_PLAYER_Y: td.FollowPlayerYType,
|
|
38
|
+
tid.FORCE_BLOCK: td.ForceBlockType,
|
|
39
|
+
tid.GAMEPLAY_OFFSET: td.GameplayOffsetType,
|
|
40
|
+
tid.GRADIENT: td.GradientType,
|
|
41
|
+
tid.GRAVITY: td.GravityType,
|
|
42
|
+
tid.INSTANT_COLLISION: td.InstantCollisionType,
|
|
43
|
+
tid.INSTANT_COUNT: td.InstantCountType,
|
|
44
|
+
tid.ITEM_COMPARE: td.ItemCompareType,
|
|
45
|
+
tid.ITEM_EDIT: td.ItemEditType,
|
|
46
|
+
tid.ITEM_PERSIST: td.ItemPersistType,
|
|
47
|
+
tid.KEYFRAME: td.KeyframeType,
|
|
48
|
+
tid.LINK_VISIBLE: td.LinkVisibleType,
|
|
49
|
+
tid.MG_SPEED: td.MgSpeedType,
|
|
50
|
+
tid.MOVE: td.MoveType,
|
|
51
|
+
tid.ON_DEATH: td.OnDeathType,
|
|
52
|
+
tid.OPTIONS: td.OptionsType,
|
|
53
|
+
tid.OFFSET_CAMERA: td.OffsetCameraType,
|
|
54
|
+
tid.PICKUP: td.PickupType,
|
|
55
|
+
tid.PLAYER_CONTROL: td.PlayerControlType,
|
|
56
|
+
tid.PULSE: td.PulseType,
|
|
57
|
+
tid.RANDOM: td.RandomType,
|
|
58
|
+
tid.RESET: td.ResetType,
|
|
59
|
+
tid.ROTATE: td.RotateType,
|
|
60
|
+
tid.ROTATE_CAMERA: td.RotateCameraType,
|
|
61
|
+
tid.SCALE: td.ScaleType,
|
|
62
|
+
tid.SEQUENCE: td.SequenceType,
|
|
63
|
+
tid.SFX: td.SfxType,
|
|
64
|
+
tid.STOP: td.StopType,
|
|
65
|
+
tid.SHAKE: td.ShakeType,
|
|
66
|
+
tid.SONG: td.SongType,
|
|
67
|
+
tid.SPAWN: td.SpawnType,
|
|
68
|
+
tid.SPAWN_PARTICLE: td.SpawnParticleType,
|
|
69
|
+
tid.STATE_BLOCK: td.StateBlockType,
|
|
70
|
+
tid.STATIC_CAMERA: td.StaticCameraType,
|
|
71
|
+
tid.TELEPORT: td.TeleportType,
|
|
72
|
+
tid.TIME: td.TimeType,
|
|
73
|
+
tid.TIMEWARP: td.TimewarpType,
|
|
74
|
+
tid.TIME_CONTROL: td.TimeControlType,
|
|
75
|
+
tid.TIME_EVENT: td.TimeEventType,
|
|
76
|
+
tid.TOGGLE: td.ToggleType,
|
|
77
|
+
tid.TOGGLE_BLOCK: td.ToggleBlockType,
|
|
78
|
+
tid.TOUCH: td.TouchType,
|
|
79
|
+
tid.UI: td.UiType,
|
|
80
|
+
tid.ZOOM_CAMERA: td.ZoomCameraType,
|
|
81
|
+
tid.PLAYER_HIDE: td.TriggerType,
|
|
82
|
+
tid.PLAYER_SHOW: td.TriggerType,
|
|
83
|
+
tid.Enter.MOVE: td.EffectType,
|
|
84
|
+
tid.EnterPreset.FADE_ONLY: td.TriggerType,
|
|
85
|
+
cid.KEY: td.CollectibleType,
|
|
86
|
+
cid.USER_COIN: td.AnimatedType,
|
|
87
|
+
cid.SMALL_COIN: td.CollectibleType,
|
|
88
|
+
obj_id.TEXT: td.TextType,
|
|
89
|
+
obj_id.ITEM_LABEL: td.ItemLabelType,
|
|
90
|
+
obj_id.PARTICLE_OBJECT: td.ParticleType,
|
|
91
|
+
obj_id.Orb.BLACK: td.TriggerType,
|
|
92
|
+
obj_id.Orb.BLUE: td.TriggerType,
|
|
93
|
+
obj_id.Orb.GREEN: td.TriggerType,
|
|
94
|
+
obj_id.Orb.RED: td.TriggerType,
|
|
95
|
+
obj_id.Orb.YELLOW: td.TriggerType,
|
|
96
|
+
obj_id.Orb.PINK: td.TriggerType,
|
|
97
|
+
obj_id.Orb.SPIDER: td.TriggerType,
|
|
98
|
+
obj_id.Orb.TELEPORT: td.TriggerType,
|
|
99
|
+
obj_id.Orb.TOGGLE: td.ToggleBlockType,
|
|
100
|
+
obj_id.Orb.DASH_GREEN: td.DashType,
|
|
101
|
+
obj_id.Orb.DASH_PINK: td.DashType,
|
|
102
|
+
obj_id.Portal.Teleport.ENTER: td.PortalType,
|
|
103
|
+
obj_id.Portal.Teleport.EXIT: td.ExitPortalType,
|
|
104
|
+
obj_id.Portal.Teleport.LINKED: td.PortalType,
|
|
105
|
+
3002: td.AnimatedType,
|
|
106
|
+
1020: td.SawType,
|
|
107
|
+
1582: td.SawType,
|
|
108
|
+
1709: td.SawType,
|
|
109
|
+
}
|
|
110
|
+
"""Unfinished mapping of Object IDs to non-common Object TypedDicts"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _append_types(cls: object, obj_type: type[ObjectType]):
|
|
114
|
+
if isinstance(cls, list):
|
|
115
|
+
for c in cls:
|
|
116
|
+
if isinstance(c, int):
|
|
117
|
+
ID_TO_TYPEDDICT[c] = obj_type
|
|
118
|
+
else:
|
|
119
|
+
for obj in [v for v in vars(cls).values() if isinstance(v, int)]:
|
|
120
|
+
ID_TO_TYPEDDICT[obj] = obj_type
|
|
121
|
+
|
|
122
|
+
_append_types(obj_id.Pad, td.TriggerType)
|
|
123
|
+
_append_types(obj_id.Portal, td.GamemodePortalType)
|
|
124
|
+
_append_types(obj_id.Speed, td.TriggerType)
|
|
125
|
+
_append_types(obj_id.Modifier, td.TriggerType)
|
|
126
|
+
_append_types(obj_id.Trigger.Shader, td.ShaderType)
|
|
127
|
+
_append_types(obj_id.Trigger.Area, td.EffectType)
|
|
128
|
+
_append_types([i for i in range(920, 925)], td.AnimatedType)
|
|
129
|
+
_append_types([i for i in range(1849, 1859)], td.AnimatedType)
|
|
130
|
+
_append_types([1936, 1937, 1938, 1939], td.AnimatedType)
|
|
131
|
+
_append_types([i for i in range(2020, 2056)], td.AnimatedType)
|
|
132
|
+
_append_types([2864, 2865], td.AnimatedType)
|
|
133
|
+
_append_types([i for i in range(2867, 2895)], td.AnimatedType)
|
|
134
|
+
_append_types([3000, 3001, 3002], td.AnimatedType)
|
|
135
|
+
_append_types([85, 86, 87, 88, 89], td.SawType)
|
|
136
|
+
_append_types([97, 98], td.SawType)
|
|
137
|
+
_append_types([137, 138, 139], td.SawType)
|
|
138
|
+
_append_types([154, 155, 156], td.SawType)
|
|
139
|
+
_append_types([i for i in range(180, 189)], td.SawType)
|
|
140
|
+
_append_types([222, 223, 224], td.SawType)
|
|
141
|
+
_append_types([375, 376, 377, 378], td.SawType)
|
|
142
|
+
_append_types([i for i in range(394, 400)], td.SawType)
|
|
143
|
+
|
|
144
|
+
ID_TO_ALLOWED_KEYS = {
|
|
145
|
+
k: set(v.__required_keys__) | set(v.__optional_keys__)
|
|
146
|
+
for k, v in ID_TO_TYPEDDICT.items()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
COMMON_ALLOWED_KEYS = td.ObjectType.__required_keys__ | td.ObjectType.__optional_keys__
|
|
150
|
+
|
|
151
|
+
UNHASHABLE_VALUE_KEYS = {
|
|
152
|
+
obj_prop.GROUPS,
|
|
153
|
+
obj_prop.PARENT_GROUPS,
|
|
154
|
+
obj_prop.Trigger.Spawn.REMAPS,
|
|
155
|
+
obj_prop.Trigger.AdvRandom.TARGETS,
|
|
156
|
+
obj_prop.Trigger.Event.EVENTS,
|
|
157
|
+
obj_prop.Trigger.Sequence.SEQUENCE
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
hashable_value_key_to_isinstance: dict[str, Callable[[Any], bool]] = {}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ACTUAL FUNCTION WE CARE ABOUT
|
|
165
|
+
def value_is_correct_type(key: str, v: Any) -> bool:
|
|
166
|
+
"""Check if value v is of the correct type for key."""
|
|
167
|
+
if key in UNHASHABLE_VALUE_KEYS:
|
|
168
|
+
raise ValueError(f"Key {key} has unhashable value, cannot be type checked.")
|
|
169
|
+
if key not in hashable_value_key_to_isinstance:
|
|
170
|
+
raise ValueError(f"No type information available for key {key!r} : {v!r}")
|
|
171
|
+
return hashable_value_key_to_isinstance[key](v)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _type_to_isinstance(typ: Any) -> Callable[[Any], bool]:
|
|
175
|
+
"""Convert a type annotation to a runtime check function."""
|
|
176
|
+
origin = get_origin(typ)
|
|
177
|
+
|
|
178
|
+
if origin is Required:
|
|
179
|
+
inner_type = get_args(typ)[0]
|
|
180
|
+
return _type_to_isinstance(inner_type)
|
|
181
|
+
if origin is Union:
|
|
182
|
+
checks = [_type_to_isinstance(t) for t in get_args(typ)]
|
|
183
|
+
return lambda v: any(check(v) for check in checks)
|
|
184
|
+
elif origin is Literal:
|
|
185
|
+
allowed = get_args(typ)
|
|
186
|
+
return lambda v: v in allowed
|
|
187
|
+
elif typ is Any:
|
|
188
|
+
return lambda v: True
|
|
189
|
+
elif isinstance(typ, type):
|
|
190
|
+
if typ is bool:
|
|
191
|
+
return lambda v: isinstance(v, bool)
|
|
192
|
+
elif typ is float:
|
|
193
|
+
return lambda v: isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
194
|
+
elif typ is int:
|
|
195
|
+
return lambda v: isinstance(v, int) and not isinstance(v, bool)
|
|
196
|
+
else:
|
|
197
|
+
return lambda v: isinstance(v, typ)
|
|
198
|
+
|
|
199
|
+
raise ValueError(f"Unsupported type annotation: {typ!r}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _typeddict_to_isinstance(typeddict: type):
|
|
203
|
+
"""Extract type annotations from a TypedDict and populate runtime checks."""
|
|
204
|
+
annotations: dict[str, Any] = {}
|
|
205
|
+
|
|
206
|
+
for base in reversed(typeddict.__mro__):
|
|
207
|
+
if hasattr(base, '__annotations__'):
|
|
208
|
+
annotations.update(base.__annotations__)
|
|
209
|
+
|
|
210
|
+
for key, typ in annotations.items():
|
|
211
|
+
if key in hashable_value_key_to_isinstance: continue
|
|
212
|
+
if key in UNHASHABLE_VALUE_KEYS: continue
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
hashable_value_key_to_isinstance[key] = _type_to_isinstance(typ)
|
|
216
|
+
except ValueError as e:
|
|
217
|
+
print(f"Warning: Skipping key {key!r} with type {typ}: {e}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
_typeddict_to_isinstance(td.ObjectType)
|
|
221
|
+
for tyd in ID_TO_TYPEDDICT.values():
|
|
222
|
+
_typeddict_to_isinstance(tyd)
|