pz-scripts-data 1.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.
@@ -0,0 +1,4 @@
1
+ PZ Scripts Data License
2
+ This dataset is provided as fully open-source. You are free to use, modify, repost this dataset and any tools in this projects. Any contribution to it is welcomed.
3
+
4
+ We are not responsible for what is made with this dataset, whenever it gets updated and breaks anything.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: pz-scripts-data
3
+ Version: 1.1.0
4
+ Summary: Dataset for the Scripts of Project Zomboid.
5
+ Author: SimKDT
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Topic :: Database
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.12.3
14
+ License-File: LICENSE
15
+ Requires-Dist: pyyaml
16
+ Requires-Dist: jsonschema
17
+ Requires-Dist: pydantic>=2.11
18
+ Dynamic: license-file
@@ -0,0 +1,41 @@
1
+ # PZ Scripts Data
2
+ Provides information regarding various script blocks and parameters of Project Zomboid [scripts](https://pzwiki.net/wiki/Scripts) system. This dataset is used to provide a [ScriptsDocs](https://pz-wiki-modding.github.io/PZ-API-Docs/).
3
+
4
+ The translation data is now moved to [pz-translation-data](https://github.com/SirDoggyJvla/pz-translation-data) and are now deprecated. The main data files holding the last updated translation data are kept for tools that may use them, but will be removed in the future.
5
+
6
+ ## Installation
7
+
8
+ PZ Scripts Data can be installed via PyPI, Git, or in development mode as described below.
9
+
10
+ <details open>
11
+ <summary>PyPI</summary>
12
+
13
+ ```bash
14
+ pip install pz-scripts-data
15
+ ```
16
+ </details>
17
+
18
+ <details>
19
+ <summary>Git</summary>
20
+
21
+ ```bash
22
+ pip install git+https://github.com/PZ-Wiki-Modding/pz-scripts-data.git
23
+ ```
24
+ </details>
25
+
26
+ <details>
27
+ <summary>Dev</summary>
28
+
29
+ ```bash
30
+ pip install -e .
31
+ ```
32
+ </details>
33
+
34
+ ## Schema
35
+ You can find a Pydantic model defining the data format in the [schema.py](src/pz_scripts_data/schema.py) file. A JSON schema file exported from this model is available in the [schemas/blocks.json](schemas/blocks.json) file.
36
+
37
+ ## Contributing
38
+ This dataset is aimed to accept any contributions. See [CONTRIBUTING](CONTRIBUTING.md) for more information on how to contribute to this dataset.
39
+
40
+ ## License
41
+ See [LICENSE](LICENSE) for more information.
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "pz-scripts-data"
3
+ description = "Dataset for the Scripts of Project Zomboid."
4
+ requires-python = ">=3.12.3"
5
+ version = "1.1.0"
6
+ license-files = ["LICENSE"]
7
+ authors = [
8
+ { name = "SimKDT" },
9
+ ]
10
+ dependencies = [
11
+ "pyyaml",
12
+ "jsonschema",
13
+ "pydantic>=2.11",
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3.12",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Topic :: Database",
19
+ "Operating System :: OS Independent",
20
+ "Development Status :: 5 - Production/Stable",
21
+ "Intended Audience :: Developers",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ [project.scripts]
26
+ release = "pz_scripts_data.release:main"
27
+
28
+ [tool.uv]
29
+ package = true
30
+
31
+ [tool.setuptools.package-data]
32
+ pz_scripts_data = [
33
+ "py.typed"
34
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,65 @@
1
+ from enum import StrEnum
2
+
3
+ class BlockProperties(StrEnum):
4
+ VERSION = 'version'
5
+ REF_DESC = '#desc'
6
+ REF_FULL = '#ref'
7
+ DEPRECATED = 'deprecated'
8
+
9
+ NAME = 'name'
10
+ ID = 'ID'
11
+ SOFT_OVERRIDE = 'softOverride'
12
+ NO_COMMA = 'noComma'
13
+ PATTERN = 'pattern'
14
+
15
+ DESCRIPTION = 'description'
16
+ SHORT_DESCRIPTION = 'short_description'
17
+
18
+ VARIANT = 'variantOf'
19
+ VARIANTS = 'variants'
20
+
21
+ PARENTS = 'parents'
22
+ CHILDREN = 'children'
23
+ NEEDS_CHILDREN = 'needsChildren'
24
+ PARAMETERS = 'parameters'
25
+ PROPERTIES = 'properties'
26
+
27
+ IS_ROOT = 'isRoot'
28
+
29
+ class IDProperties(StrEnum):
30
+ OPTIONAL = 'optional'
31
+ CAN_HAVE_SPACES = 'canHaveSpace'
32
+ FORBIDDEN = 'forbidden'
33
+
34
+ VALUES = 'values'
35
+ PARENTS_WITHOUT = 'parentsWithout'
36
+ AS_TYPE = 'asType'
37
+
38
+ TRANSLATION = 'translation'
39
+
40
+ class ParameterProperties(StrEnum):
41
+ REF_DESC = '#desc'
42
+ REF_FULL = '#ref'
43
+
44
+ NAME = 'name'
45
+ DESCRIPTION = 'description'
46
+
47
+ TYPE = 'type'
48
+ DEFAULT = 'default'
49
+ REQUIRED = 'required'
50
+ CAN_BE_EMPTY = 'canBeEmpty'
51
+ MINIMUM = 'minimum'
52
+ MAXIMUM = 'maximum'
53
+ VALUES = 'values'
54
+
55
+ DEPRECATED = 'deprecated'
56
+ IS_USELESS = 'isUseless'
57
+ ALLOW_DUPLICATES = 'allowDuplicates'
58
+ INCOMPATIBLE_WITH = 'incompatibleWith'
59
+
60
+ SEE_ALSO = 'seeAlso'
61
+
62
+ class PropertyData(StrEnum):
63
+ NAME = 'name'
64
+ DESCRIPTION = 'description'
65
+ PROPERTIES = 'properties'
@@ -0,0 +1,279 @@
1
+ import os, json, yaml
2
+ from pathlib import Path
3
+ from typing import TypedDict, Optional, Any
4
+
5
+ from pz_scripts_data.blocks import BlockProperties, ParameterProperties, IDProperties, PropertyData
6
+
7
+
8
+ type ScriptsBlock = dict[str, Any]
9
+ class Item(TypedDict):
10
+ input: Path
11
+ output: Path
12
+ data: Optional[dict[str, ScriptsBlock]]
13
+
14
+
15
+ items: list[Item] = [
16
+ Item(
17
+ input=Path("data/blocks"),
18
+ output=Path("out/scriptsBlocks.json"),
19
+ data=None,
20
+ ),
21
+ Item(
22
+ input=Path("data/roots"),
23
+ output=Path("out/roots.json"),
24
+ data=None,
25
+ ),
26
+ ]
27
+
28
+
29
+
30
+
31
+
32
+ ## PROCESS ITEM
33
+
34
+ def fetchBlocks(item: Item):
35
+ # combine all block json files into one
36
+ blocks: dict[str, ScriptsBlock] = {}
37
+ for file_path in item['input'].glob("*.yaml"):
38
+ with open(file_path, 'r', encoding='utf-8') as f:
39
+ data = yaml.safe_load(f)
40
+ key = data[ParameterProperties.NAME]
41
+ if key in blocks:
42
+ raise ValueError(f"Duplicate block name '{key}' found in '{file_path}'.")
43
+ blocks[key] = data
44
+ return blocks
45
+
46
+ def process_parameters(data: ScriptsBlock) -> ScriptsBlock:
47
+ # remove unnecessary fields
48
+ data.pop(BlockProperties.VERSION)
49
+
50
+ # replace parameters array into dictionary
51
+ # where keys are lowercased parameter names
52
+ parameters_data = data.get(BlockProperties.PARAMETERS, [])
53
+ parameters_new = {}
54
+ for param_data in parameters_data:
55
+ name = param_data[ParameterProperties.NAME]
56
+ parameters_new[name.lower()] = param_data
57
+ data[BlockProperties.PARAMETERS] = parameters_new
58
+
59
+ # handle properties
60
+ properties_data = data.get(BlockProperties.PROPERTIES, None)
61
+ if properties_data is not None:
62
+ for property_data in properties_data.values():
63
+ # transform into dictionary too
64
+ new_properties = {}
65
+ properties: list = property_data[PropertyData.PROPERTIES]
66
+ for prop_data in properties:
67
+ prop_name = prop_data[PropertyData.NAME]
68
+ new_properties[prop_name] = prop_data
69
+ property_data[PropertyData.PROPERTIES] = new_properties
70
+
71
+ return data
72
+
73
+ # main
74
+
75
+ def process(item: Item):
76
+ blocks = fetchBlocks(item)
77
+ for key, block in blocks.items():
78
+ blocks[key] = process_parameters(block)
79
+
80
+ return blocks
81
+
82
+
83
+
84
+ ## PREPARE
85
+
86
+ # relationship and hierarchy
87
+
88
+ def add_source_ref_to_targets(
89
+ source_name: str,
90
+ targets_names: list[str],
91
+ items: list[Item],
92
+ array_type: BlockProperties):
93
+ """
94
+ This is used to add source_name to the targets's array_type.
95
+
96
+ For example:
97
+ - source_name -> child_name
98
+ - targets_names -> child_parents
99
+ - array_type -> children (of parent)
100
+ will add source_name in the array stored at key array_type of the targets of targets_names.
101
+ (child_name --append--> array (key: children) of each parents)
102
+ """
103
+ _processed: list[str] = []
104
+ for item in items:
105
+ blocks = item['data']
106
+ assert blocks is not None, "Something went wrong."
107
+
108
+ # add children_name ref to every parents
109
+ for parent_name in targets_names:
110
+ parent_block = blocks.get(parent_name, None)
111
+ if parent_block is None:
112
+ continue
113
+
114
+ # init if needed
115
+ if array_type not in parent_block:
116
+ parent_block[array_type] = []
117
+
118
+ # add name
119
+ children: list[str] = parent_block[array_type]
120
+ if source_name not in children:
121
+ children.append(source_name)
122
+ _processed.append(parent_name)
123
+
124
+ # we make sure they were all properly processed
125
+ for parent_name in targets_names:
126
+ if parent_name not in _processed:
127
+ raise ValueError(f"Parent block '{parent_name}' not found for block '{source_name}'.")
128
+
129
+
130
+ def prepare_relationship(block: ScriptsBlock, items: list[Item]):
131
+ """
132
+ This will retrieve the parents of this block and add a reference to itself inside the data,
133
+ of this parent block
134
+ """
135
+ # handle parents
136
+ parents = block.get(BlockProperties.PARENTS, [])
137
+ if parents:
138
+ add_source_ref_to_targets(
139
+ block[BlockProperties.NAME], parents, items,
140
+ BlockProperties.CHILDREN)
141
+
142
+ # handle variants
143
+ variant = block.get(BlockProperties.VARIANT, None)
144
+ if variant:
145
+ add_source_ref_to_targets(
146
+ block[BlockProperties.NAME], [variant], items,
147
+ BlockProperties.VARIANTS)
148
+
149
+
150
+ # description and reference
151
+
152
+ def find_block_in_items(block_name: str, items: list[Item]) -> ScriptsBlock | None:
153
+ for item in items:
154
+ blocks = item['data']
155
+ assert blocks is not None, "Something is wrong."
156
+
157
+ if block_name in blocks:
158
+ return blocks[block_name]
159
+ return None
160
+
161
+
162
+ def copy_block_ref(block: ScriptsBlock, items: list[Item]) -> ScriptsBlock:
163
+ """
164
+ #desc copies the description of the source block
165
+ #ref copies every key values of that source block into the block (if not already present)
166
+ """
167
+ # copy #desc
168
+ if BlockProperties.REF_DESC in block:
169
+ desc_source_key: str = block[BlockProperties.REF_DESC]
170
+ desc_source = find_block_in_items(desc_source_key, items)
171
+
172
+ if desc_source is not None:
173
+ block[BlockProperties.DESCRIPTION] = desc_source[BlockProperties.DESCRIPTION]
174
+
175
+ # copy the short description if available too
176
+ if BlockProperties.SHORT_DESCRIPTION in desc_source:
177
+ block[BlockProperties.SHORT_DESCRIPTION] = desc_source[BlockProperties.SHORT_DESCRIPTION]
178
+
179
+ # copy #ref
180
+ if BlockProperties.REF_FULL in block:
181
+ ref_source_key: str = block[BlockProperties.REF_FULL]
182
+ ref_source = find_block_in_items(ref_source_key, items)
183
+ if ref_source is not None:
184
+ # we copy every key-value from the ref_block
185
+ # that doesn't already exist in the block
186
+ for key, value in ref_source.items():
187
+ if key not in block:
188
+ block[key] = value
189
+
190
+ return block
191
+
192
+
193
+ # parameters
194
+
195
+ def get_source_parameter(ref_key: str, items: list[Item]) -> dict:
196
+ """
197
+ This will return the source parameter data from the ref_key.
198
+ The ref_key is in the format "BlockName/ParameterName"
199
+ """
200
+ # find the source block and parameter
201
+ path = ref_key.split('/')
202
+ assert len(path) == 2, f"Invalid #ref format for '{ref_key}'. Needs to be in format 'BlockName/ParameterName'."
203
+ origin_block = path[0]
204
+ origin_param = path[1].lower()
205
+
206
+ origin_block_data = find_block_in_items(origin_block, items)
207
+
208
+ # make sure the source block and parameter exist
209
+ if origin_block_data is None:
210
+ raise ValueError(f"Source block '{origin_block}' not found for parameter '{origin_param}'.")
211
+ if BlockProperties.PARAMETERS not in origin_block_data:
212
+ raise ValueError(f"Source block '{origin_block}' has no parameters for parameter '{origin_param}'.")
213
+ if origin_param not in origin_block_data[BlockProperties.PARAMETERS]:
214
+ raise ValueError(f"Source block '{origin_block}' has no parameter '{origin_param}'.")
215
+
216
+ return origin_block_data[BlockProperties.PARAMETERS][origin_param]
217
+
218
+ def copy_parameters_ref(parameters: dict[str, dict], items: list[Item]):
219
+ for param_key, param_data in parameters.items():
220
+ if ParameterProperties.REF_FULL in param_data:
221
+ ref_key = param_data[ParameterProperties.REF_FULL]
222
+
223
+ source_param_data = get_source_parameter(ref_key, items)
224
+ assert source_param_data is not None, f"Source parameter '{ref_key}' not found for parameter '{param_key}'."
225
+
226
+ # copy every key-value from the source parameter data
227
+ # that doesn't already exist in the parameter data
228
+ for key, value in source_param_data.items():
229
+ if key not in param_data:
230
+ param_data[key] = value
231
+
232
+ if ParameterProperties.REF_DESC in param_data:
233
+ desc_key = param_data[ParameterProperties.REF_DESC]
234
+
235
+ source_param_data = get_source_parameter(desc_key, items)
236
+ assert source_param_data is not None, f"Source parameter '{desc_key}' not found for parameter '{param_key}'."
237
+
238
+ # make sure the source parameter has a description
239
+ if ParameterProperties.DESCRIPTION not in source_param_data:
240
+ raise ValueError(f"Parameter '{param_key}' has #desc but source has no description.")
241
+
242
+ # only copy the description
243
+ param_data[ParameterProperties.DESCRIPTION] = source_param_data[ParameterProperties.DESCRIPTION]
244
+
245
+
246
+ # main
247
+
248
+ def prepare(item: Item, items: list[Item]):
249
+ blocks = item['data']
250
+ assert blocks is not None, "Something went wrong."
251
+
252
+ for block in blocks.values():
253
+ prepare_relationship(block, items)
254
+ copy_block_ref(block, items)
255
+
256
+ parameters = block.get(BlockProperties.PARAMETERS, {})
257
+ copy_parameters_ref(parameters, items)
258
+
259
+
260
+ return blocks
261
+
262
+
263
+
264
+
265
+
266
+
267
+ def main():
268
+ for item in items:
269
+ item['data'] = process(item)
270
+ for item in items:
271
+ item['data'] = prepare(item, items)
272
+ for item in items:
273
+ output_file = item['output']
274
+ output_file.parent.mkdir(parents=True, exist_ok=True)
275
+ with open(output_file, 'w', encoding='utf-8') as f:
276
+ json.dump(item['data'], f, indent=2, ensure_ascii=False, sort_keys=True)
277
+
278
+ if __name__ == "__main__":
279
+ main()
File without changes
@@ -0,0 +1,150 @@
1
+ import json
2
+ import datetime
3
+ import argparse
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ def assert_release_number(version: str) -> tuple[int, int, int]:
8
+ splitted = version.split('.')
9
+ if len(splitted) != 3:
10
+ raise ValueError("Invalid build version format. Expected format: 'major.minor.patch'")
11
+ try:
12
+ major, minor, patch = [int(x) for x in splitted]
13
+ except ValueError:
14
+ raise ValueError("Invalid build version format. Expected format: 'major.minor.patch' with integers")
15
+ except Exception as e:
16
+ raise ValueError(f"Error parsing build version: {e}")
17
+ return major, minor, patch
18
+
19
+ def assert_release_with_version(version: str) -> tuple[int, int, int, int]:
20
+ splitted = version.split('.')
21
+ if len(splitted) != 4:
22
+ raise ValueError("Invalid release version format. Expected format: 'major.minor.patch.version'")
23
+ try:
24
+ major, minor, patch, release_version = [int(x) for x in splitted]
25
+ except ValueError:
26
+ raise ValueError("Invalid release version format. Expected format: 'major.minor.patch.version' with integers")
27
+ except Exception as e:
28
+ raise ValueError(f"Error parsing release version: {e}")
29
+ return major, minor, patch, release_version
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser(description="Release a dataset build")
33
+ parser.add_argument("--set-stable", type=str,
34
+ help="Move the stable to the provided build version")
35
+ parser.add_argument("--set-latest-build", type=str,
36
+ help="Update the latest build version in the manifest")
37
+ parser.add_argument("--set-latest-release", type=str,
38
+ help="Update the latest release version in the manifest")
39
+ parser.add_argument("--build", type=str,
40
+ help="Specify the new build version")
41
+ parser.add_argument("--dry-run", action="store_true",
42
+ help="Perform a dry run without making any changes")
43
+ args = parser.parse_args()
44
+
45
+ # retrieve manifest file
46
+ WORKSPACE = Path(__file__).parent.parent.parent
47
+ MANIFEST_FILE = WORKSPACE / "manifest.json"
48
+ assert MANIFEST_FILE.exists(), f"Manifest file not found at {MANIFEST_FILE}"
49
+
50
+ # retrieve the latest manifest data
51
+ MANIFEST_DATA = json.loads(MANIFEST_FILE.read_text())
52
+ latest_build = MANIFEST_DATA['latest_build']
53
+ major, minor, patch = assert_release_number(latest_build)
54
+
55
+ # handle set-latest-build argument
56
+ set_latest_build = args.set_latest_build
57
+ set_latest_release = args.set_latest_release
58
+ set_stable = args.set_stable
59
+ dry_run = args.dry_run
60
+
61
+ pushed_new_release = None
62
+ if set_latest_build is not None:
63
+ print(f"Setting latest build to {set_latest_build}")
64
+ splitted = set_latest_build.split('.')
65
+ if len(splitted) != 3:
66
+ raise ValueError("Invalid build version format. Expected format: 'major.minor.patch'")
67
+ assert_release_number(set_latest_build)
68
+
69
+ # set new version in the manifest
70
+ MANIFEST_DATA['latest_build'] = set_latest_build
71
+
72
+ # set new stable
73
+ elif set_stable:
74
+ print(f"Setting stable to {set_stable}")
75
+ assert_release_with_version(set_stable)
76
+ MANIFEST_DATA['stable'] = set_stable
77
+
78
+ # handle set-latest-release argument
79
+ elif set_latest_release is not None:
80
+ print(f"Setting latest release to {set_latest_release}")
81
+ assert_release_with_version(set_latest_release)
82
+
83
+ # get associated release
84
+ releases = MANIFEST_DATA['releases']
85
+ if set_latest_release not in releases.keys():
86
+ raise ValueError(f"Release version {set_latest_release} not found in manifest")
87
+ MANIFEST_DATA['latest_release'] = set_latest_release
88
+
89
+ # no args, push a new release
90
+ else:
91
+ print("Pushing a new release")
92
+ current_date = datetime.datetime.now().isoformat()
93
+ releases = MANIFEST_DATA['releases']
94
+ build = args.build if args.build is not None else f"{major}.{minor}.{patch}"
95
+ version = 0
96
+ if build not in releases.keys():
97
+ releases[build] = {
98
+ 'date': current_date,
99
+ 'version': version,
100
+ }
101
+ else:
102
+ existing_build = releases[build]
103
+ existing_build['date'] = current_date
104
+ existing_build['version'] += 1
105
+ version = existing_build['version']
106
+ pushed_new_release = f"{build}.{version}"
107
+
108
+ # write latest changes
109
+ if not dry_run:
110
+ MANIFEST_FILE.write_text(json.dumps(MANIFEST_DATA, indent=4))
111
+
112
+ # commit latest manifest changes
113
+ try:
114
+ if not dry_run:
115
+ subprocess.run(["git", "add", str(MANIFEST_FILE)], check=True)
116
+ message = "manifest"
117
+ if pushed_new_release is not None:
118
+ message += f": {pushed_new_release}"
119
+ elif set_stable is not None:
120
+ message += f": New stable {set_stable}"
121
+ elif set_latest_release is not None:
122
+ message += f": New latest release {set_latest_release}"
123
+ elif set_latest_build is not None:
124
+ message += f": New latest build {set_latest_build}"
125
+ subprocess.run(["git", "commit", "-m", message], check=True)
126
+ except Exception as e:
127
+ print(f"Error committing changes: {e}")
128
+
129
+ # that one we don't ignore, since that could been connection issues
130
+ if not dry_run:
131
+ subprocess.run(["git", "push"], check=True)
132
+
133
+ # if set as stable is active
134
+ # move stable tag
135
+ if set_stable is not None:
136
+ if not dry_run:
137
+ subprocess.run(["git", "tag", "-fa", "stable", "-m", "Moved stable tag"], check=True)
138
+ subprocess.run(["git", "push", "-f", "--tags"], check=True)
139
+ print(f"Moved stable tag")
140
+
141
+ if pushed_new_release is not None:
142
+ if not dry_run:
143
+ subprocess.run(["git", "tag", "-fa", pushed_new_release, "-m", "New release"], check=True)
144
+ subprocess.run(["git", "push", "-f", "--tags"], check=True)
145
+ print(f"Pushed new release: {pushed_new_release}")
146
+
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()
@@ -0,0 +1,500 @@
1
+ """Pydantic models defining the structure of block definition files.
2
+
3
+ ``BlockDefinition`` is the root model. Its JSON Schema is exported to
4
+ ``schemas/blocks.json`` by running ``python -m pz_scripts_data.schema``.
5
+ """
6
+
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any, Literal, Self
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
13
+ from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
14
+ from pydantic_core import core_schema
15
+
16
+ DEFAULT_OUTPUT = Path(__file__).resolve().parents[2] / "schemas" / "blocks.json"
17
+
18
+ ScalarType = Literal["string", "float", "boolean", "integer"]
19
+
20
+
21
+ class SchemaModel(BaseModel):
22
+ """Base model. Unknown keys are rejected and no value may be null."""
23
+
24
+ model_config = ConfigDict(extra="forbid", strict=True, validate_by_name=True)
25
+
26
+ @model_validator(mode="before")
27
+ @classmethod
28
+ def _reject_null(cls, data: Any) -> Any:
29
+ if isinstance(data, dict) and any(value is None for value in data.values()):
30
+ raise ValueError("null is not allowed; omit the key instead")
31
+ return data
32
+
33
+
34
+ class BlockReference(SchemaModel):
35
+ """If the parameter is of type 'block', this can be used to provide a description of the expected format of the block."""
36
+
37
+ name: str = Field(description="The name of the block type that this parameter references to.")
38
+ full_type: bool = Field(
39
+ True,
40
+ alias="fullType",
41
+ description="Indicates whether the parameter's value can match the module and block name, for example 'module.block'.",
42
+ )
43
+ no_auto_import: bool = Field(
44
+ False,
45
+ alias="noAutoImport",
46
+ description="Indicates whenever this parameter can automatically check its own parent block module when fullType is set to true. If false, this means that the module won't be automatically imported.",
47
+ )
48
+
49
+
50
+ class TranslationDefinition(SchemaModel):
51
+ """If the parameter is of type 'translation', this can be used to provide a description of the expected format of the translation. For script blocks, this is associated to the ID of the block."""
52
+
53
+ key_pattern: str = Field(
54
+ alias="keyPattern",
55
+ description="A regex pattern that the translation key in the source must match. The '{value}' placeholder is replaced by the ID of the block or value of the parameter. '{module}' is replaced by the module of the block.",
56
+ )
57
+ source_file: str = Field(
58
+ alias="sourceFile",
59
+ description="The source file where the translation key is defined (without the .json extension).",
60
+ )
61
+
62
+
63
+ class BlockID(SchemaModel):
64
+ """Defines the ID properties for this block type. If not specified, the block type does not have an ID."""
65
+
66
+ parents_without: list[str] | None = Field(
67
+ None,
68
+ alias="parentsWithout",
69
+ description="A list of block names that cannot be parents if this block has an ID.",
70
+ )
71
+ values: list[str] | None = Field(
72
+ None,
73
+ description="A list of allowed ID values for this block. If not given, any value is allowed.",
74
+ )
75
+ as_type: bool | None = Field(
76
+ None,
77
+ alias="asType",
78
+ description="Specifies if the ID of the block should count as the block type. For example, components have specific definitions based on their ID. This means a script block needs to be defined with the name 'component <componentID>' as well as 'component' to cover all component types available with the 'values' attribute.",
79
+ )
80
+ optional: list[str] | None = Field(
81
+ None,
82
+ description="A list of ID values that are optional for this block type.",
83
+ )
84
+ can_have_space: bool | None = Field(
85
+ None,
86
+ alias="canHaveSpace",
87
+ description="Indicates whether the ID of the block can contain spaces. If false or not specified, the ID cannot contain spaces.",
88
+ )
89
+ forbidden: list[str] | None = Field(
90
+ None,
91
+ description="A list of forbidden ID values for this block. If not provided, no values are forbidden.",
92
+ )
93
+ translation: TranslationDefinition | None = None
94
+
95
+
96
+ class BlockDeprecation(SchemaModel):
97
+ """Indicates whether the block type is deprecated."""
98
+
99
+ replaced_by: str | None = Field(
100
+ None,
101
+ alias="replacedBy",
102
+ description="The name of the block type that replaces this deprecated block type.",
103
+ )
104
+ description: str | None = Field(
105
+ None,
106
+ description="A brief description of why the block type is deprecated and any additional information about the deprecation.",
107
+ )
108
+
109
+
110
+ class _ArrayBase(SchemaModel):
111
+ separator: str = Field(description="Specifies the separator used to split the string into array items.")
112
+ min_items: float | None = Field(
113
+ None,
114
+ alias="min",
115
+ description="The minimum number of items required in the array.",
116
+ )
117
+ max_items: float | None = Field(
118
+ None,
119
+ alias="max",
120
+ description="The maximum number of items allowed in the array.",
121
+ )
122
+ soft_override: bool | None = Field(
123
+ None,
124
+ alias="softOverride",
125
+ description="If true, a soft override of this property will add its content to the previously already existing array. Thus adding the old entries is not necessary and should be avoided to prevent duplicates.",
126
+ )
127
+
128
+
129
+ class ScalarArrayDefinition(_ArrayBase):
130
+ item_type: ScalarType = Field(alias="type", description="Specifies the data type of the items in the array.")
131
+
132
+
133
+ class BlockArrayDefinition(_ArrayBase):
134
+ item_type: Literal["block"] = Field(alias="type", description="Specifies the data type of the items in the array.")
135
+ block: BlockReference = Field(description="The block type of the items in the array.")
136
+
137
+
138
+ ArrayDefinition = ScalarArrayDefinition | BlockArrayDefinition
139
+
140
+
141
+ class _ObjectBase(SchemaModel):
142
+ key_value_separator: str = Field(
143
+ alias="keyValueSeparator",
144
+ description="Specifies the separator used to split the key and the value in the key-value pairs. The parameter 'pairsSeparator' is used to split the pairs from each others.",
145
+ )
146
+ pairs_separator: str = Field(
147
+ alias="pairsSeparator",
148
+ description="Specifies the separator used to split the pairs from each others. The parameter 'keyValueSeparator' is used to split the keys and values.",
149
+ )
150
+ soft_override: bool | None = Field(
151
+ None,
152
+ alias="softOverride",
153
+ description="If true, a soft override of this property will add its content to the previously already existing array. Thus adding the old entries is not necessary and should be avoided to prevent duplicates.",
154
+ )
155
+
156
+
157
+ class ScalarKeyScalarValueObject(_ObjectBase):
158
+ key_type: ScalarType = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
159
+ value_type: ScalarType = Field(alias="valueType", description="Specifies the data type of the values in the object.")
160
+
161
+
162
+ class BlockKeyBlockValueObject(_ObjectBase):
163
+ key_type: Literal["block"] = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
164
+ key_block: BlockReference = Field(alias="keyBlock", description="The block type of the keys in the object.")
165
+ value_type: Literal["block"] = Field(alias="valueType", description="Specifies the data type of the values in the object.")
166
+ value_block: BlockReference = Field(alias="valueBlock", description="The block type of the values in the object.")
167
+
168
+
169
+ class BlockKeyScalarValueObject(_ObjectBase):
170
+ key_type: Literal["block"] = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
171
+ key_block: BlockReference = Field(alias="keyBlock", description="The block type of the keys in the object.")
172
+ value_type: ScalarType = Field(alias="valueType", description="Specifies the data type of the values in the object.")
173
+
174
+
175
+ class ScalarKeyBlockValueObject(_ObjectBase):
176
+ key_type: ScalarType = Field(alias="keyType", description="Specifies the data type of the keys in the object.")
177
+ value_type: Literal["block"] = Field(alias="valueType", description="Specifies the data type of the values in the object.")
178
+ value_block: BlockReference = Field(alias="valueBlock", description="The block type of the values in the object.")
179
+
180
+
181
+ ObjectDefinition = (
182
+ ScalarKeyScalarValueObject
183
+ | BlockKeyBlockValueObject
184
+ | BlockKeyScalarValueObject
185
+ | ScalarKeyBlockValueObject
186
+ )
187
+
188
+
189
+ class CallbackParameter(SchemaModel):
190
+ name: str = Field(description="The name of the parameter.")
191
+ param_type: str = Field(alias="type", description="The type of the parameter.")
192
+ is_java: bool = Field(
193
+ False,
194
+ alias="isJava",
195
+ description="Indicates whether the parameter is a Java object.",
196
+ )
197
+
198
+
199
+ class CallbackDefinition(SchemaModel):
200
+ """Indicates whether the parameter is a callback function."""
201
+
202
+ parameters: list[CallbackParameter] = Field(description="A list of parameters that the callback function takes.")
203
+ returns: str | None = Field(None, description="The return type of the callback function.")
204
+
205
+
206
+ class ArrayParameterType(SchemaModel):
207
+ main: Literal["array"] = Field(description="The main data type of the parameter.")
208
+ array: ArrayDefinition = Field(
209
+ description="If the parameter is of type 'array', this can be used to provide a description of the expected format of the array.",
210
+ )
211
+
212
+
213
+ class ObjectParameterType(SchemaModel):
214
+ main: Literal["object"] = Field(description="The main data type of the parameter.")
215
+ object_: ObjectDefinition = Field(
216
+ alias="object",
217
+ description="If the parameter is of type 'object', this can be used to provide a description of the expected format of the object.",
218
+ )
219
+
220
+
221
+ class BlockParameterType(SchemaModel):
222
+ main: Literal["block"] = Field(description="The main data type of the parameter.")
223
+ block: BlockReference = Field(
224
+ description="If the parameter is of type 'block', this can be used to provide a description of the expected format of the block.",
225
+ )
226
+
227
+
228
+ class CallbackParameterType(SchemaModel):
229
+ main: Literal["callback"] = Field(description="The main data type of the parameter.")
230
+ callback: CallbackDefinition = Field(description="Indicates whether the parameter is a callback function.")
231
+
232
+
233
+ class TranslationParameterType(SchemaModel):
234
+ main: Literal["translation"] = Field(description="The main data type of the parameter.")
235
+ translation: TranslationDefinition = Field(
236
+ description="If the parameter is of type 'translation', this can be used to provide a description of the expected format of the translation.",
237
+ )
238
+
239
+
240
+ class ScalarParameterType(SchemaModel):
241
+ main: ScalarType = Field(description="The main data type of the parameter.")
242
+
243
+
244
+ ParameterType = (
245
+ ArrayParameterType
246
+ | ObjectParameterType
247
+ | BlockParameterType
248
+ | CallbackParameterType
249
+ | TranslationParameterType
250
+ | ScalarParameterType
251
+ )
252
+
253
+
254
+ class ParameterDeprecation(SchemaModel):
255
+ """Indicates whether the parameter is deprecated."""
256
+
257
+ replaced_by: str | None = Field(
258
+ None,
259
+ alias="replacedBy",
260
+ description="The name of the parameter that replaces this deprecated parameter.",
261
+ )
262
+ description: str | None = Field(
263
+ None,
264
+ description="A brief description of why the parameter is deprecated and any additional information about the deprecation.",
265
+ )
266
+ version: str | None = Field(None, description="The version(s) in which the parameter is deprecated.")
267
+
268
+
269
+ class ParameterNeed(SchemaModel):
270
+ """A condition on another parameter that must be met for this parameter to be valid."""
271
+
272
+ name: str | None = Field(None, description="The name of the parameter that this condition depends on.")
273
+ values: list[str | float | bool] | None = Field(
274
+ None,
275
+ description="The values of the dependent parameter that must be met for this parameter to be valid.",
276
+ )
277
+ value_to_type: dict[str, str] | None = Field(
278
+ None,
279
+ alias="valueToType",
280
+ description="A mapping of values of the dependent parameter to the expected type of this parameter. For example, if the dependent parameter is 'type' and it has a value 'integer', then this parameter should be of type 'float'. The key is the value of the dependent parameter, and the value is the expected type of this parameter when the dependent parameter has that value.",
281
+ examples=[{"integer": "float", "double": "float", "boolean": "boolean", "string": "string"}],
282
+ )
283
+ minimum: float | None = Field(
284
+ None,
285
+ description="The minimum value for this parameter when the dependent parameter has a specific value. Requires the target parameter to be of type 'float' or 'integer'.",
286
+ )
287
+ maximum: float | None = Field(
288
+ None,
289
+ description="The maximum value for this parameter when the dependent parameter has a specific value. Requires the target parameter to be of type 'float' or 'integer'.",
290
+ )
291
+ children: list[str] | None = Field(
292
+ None,
293
+ description="A list of child blocks that must be present for this parameter to be valid.",
294
+ )
295
+ parents: list[str] | None = Field(
296
+ None,
297
+ description="A list of parent blocks that must be present for this parameter to be valid.",
298
+ )
299
+
300
+
301
+ class ParameterDefinition(SchemaModel):
302
+ """A parameter available for a block type."""
303
+
304
+ name: str | None = Field(None, description="The name of the parameter.")
305
+ any_name: bool | None = Field(
306
+ None,
307
+ alias="anyName",
308
+ description="A parameter with `anyName` can have any name.",
309
+ )
310
+ ref: str | None = Field(None, alias="#ref", description="A reference to another block parameter.")
311
+ ref_desc: str | None = Field(
312
+ None,
313
+ alias="#desc",
314
+ description="A reference to the description of another block parameter. This allows sharing the same description between multiple parameters without using #ref, which would also share other properties.",
315
+ )
316
+ description: str | None = Field(None, description="A brief description of what the parameter does.")
317
+ allowed_duplicate: bool | None = Field(
318
+ None,
319
+ alias="allowedDuplicate",
320
+ description="Indicates whether the parameter can be duplicated.",
321
+ )
322
+ can_be_empty: bool | None = Field(
323
+ None,
324
+ alias="canBeEmpty",
325
+ description="Indicates whether the parameter can be empty.",
326
+ )
327
+ item_types: list[str] | None = Field(
328
+ None,
329
+ alias="itemTypes",
330
+ description="DEPRECATED: Specifies the type of item the parameter is associated with (e.g., 'vehicle', 'item', etc.). Only for item scripts.",
331
+ )
332
+ parents_only: list[str] | None = Field(
333
+ None,
334
+ alias="parentsOnly",
335
+ description="DEPRECATED: A list of parent block types for which this parameter is applicable.",
336
+ )
337
+ required: bool | None = Field(None, description="Indicates whether the parameter is required.")
338
+ default_value: str | float | bool | list[Any] | dict[str, Any] | None = Field(
339
+ None,
340
+ alias="default",
341
+ description="The default value for the parameter if not specified.",
342
+ )
343
+ minimum: float | None = Field(
344
+ None,
345
+ description="The minimum value for the parameter if it is a number. Needs type.main to be 'float' or 'integer' to be applicable.",
346
+ )
347
+ maximum: float | None = Field(
348
+ None,
349
+ description="The maximum value for the parameter if it is a number. Needs type.main to be 'float' or 'integer' to be applicable.",
350
+ )
351
+ data_type: ParameterType | None = Field(
352
+ None,
353
+ alias="type",
354
+ description="The data type of the parameter.",
355
+ )
356
+ values: list[str | float] | None = Field(None, description="A list of allowed values for the parameter.")
357
+ deprecated: ParameterDeprecation | None = None
358
+ needs: list[ParameterNeed] | None = Field(
359
+ None,
360
+ description="A list of conditions of other parameters that must be met for this parameter to be valid.",
361
+ )
362
+ incompatible_with: list[str] | None = Field(
363
+ None,
364
+ alias="incompatibleWith",
365
+ description="List of other parameters which are incompatible with this one.",
366
+ )
367
+ is_useless: bool = Field(
368
+ False,
369
+ alias="isUseless",
370
+ description="Indicates whether the parameter is considered useless or not. This can be used to flag parameters that have no effect in the game. Not to be confused with deprecated which indicates a parameter that previously worked but no longer does.",
371
+ )
372
+ see_also: list[str] | None = Field(
373
+ None,
374
+ alias="seeAlso",
375
+ description="A list of other parameters that are related to this one.",
376
+ )
377
+
378
+
379
+ class BlockDefinition(SchemaModel):
380
+ """Schema of a block definition file."""
381
+
382
+ model_config = ConfigDict(
383
+ title="Blocks Folder General Schema",
384
+ json_schema_extra=lambda schema: schema.update(
385
+ allOf=[
386
+ {
387
+ "if": {"required": ["isRoot"], "properties": {"isRoot": {"const": True}}},
388
+ "then": {"required": ["pattern"]},
389
+ }
390
+ ]
391
+ ),
392
+ )
393
+
394
+ version: float | str | None = None
395
+ ref_desc: str | None = Field(
396
+ None,
397
+ alias="#desc",
398
+ description="A reference to the description of another block type. This allows sharing the same description between multiple block types without using #ref, which would also share other properties.",
399
+ )
400
+ ref: str | None = Field(
401
+ None,
402
+ alias="#ref",
403
+ description="A reference to another block type. This allows sharing the same properties between multiple block types.",
404
+ )
405
+ variant_of: str | None = Field(
406
+ None,
407
+ alias="variantOf",
408
+ description="Indicates whether this block type is a variant of another block type. The value corresponds to the block type it is a variant of.",
409
+ )
410
+ name: str = Field(description="The name of the block type.")
411
+ any_name: bool | None = Field(
412
+ None,
413
+ alias="anyName",
414
+ description="A block with `anyName` can have any name. This requires strict placement in parent blocks to correctly validate the block parameters.",
415
+ )
416
+ short_description: str | None = Field(
417
+ None,
418
+ alias="shortDescription",
419
+ description="A brief description of the block type, used for quick reference in completion items.",
420
+ )
421
+ description: str | None = Field(None, description="A brief description of the block type.")
422
+ block_id: BlockID | None = Field(None, alias="ID")
423
+ parents: list[str] | None = Field(None, description="A list of valid parent block types.")
424
+ needs_children: list[str] | None = Field(
425
+ None,
426
+ alias="needsChildren",
427
+ description="A list of required child block types.",
428
+ )
429
+ soft_override: bool | None = Field(
430
+ None,
431
+ alias="softOverride",
432
+ description="Indicates whether the block can be soft overridden in scripts.",
433
+ )
434
+ is_root: bool | None = Field(
435
+ None,
436
+ alias="isRoot",
437
+ description="Indicates whether the block is a root document.",
438
+ )
439
+ pattern: list[str] | None = Field(
440
+ None,
441
+ description="A list of regex patterns that can be used to match this root file. Required when 'isRoot' is true.",
442
+ )
443
+ no_comma: bool | None = Field(
444
+ None,
445
+ alias="noComma",
446
+ description="Indicates whether the block parameters should not be followed by a comma.",
447
+ )
448
+ deprecated: BlockDeprecation | None = None
449
+ parameters: list[ParameterDefinition] | None = Field(
450
+ None,
451
+ description="A list of parameters available for this block type.",
452
+ )
453
+ properties: dict[str, Any] | None = Field(
454
+ None,
455
+ description="Custom additional properties for the block type.",
456
+ )
457
+
458
+ @model_validator(mode="after")
459
+ def _root_requires_pattern(self) -> Self:
460
+ if self.is_root is True and self.pattern is None:
461
+ raise ValueError("'pattern' is required when 'isRoot' is true")
462
+ return self
463
+
464
+
465
+ class _BlocksJsonSchemaGenerator(GenerateJsonSchema):
466
+ """Draft-07 output without null members, titles or `default: null` entries, matching the hand-written schema."""
467
+
468
+ schema_dialect = "http://json-schema.org/draft-07/schema#"
469
+
470
+ def field_title_should_be_set(self, schema: Any) -> bool:
471
+ return False
472
+
473
+ def model_schema(self, schema: core_schema.ModelSchema) -> JsonSchemaValue:
474
+ json_schema = super().model_schema(schema)
475
+ if schema["cls"] is not BlockDefinition:
476
+ json_schema.pop("title", None)
477
+ return json_schema
478
+
479
+ def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue:
480
+ return self.generate_inner(schema["schema"])
481
+
482
+ def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
483
+ if schema.get("default") is None:
484
+ return self.generate_inner(schema["schema"])
485
+ return super().default_schema(schema)
486
+
487
+
488
+ def build_json_schema() -> dict[str, Any]:
489
+ return BlockDefinition.model_json_schema(by_alias=True, schema_generator=_BlocksJsonSchemaGenerator)
490
+
491
+
492
+ def main(argv: list[str]) -> int:
493
+ output = Path(argv[0]) if argv else DEFAULT_OUTPUT
494
+ output.write_text(json.dumps(build_json_schema(), indent=2) + "\n", encoding="utf-8")
495
+ print(f"Wrote {output}")
496
+ return 0
497
+
498
+
499
+ if __name__ == "__main__":
500
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
1
+ import json
2
+
3
+ INPUT_FILE = "out/scriptsBlocks.json"
4
+ OUTPUT_FILE = "out/itemParameters.json"
5
+
6
+
7
+
8
+ def get_itemtype_needs(param_data: dict) -> list[str] | None:
9
+ # skip if no needs
10
+ if 'needs' not in param_data:
11
+ return None
12
+ # skip if no need for ItemType
13
+ for need in param_data['needs']:
14
+ if need.get('name', None) == 'ItemType':
15
+ return need.get('values', None)
16
+ return None
17
+
18
+
19
+ # load item ItemTypes available
20
+ with open(INPUT_FILE, 'r', encoding='utf-8') as f:
21
+ blocks = json.load(f)
22
+
23
+ item_block = blocks['item']
24
+ itemtypes = item_block['parameters']['itemtype']['values']
25
+
26
+ # initialize dictionary of ItemTypes
27
+ type_2_params = {}
28
+ for itemtype in itemtypes:
29
+ type_2_params[itemtype] = []
30
+
31
+
32
+ # for each parameter, check if it has a need for ItemType
33
+ # if so, it means it specifically is only valid those ItemTypes
34
+ for param_key, param_data in item_block['parameters'].items():
35
+ itemtypes = get_itemtype_needs(param_data)
36
+ if itemtypes is not None:
37
+ for itemtype in itemtypes:
38
+ type_2_params[itemtype].append(param_data['name'])
39
+
40
+
41
+ with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
42
+ json.dump(type_2_params, f, indent=4)
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate YAML block files against the blocks.json schema.
4
+
5
+ Usage:
6
+ python validateBlocks.py # Validate all YAML files in data/blocks/
7
+ python validateBlocks.py <file_path> # Validate a specific file
8
+ python validateBlocks.py <directory_path> # Validate all YAML files in a directory
9
+ python validateBlocks.py --help # Show this help message
10
+ """
11
+
12
+ import json, yaml, sys, os
13
+ from pathlib import Path
14
+ from jsonschema import Draft7Validator
15
+ from typing import List, Tuple
16
+
17
+ # Default paths
18
+ DEFAULT_BLOCKS_DIR = "data/blocks"
19
+ DEFAULT_SCHEMA_PATH = "schemas/blocks.json"
20
+
21
+ def load_schema(schema_path: str) -> dict:
22
+ """Load the JSON schema from file."""
23
+ with open(schema_path, 'r') as f:
24
+ return json.load(f)
25
+
26
+ def load_yaml_file(file_path: str) -> dict:
27
+ """Load a YAML file and return its contents."""
28
+ with open(file_path, 'r') as f:
29
+ return yaml.safe_load(f)
30
+
31
+ def validate_file(file_path: str, schema: dict) -> Tuple[bool, List[str]]:
32
+ """
33
+ Validate a single YAML file against the schema.
34
+
35
+ Returns:
36
+ Tuple of (is_valid, error_messages)
37
+ """
38
+ errors = []
39
+ try:
40
+ data = load_yaml_file(file_path)
41
+
42
+ # Handle empty files
43
+ if data is None:
44
+ return True, []
45
+
46
+ # Validate against schema
47
+ validator = Draft7Validator(schema)
48
+ validation_errors = list(validator.iter_errors(data))
49
+
50
+ if validation_errors:
51
+ for error in validation_errors:
52
+ path = ".".join(str(p) for p in error.absolute_path) or "root"
53
+ errors.append(f" {path}: {error.message}")
54
+ return False, errors
55
+
56
+ return True, []
57
+
58
+ except yaml.YAMLError as e:
59
+ return False, [f" YAML Parse Error: {str(e)}"]
60
+ except Exception as e:
61
+ return False, [f" Error: {str(e)}"]
62
+
63
+ def get_yaml_files(path: str) -> List[str]:
64
+ """Get all YAML files from a path (file or directory)."""
65
+ path_obj = Path(path)
66
+
67
+ if path_obj.is_file():
68
+ if path_obj.suffix.lower() in ['.yaml', '.yml']:
69
+ return [str(path_obj)]
70
+ return []
71
+ elif path_obj.is_dir():
72
+ return sorted([str(f) for f in path_obj.glob("*.yaml")] +
73
+ [str(f) for f in path_obj.glob("*.yml")])
74
+
75
+ return []
76
+
77
+ def main():
78
+ """Main validation function."""
79
+ # Parse arguments
80
+ if len(sys.argv) > 1:
81
+ if sys.argv[1] in ['--help', '-h', 'help']:
82
+ print(__doc__)
83
+ return 0
84
+ target_path = sys.argv[1]
85
+ else:
86
+ target_path = DEFAULT_BLOCKS_DIR
87
+
88
+ # Check if project root is correct
89
+ if not os.path.exists(DEFAULT_SCHEMA_PATH):
90
+ print(f"Error: Schema not found at {DEFAULT_SCHEMA_PATH}")
91
+ print("Please run this script from the project root directory.")
92
+ return 1
93
+
94
+ # Load schema
95
+ try:
96
+ schema = load_schema(DEFAULT_SCHEMA_PATH)
97
+ except Exception as e:
98
+ print(f"Error loading schema: {e}")
99
+ return 1
100
+
101
+ # Get files to validate
102
+ if not os.path.exists(target_path):
103
+ print(f"Error: Path not found: {target_path}")
104
+ return 1
105
+
106
+ yaml_files = get_yaml_files(target_path)
107
+
108
+ if not yaml_files:
109
+ print(f"No YAML files found in {target_path}")
110
+ return 1
111
+
112
+ # Validate files
113
+ print(f"Validating {len(yaml_files)} file(s)...\n")
114
+
115
+ valid_count = 0
116
+ invalid_count = 0
117
+
118
+ for file_path in yaml_files:
119
+ is_valid, errors = validate_file(file_path, schema)
120
+
121
+ if is_valid:
122
+ print(f"✓ {file_path}")
123
+ valid_count += 1
124
+ else:
125
+ print(f"✗ {file_path}")
126
+ for error in errors:
127
+ print(error)
128
+ print()
129
+ invalid_count += 1
130
+
131
+ # Summary
132
+ print("\n" + "="*60)
133
+ print(f"Results: {valid_count} valid, {invalid_count} invalid")
134
+ print("="*60)
135
+
136
+ return 0 if invalid_count == 0 else 1
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: pz-scripts-data
3
+ Version: 1.1.0
4
+ Summary: Dataset for the Scripts of Project Zomboid.
5
+ Author: SimKDT
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Topic :: Database
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.12.3
14
+ License-File: LICENSE
15
+ Requires-Dist: pyyaml
16
+ Requires-Dist: jsonschema
17
+ Requires-Dist: pydantic>=2.11
18
+ Dynamic: license-file
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/pz_scripts_data/blocks.py
5
+ src/pz_scripts_data/formatBlocks.py
6
+ src/pz_scripts_data/py.typed
7
+ src/pz_scripts_data/release.py
8
+ src/pz_scripts_data/schema.py
9
+ src/pz_scripts_data/sortItemParameters.py
10
+ src/pz_scripts_data/validateBlocks.py
11
+ src/pz_scripts_data.egg-info/PKG-INFO
12
+ src/pz_scripts_data.egg-info/SOURCES.txt
13
+ src/pz_scripts_data.egg-info/dependency_links.txt
14
+ src/pz_scripts_data.egg-info/entry_points.txt
15
+ src/pz_scripts_data.egg-info/requires.txt
16
+ src/pz_scripts_data.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ release = pz_scripts_data.release:main
@@ -0,0 +1,3 @@
1
+ pyyaml
2
+ jsonschema
3
+ pydantic>=2.11
@@ -0,0 +1 @@
1
+ pz_scripts_data