Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ dist/
base/
modified/

# Extracted game schemas (copyrighted, not committed)
schemas/

# Editor files
*.swp
*~
Expand Down
18 changes: 5 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
.PHONY: build clean validate test all schemas schema-validate lint check-translations
.PHONY: build clean validate test all lint check-translations schema-validate

DIST := dist
SRC := src
SCHEMAS := schemas

all: validate build test

Expand All @@ -14,16 +13,6 @@ $(DIST)/ext_01.cat: $(shell find $(SRC) -type f 2>/dev/null) content.xml
cp content.xml $(DIST)/content.xml
@echo "Build complete: $(DIST)/"

schemas:
ifndef X4_GAME_DIR
$(error X4_GAME_DIR not set — e.g. make schemas X4_GAME_DIR="/path/to/X4 Foundations")
endif
uv run x4cat extract "$(X4_GAME_DIR)" -o $(SCHEMAS) -g '*.xsd'
@echo "Schemas extracted to $(SCHEMAS)/"

schema-validate:
uv run pytest tests/test_mod.py::TestSchema -q

lint:
ifndef X4_GAME_DIR
$(error X4_GAME_DIR not set — e.g. make lint X4_GAME_DIR="/path/to/X4 Foundations")
Expand All @@ -33,11 +22,14 @@ endif
check-translations:
uv run x4cat validate-translations $(SRC)

schema-validate:
uv run x4cat validate-schema $(SRC)

validate:
uv run pytest tests/test_mod.py::TestValidate -q

test: build
uv run pytest -q -k "not TestSchema"
uv run pytest -q

clean:
rm -rf $(DIST)
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Proof of concept [X4: Foundations](https://wiki.egosoft.com/X4%20Foundations%20Wiki/Modding%20Support/) extension mod template.

Uses [x4cat](https://github.com/meethune/x4cat) to pack mod files into `.cat/.dat` catalogs, generate and validate diff patches, and manage translations. See the [x4cat documentation](https://meethune.github.io/x4cat/) for full reference.
Uses [x4cat](https://github.com/meethune/x4cat) for catalog packing, diff patch validation, translation checking, schema validation, and content scaffolding. See the [x4cat documentation](https://meethune.github.io/x4cat/) for full reference.

## Structure

Expand All @@ -15,7 +15,6 @@ src/ — Mod files (packed into catalog)
dist/ — Build output (do not commit)
base/ — Extracted base files for xmldiff workflow (do not commit)
modified/ — Modified files for xmldiff workflow (do not commit)
schemas/ — XSD schemas extracted from game (do not commit)
tests/ — Build validation tests
```

Expand Down Expand Up @@ -49,8 +48,9 @@ x4cat xmldiff --base ./base/libraries/wares.xml \
# 4. Validate your patches against the game
make lint X4_GAME_DIR="/path/to/X4 Foundations"

# 5. Validate translations
# 5. Validate translations and schema
make check-translations
make schema-validate

# 6. Build and test
make all
Expand All @@ -74,6 +74,7 @@ make all # validate, build, test
make build # pack src/ into dist/ext_01.cat + copy content.xml
make validate # XML well-formedness + structural checks
make check-translations # validate text references against t/*.xml
make schema-validate # validate MD/AI scripts against indexed schema rules
make test # full test suite (includes build verification)
make clean # remove dist/
```
Expand All @@ -87,27 +88,26 @@ the base game files they target:
make lint X4_GAME_DIR="/path/to/X4 Foundations"
```

## Conflict checking
## Schema validation

Check your mod against another mod for overlapping diff patches:
Validate MD and AI scripts against indexed schema rules (requires a game index):

```bash
x4cat check-conflicts src/ /path/to/other_mod/src/
x4cat index "/path/to/X4 Foundations"
make schema-validate
```

## Schema validation
This uses x4cat's SQLite-backed schema validator which runs in milliseconds,
replacing the previous lxml-based XSD validation that took ~70 seconds.

Optionally extract XSD schemas from your X4 install for full schema validation
of MD and AI scripts:
## Conflict checking

Check your mod against another mod for overlapping diff patches:

```bash
make schemas X4_GAME_DIR="/path/to/X4 Foundations"
make schema-validate
x4cat check-conflicts src/ /path/to/other_mod/src/
```

Note: full XSD validation is slow (~70s) due to the size of Egosoft's schemas.
The default `make all` uses fast structural checks instead.

## Installing

Copy the contents of `dist/` into your X4 extensions directory:
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Repository = "https://github.com/meethune/extension_poc"
dev = [
"x4cat",
"pytest>=8.0",
"lxml>=6.0.4",
]

[tool.pytest.ini_options]
Expand Down
68 changes: 0 additions & 68 deletions tests/test_mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
PROJECT_ROOT = Path(__file__).resolve().parent.parent
SRC_DIR = PROJECT_ROOT / "src"
DIST_DIR = PROJECT_ROOT / "dist"
SCHEMAS_DIR = PROJECT_ROOT / "schemas"
CONTENT_XML = PROJECT_ROOT / "content.xml"

REQUIRED_CONTENT_ATTRS = ("id", "version", "name")
Expand All @@ -30,12 +29,6 @@
},
}

# Maps src/ subdirectories to their XSD schema paths (relative to schemas/).
SCHEMA_MAP: dict[str, str] = {
"md": "md/md.xsd",
"aiscripts": "aiscripts/aiscripts.xsd",
}


class TestValidate:
"""Pre-build validation — XML well-formedness and structural checks."""
Expand Down Expand Up @@ -95,67 +88,6 @@ def test_xml_structure(self) -> None:
pytest.fail("Structural validation errors:\n" + "\n".join(errors))


class TestSchema:
"""Full XSD schema validation — slow (~70s), opt-in via ``make schema-validate``.

Requires schemas extracted from game files:
``make schemas X4_GAME_DIR="/path/to/X4 Foundations"``
"""

@pytest.fixture(autouse=True)
def _require_schemas(self) -> None:
if not SCHEMAS_DIR.is_dir() or not any(SCHEMAS_DIR.rglob("*.xsd")):
pytest.skip("schemas/ not found — run 'make schemas' to extract from game")

@staticmethod
def _load_schema(xsd_path: Path) -> "etree.XMLSchema": # type: ignore[name-defined]
from lxml import etree

schema_doc = etree.parse(str(xsd_path))
return etree.XMLSchema(schema_doc)

@staticmethod
def _collect_xml_for_schema(subdir: str) -> list[Path]:
src_subdir = SRC_DIR / subdir
if not src_subdir.is_dir():
return []
return sorted(src_subdir.rglob("*.xml"))

def test_md_scripts_valid(self) -> None:
xsd_path = SCHEMAS_DIR / SCHEMA_MAP["md"]
if not xsd_path.exists():
pytest.skip(f"Schema not found: {xsd_path}")
xml_files = self._collect_xml_for_schema("md")
if not xml_files:
pytest.skip("No MD scripts in src/md/")
schema = self._load_schema(xsd_path)
self._validate_files(schema, xml_files)

def test_aiscripts_valid(self) -> None:
xsd_path = SCHEMAS_DIR / SCHEMA_MAP["aiscripts"]
if not xsd_path.exists():
pytest.skip(f"Schema not found: {xsd_path}")
xml_files = self._collect_xml_for_schema("aiscripts")
if not xml_files:
pytest.skip("No AI scripts in src/aiscripts/")
schema = self._load_schema(xsd_path)
self._validate_files(schema, xml_files)

@staticmethod
def _validate_files(schema: "etree.XMLSchema", xml_files: list[Path]) -> None: # type: ignore[name-defined]
from lxml import etree

errors: list[str] = []
for xml_file in xml_files:
doc = etree.parse(str(xml_file))
if not schema.validate(doc):
rel = xml_file.relative_to(PROJECT_ROOT)
for err in schema.error_log:
errors.append(f"{rel}:{err.line}: {err.message}")
if errors:
pytest.fail("XSD validation errors:\n" + "\n".join(errors))


class TestBuild:
"""Post-build verification — catalog integrity and round-trip correctness."""

Expand Down
Loading
Loading