Compare commits
No commits in common. "main" and "main" have entirely different histories.
@ -11,6 +11,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v5
|
||||
- run: apt-get update && apt-get install libxml2-utils -qqy
|
||||
- name: Fetch main branch
|
||||
@ -50,7 +51,6 @@ jobs:
|
||||
if: github.ref == 'refs/heads/signed' && github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v5
|
||||
- run: pip3 install requests
|
||||
- name: Install minisign
|
||||
|
@ -16,15 +16,3 @@ repos:
|
||||
- id: yamllint
|
||||
args:
|
||||
- -s
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --output-format=full
|
||||
exclude: ^source/tools/webservices/
|
||||
- id: ruff-format
|
||||
args:
|
||||
- --check
|
||||
- --target-version
|
||||
- py311
|
||||
|
@ -1,11 +1,5 @@
|
||||
[font="sans-bold-24"] Community-mod changelog
|
||||
|
||||
[font="sans-bold-20"]
|
||||
#Version 12 (September 10, 2024)
|
||||
[font="sans-16"]
|
||||
- Double the Experience required for Athenian Hoplites to convert into Champion Spearmen.
|
||||
- Remove walls destroying trees upon completion due to crashes.
|
||||
|
||||
[font="sans-bold-20"]
|
||||
#Version 11 (September 9, 2024)
|
||||
[font="sans-16"]
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "community-mod",
|
||||
"version": "0.26.12",
|
||||
"version": "0.26.11",
|
||||
"label": "0 A.D. Community Mod",
|
||||
"url": "https://gitlab.com/0ad/0ad-community-mod-a26",
|
||||
"description": "The Community Mod is a community-led effort to improve the gameplay of 0 A.D., officially backed by the 0 A.D. team.",
|
||||
|
@ -81,8 +81,7 @@ BuildRestrictions.prototype.Init = function()
|
||||
* (template name should be "preview|"+templateName), as otherwise territory
|
||||
* checks for buildings with territory influence will not work as expected.
|
||||
*/
|
||||
BuildRestrictions.prototype.CheckPlacement = function()
|
||||
{
|
||||
BuildRestrictions.prototype.CheckPlacement = function () {
|
||||
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
|
||||
var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building";
|
||||
|
||||
@ -101,8 +100,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
return result; // Fail
|
||||
|
||||
// TODO: AI has no visibility info
|
||||
if (!cmpPlayer.IsAI())
|
||||
{
|
||||
if (!cmpPlayer.IsAI()) {
|
||||
// Check whether it's in a visible or fogged region
|
||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
||||
@ -110,8 +108,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
return result; // Fail
|
||||
|
||||
var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden");
|
||||
if (!explored)
|
||||
{
|
||||
if (!explored) {
|
||||
result.message = markForTranslation("%(name)s cannot be built in unexplored area");
|
||||
return result; // Fail
|
||||
}
|
||||
@ -119,8 +116,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
|
||||
// Check obstructions and terrain passability
|
||||
var passClassName = "";
|
||||
switch (this.template.PlacementType)
|
||||
{
|
||||
switch (this.template.PlacementType) {
|
||||
case "shore":
|
||||
passClassName = "building-shore";
|
||||
break;
|
||||
@ -141,20 +137,28 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
return result; // Fail
|
||||
|
||||
|
||||
if (this.template.Category == "Wall")
|
||||
{
|
||||
// for walls, only test the center point
|
||||
var ret = cmpObstruction.CheckFoundation(passClassName, true);
|
||||
if (this.template.Category == "Wall") {
|
||||
// allow walls to be built on top of trees
|
||||
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
|
||||
let areWoodEnts = collisions.length > 0;
|
||||
for (let ent of collisions) {
|
||||
// don't let entities with unitmotion prevent us from laying a foundation
|
||||
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
|
||||
let cmpResource = Engine.QueryInterface(ent, IID_ResourceSupply);
|
||||
if ((!cmpResource || cmpResource.GetType().generic !== "wood") && !cmpUnitMotion) {
|
||||
areWoodEnts = false;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
// for walls, only test the center point
|
||||
var ret = areWoodEnts ? "success" : cmpObstruction.CheckFoundation(passClassName, true);
|
||||
}
|
||||
else {
|
||||
var ret = cmpObstruction.CheckFoundation(passClassName, false);
|
||||
}
|
||||
|
||||
if (ret != "success")
|
||||
{
|
||||
switch (ret)
|
||||
{
|
||||
if (ret != "success") {
|
||||
switch (ret) {
|
||||
case "fail_error":
|
||||
case "fail_no_obstruction":
|
||||
error("CheckPlacement: Error returned from CheckFoundation");
|
||||
@ -183,8 +187,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
var isNeutral = tileOwner == 0;
|
||||
|
||||
var invalidTerritory = "";
|
||||
if (isOwn)
|
||||
{
|
||||
if (isOwn) {
|
||||
if (!this.HasTerritory("own"))
|
||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||
invalidTerritory = markForTranslationWithContext("Territory type", "own");
|
||||
@ -192,8 +195,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own");
|
||||
}
|
||||
else if (isMutualAlly)
|
||||
{
|
||||
else if (isMutualAlly) {
|
||||
if (!this.HasTerritory("ally"))
|
||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||
invalidTerritory = markForTranslationWithContext("Territory type", "allied");
|
||||
@ -201,22 +203,19 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied");
|
||||
}
|
||||
else if (isNeutral)
|
||||
{
|
||||
else if (isNeutral) {
|
||||
if (!this.HasTerritory("neutral"))
|
||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||
invalidTerritory = markForTranslationWithContext("Territory type", "neutral");
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
// consider everything else enemy territory
|
||||
if (!this.HasTerritory("enemy"))
|
||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||
invalidTerritory = markForTranslationWithContext("Territory type", "enemy");
|
||||
}
|
||||
|
||||
if (invalidTerritory)
|
||||
{
|
||||
if (invalidTerritory) {
|
||||
result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s");
|
||||
result.translateParameters.push("territoryType");
|
||||
result.translateParameters.push("validTerritories");
|
||||
@ -227,10 +226,8 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
}
|
||||
|
||||
// Check special requirements
|
||||
if (this.template.PlacementType == "shore")
|
||||
{
|
||||
if (!cmpObstruction.CheckShorePlacement())
|
||||
{
|
||||
if (this.template.PlacementType == "shore") {
|
||||
if (!cmpObstruction.CheckShorePlacement()) {
|
||||
result.message = markForTranslation("%(name)s must be built on a valid shoreline");
|
||||
return result; // Fail
|
||||
}
|
||||
@ -242,22 +239,18 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName));
|
||||
|
||||
// Check distance restriction
|
||||
if (this.template.Distance)
|
||||
{
|
||||
if (this.template.Distance) {
|
||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
var cat = this.template.Distance.FromClass;
|
||||
|
||||
var filter = function(id)
|
||||
{
|
||||
var filter = function (id) {
|
||||
var cmpIdentity = Engine.QueryInterface(id, IID_Identity);
|
||||
return cmpIdentity.GetClassesList().indexOf(cat) > -1;
|
||||
};
|
||||
|
||||
if (this.template.Distance.MinDistance !== undefined)
|
||||
{
|
||||
if (this.template.Distance.MinDistance !== undefined) {
|
||||
let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template);
|
||||
if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter))
|
||||
{
|
||||
if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) {
|
||||
let result = markForPluralTranslation(
|
||||
"%(name)s too close to a %(category)s, must be at least %(distance)s meter away",
|
||||
"%(name)s too close to a %(category)s, must be at least %(distance)s meters away",
|
||||
@ -274,11 +267,9 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
return result; // Fail
|
||||
}
|
||||
}
|
||||
if (this.template.Distance.MaxDistance !== undefined)
|
||||
{
|
||||
if (this.template.Distance.MaxDistance !== undefined) {
|
||||
let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template);
|
||||
if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter))
|
||||
{
|
||||
if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) {
|
||||
let result = markForPluralTranslation(
|
||||
"%(name)s too far from a %(category)s, must be within %(distance)s meter",
|
||||
"%(name)s too far from a %(category)s, must be within %(distance)s meters",
|
||||
@ -303,6 +294,11 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
||||
return result;
|
||||
};
|
||||
|
||||
BuildRestrictions.prototype.IsWoodEntity = function (entity) {
|
||||
let cmpResource = Engine.QueryInterface(entity, IID_ResourceSupply);
|
||||
return cmpResource && cmpResource.GetType().generic === "wood";
|
||||
}
|
||||
|
||||
BuildRestrictions.prototype.GetCategory = function()
|
||||
{
|
||||
return this.template.Category;
|
||||
|
@ -5,8 +5,7 @@ Foundation.prototype.Schema =
|
||||
"<ref name='nonNegativeDecimal'/>" +
|
||||
"</element>";
|
||||
|
||||
Foundation.prototype.Init = function()
|
||||
{
|
||||
Foundation.prototype.Init = function () {
|
||||
// Foundations are initially 'uncommitted' and do not block unit movement at all
|
||||
// (to prevent players exploiting free foundations to confuse enemy units).
|
||||
// The first builder to reach the uncommitted foundation will tell friendly units
|
||||
@ -21,6 +20,7 @@ Foundation.prototype.Init = function()
|
||||
this.buildTimeModifier = +this.template.BuildTimeModifier;
|
||||
|
||||
this.previewEntity = INVALID_ENTITY;
|
||||
this.entsToDestroy;
|
||||
};
|
||||
|
||||
Foundation.prototype.Serialize = function()
|
||||
@ -101,9 +101,12 @@ Foundation.prototype.GetNumBuilders = function()
|
||||
return this.builders.size;
|
||||
};
|
||||
|
||||
Foundation.prototype.IsFinished = function()
|
||||
{
|
||||
return (this.GetBuildProgress() == 1.0);
|
||||
Foundation.prototype.IsFinished = function () {
|
||||
if (this.GetBuildProgress() == 1.0 && this.entsToDestroy)
|
||||
for (let ent of this.entsToDestroy)
|
||||
Engine.DestroyEntity(ent);
|
||||
|
||||
return this.GetBuildProgress() == 1.0;
|
||||
};
|
||||
|
||||
Foundation.prototype.OnOwnershipChanged = function(msg)
|
||||
@ -246,22 +249,18 @@ Foundation.prototype.GetBuildTime = function()
|
||||
/**
|
||||
* @return {boolean} - Whether the foundation has been committed sucessfully.
|
||||
*/
|
||||
Foundation.prototype.Commit = function()
|
||||
{
|
||||
Foundation.prototype.Commit = function () {
|
||||
if (this.committed)
|
||||
return false;
|
||||
|
||||
|
||||
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
||||
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
|
||||
{
|
||||
for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
|
||||
Engine.DestroyEntity(ent);
|
||||
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true)) {
|
||||
this.entsToDestroy = cmpObstruction.GetEntitiesDeletedUponConstruction();
|
||||
|
||||
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
|
||||
if (collisions.length)
|
||||
{
|
||||
for (let ent of collisions)
|
||||
{
|
||||
if (collisions.length) {
|
||||
for (let ent of collisions) {
|
||||
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (cmpUnitAI)
|
||||
cmpUnitAI.LeaveFoundation(this.entity);
|
||||
@ -273,10 +272,10 @@ Foundation.prototype.Commit = function()
|
||||
// animation to indicate they're waiting for people to get
|
||||
// out the way
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The obstruction always blocks new foundations/construction,
|
||||
// but we've temporarily allowed units to walk all over it
|
||||
// (via CCmpTemplateManager). Now we need to remove that temporary
|
||||
|
@ -4,7 +4,7 @@
|
||||
<Rank>Elite</Rank>
|
||||
</Identity>
|
||||
<Promotion>
|
||||
<RequiredXp>400</RequiredXp>
|
||||
<RequiredXp>200</RequiredXp>
|
||||
<Entity>units/athen/champion_infantry</Entity>
|
||||
</Promotion>
|
||||
<VisualActor>
|
||||
|
56
ruff.toml
56
ruff.toml
@ -1,56 +0,0 @@
|
||||
line-length = 99
|
||||
|
||||
[format]
|
||||
line-ending = "lf"
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = [
|
||||
"ANN",
|
||||
"BLE001",
|
||||
"C90",
|
||||
"COM812",
|
||||
"D10",
|
||||
"DTZ005",
|
||||
"EM",
|
||||
"FA",
|
||||
"FIX",
|
||||
"FBT",
|
||||
"ISC001",
|
||||
"N817",
|
||||
"PERF203",
|
||||
"PERF401",
|
||||
"PLR0912",
|
||||
"PLR0913",
|
||||
"PLR0915",
|
||||
"PLR2004",
|
||||
"PLW2901",
|
||||
"PT",
|
||||
"PTH",
|
||||
"RUF012",
|
||||
"S101",
|
||||
"S310",
|
||||
"S314",
|
||||
"S324",
|
||||
"S320",
|
||||
"S603",
|
||||
"S607",
|
||||
"T20",
|
||||
"TD002",
|
||||
"TD003",
|
||||
"TRY002",
|
||||
"TRY003",
|
||||
"TRY004",
|
||||
"TRY301",
|
||||
"UP038",
|
||||
"W505"
|
||||
]
|
||||
|
||||
[lint.isort]
|
||||
lines-after-imports = 2
|
||||
|
||||
[lint.pycodestyle]
|
||||
max-doc-length = 72
|
||||
|
||||
[lint.pydocstyle]
|
||||
convention = "pep257"
|
@ -1,17 +1,16 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MOD_PATH = Path("community-mod")
|
||||
|
||||
|
||||
def check_cwd_is_correct():
|
||||
cwd = Path().resolve()
|
||||
try:
|
||||
if not Path(".git").exists():
|
||||
raise Exception("No .git in current folder.")
|
||||
with open(MOD_PATH / "mod.json") as f:
|
||||
with open(MOD_PATH / "mod.json", "r") as f:
|
||||
mod = json.load(f)
|
||||
if mod["name"] != "community-mod":
|
||||
if mod['name'] != "community-mod":
|
||||
raise Exception("mod.json has incorrect name")
|
||||
except Exception as e:
|
||||
raise Exception("scripts.py must be called from the community mod repository") from e
|
||||
except:
|
||||
raise Exception("scripts.py must be called from the community mod repository")
|
||||
|
@ -1,25 +1,25 @@
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from . import MOD_PATH, check_cwd_is_correct
|
||||
|
||||
|
||||
PUBLIC_PATH = Path("binaries/data/mods/public/")
|
||||
|
||||
DEFAULT_COPY = ["simulation/data", "simulation/templates"]
|
||||
|
||||
DEFAULT_COPY = [
|
||||
"simulation/data",
|
||||
"simulation/templates"
|
||||
]
|
||||
|
||||
def validate_path(path: str):
|
||||
mod_path = Path(path) / PUBLIC_PATH
|
||||
try:
|
||||
with open(mod_path / "mod.json", encoding="utf-8") as f:
|
||||
with open(mod_path / "mod.json", "r", encoding="utf-8") as f:
|
||||
mod = json.load(f)
|
||||
if mod["name"] != "0ad":
|
||||
if mod['name'] != "0ad":
|
||||
raise Exception("mod.json has incorrect name")
|
||||
except Exception as e:
|
||||
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.") from e
|
||||
except:
|
||||
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.")
|
||||
return mod_path
|
||||
|
||||
|
||||
@ -28,15 +28,16 @@ def copy_0ad_files(path_0ad: Path, to_copy=DEFAULT_COPY):
|
||||
shutil.copytree(path_0ad / path, MOD_PATH / path, dirs_exist_ok=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Copy files from 0 A.D. to the community mod.")
|
||||
parser.add_argument("-0ad", help="Path to the 0 A.D. folder")
|
||||
parser.add_argument("-p", "--path", nargs="*", help="Optionally, a list of paths to copy.")
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Copy files from 0 A.D. to the community mod.')
|
||||
parser.add_argument('-0ad', help='Path to the 0 A.D. folder')
|
||||
parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of paths to copy.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
check_cwd_is_correct()
|
||||
path = validate_path(getattr(args, "0ad"))
|
||||
path = validate_path(getattr(args, '0ad'))
|
||||
copy_0ad_files(path, args.folder or DEFAULT_COPY)
|
||||
else:
|
||||
raise Exception("Must be called directly")
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import logging
|
||||
from os import chdir
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError, run
|
||||
from subprocess import run, CalledProcessError
|
||||
from sys import exit
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from xml.etree import ElementTree
|
||||
from .scriptlib import SimulTemplateEntity, find_files
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -16,54 +15,37 @@ relaxng_schema = root / "scripts" / "entity.rng"
|
||||
vfs_root = root
|
||||
mod = "community-mod"
|
||||
|
||||
|
||||
def main():
|
||||
if not relaxng_schema.exists():
|
||||
print(f"""Relax NG schema non existant.
|
||||
Please create the file {relaxng_schema.relative_to(root)}
|
||||
You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""")
|
||||
exit(1)
|
||||
if run(["xmllint", "--version"], capture_output=True, check=False).returncode != 0:
|
||||
if run(['xmllint', '--version'], capture_output=True).returncode != 0:
|
||||
print("xmllint not found in your PATH, please install it (usually in libxml2 package)")
|
||||
exit(2)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Validate templates")
|
||||
parser.add_argument(
|
||||
"-p", "--path", nargs="*", help="Optionally, a list of templates to validate."
|
||||
)
|
||||
parser = argparse.ArgumentParser(description='Validate templates')
|
||||
parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of templates to validate.')
|
||||
args = parser.parse_args()
|
||||
|
||||
simul_templates_path = Path("simulation/templates")
|
||||
simul_templates_path = Path('simulation/templates')
|
||||
simul_template_entity = SimulTemplateEntity(vfs_root, logger)
|
||||
count = 0
|
||||
failed = 0
|
||||
|
||||
templates = []
|
||||
if args.path is not None:
|
||||
templates = sorted([(Path(p), None) for p in args.path])
|
||||
else:
|
||||
templates = sorted(find_files(vfs_root, [mod], "simulation/templates", "xml"))
|
||||
|
||||
templates = sorted([(Path(p), None) for p in args.path]) if args.path else sorted(find_files(vfs_root, [mod], 'simulation/templates', 'xml'))
|
||||
for fp, _ in templates:
|
||||
if fp.stem.startswith("template_"):
|
||||
if fp.stem.startswith('template_'):
|
||||
continue
|
||||
path = fp.as_posix()
|
||||
if path.startswith(("simulation/templates/mixins/", "simulation/templates/special/")):
|
||||
if path.startswith('simulation/templates/mixins/') or path.startswith("simulation/templates/special/"):
|
||||
continue
|
||||
print(f"# {fp}...")
|
||||
count += 1
|
||||
entity = simul_template_entity.load_inherited(
|
||||
simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod]
|
||||
)
|
||||
xmlcontent = ET.tostring(entity, encoding="unicode")
|
||||
entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod])
|
||||
xmlcontent = ElementTree.tostring(entity, encoding='unicode')
|
||||
try:
|
||||
run(
|
||||
["xmllint", "--relaxng", str(relaxng_schema.resolve()), "-"],
|
||||
input=xmlcontent,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
run(['xmllint', '--relaxng', str(relaxng_schema.resolve()), '-'], input=xmlcontent, capture_output=True, text=True, check=True)
|
||||
except CalledProcessError as e:
|
||||
failed += 1
|
||||
print(e.stderr)
|
||||
@ -71,5 +53,5 @@ You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""
|
||||
print(f"\nTotal: {count}; failed: {failed}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -1,15 +1,12 @@
|
||||
import json
|
||||
|
||||
from . import MOD_PATH, check_cwd_is_correct
|
||||
|
||||
|
||||
def get_version():
|
||||
with open(MOD_PATH / "mod.json") as f:
|
||||
with open(MOD_PATH / "mod.json", "r") as f:
|
||||
mod = json.load(f)
|
||||
print(mod["version"])
|
||||
print(mod['version'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
check_cwd_is_correct()
|
||||
get_version()
|
||||
else:
|
||||
|
@ -1,42 +1,36 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
|
||||
from . import MOD_PATH
|
||||
|
||||
import subprocess
|
||||
|
||||
def run_git_command(command):
|
||||
""" Run a git command and return its output. """
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"Git command failed: {result.stderr}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_current_commit():
|
||||
""" Get the current commit hash. """
|
||||
return run_git_command(["git", "rev-parse", "HEAD"])
|
||||
|
||||
return run_git_command(['git', 'rev-parse', 'HEAD'])
|
||||
|
||||
def get_commit_for_tag(tag):
|
||||
""" Get the commit hash for a tag, or return None if the tag doesn't exist. """
|
||||
try:
|
||||
return run_git_command(["git", "rev-list", "-n", "1", tag])
|
||||
except Exception as e:
|
||||
print(f"Failed to get commit for tag {tag}: {e}")
|
||||
commit_hash = run_git_command(['git', 'rev-list', '-n', '1', tag])
|
||||
return commit_hash
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_previous_tag_in_series(tag):
|
||||
""" Get the previous tag in the same minor version series (e.g., 0.26.X-1). """
|
||||
try:
|
||||
version_array = tag.split(".")
|
||||
version_array = tag.split('.')
|
||||
# Extract the major.minor part of the tag (e.g., '0.26' from '0.26.11')
|
||||
major_minor_version = ".".join(version_array[:2])
|
||||
major_minor_version = '.'.join(version_array[:2])
|
||||
# List all tags, reverse sorted by version number
|
||||
tags = run_git_command(["git", "tag", "--sort=-v:refname"]).splitlines()
|
||||
tags = run_git_command(['git', 'tag', '--sort=-v:refname']).splitlines()
|
||||
# Filter tags that match the major.minor version
|
||||
filtered_tags = [t for t in tags if t.startswith(major_minor_version)]
|
||||
if tag in filtered_tags:
|
||||
@ -45,30 +39,28 @@ def get_previous_tag_in_series(tag):
|
||||
return filtered_tags[current_index + 1]
|
||||
|
||||
version_array[2] = str(int(version_array[2]) - 1)
|
||||
manual_tag = ".".join(version_array)
|
||||
if manual_tag not in filtered_tags:
|
||||
return None
|
||||
|
||||
manual_tag = '.'.join(version_array)
|
||||
if manual_tag in filtered_tags:
|
||||
current_index = filtered_tags.index(manual_tag)
|
||||
return filtered_tags[current_index]
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to get previous tag in series: {e}") from e
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to get previous tag in series: {e}")
|
||||
|
||||
def get_changelog(current_commit, previous_tag):
|
||||
""" Get the changelog between the current commit and the previous tag. """
|
||||
try:
|
||||
if previous_tag is not None:
|
||||
return run_git_command(
|
||||
["git", "log", f"{previous_tag}..{current_commit}", "--oneline"]
|
||||
)
|
||||
changelog = run_git_command(['git', 'log', f'{previous_tag}..{current_commit}', '--oneline'])
|
||||
else:
|
||||
# No previous tag, so get the log from the start of the repository
|
||||
return run_git_command(["git", "log", current_commit, "--oneline"])
|
||||
changelog = run_git_command(['git', 'log', current_commit, '--oneline'])
|
||||
return changelog
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to generate changelog: {e}") from e
|
||||
raise Exception(f"Failed to generate changelog: {e}")
|
||||
|
||||
|
||||
api_key = os.getenv("MODIO_API_KEY")
|
||||
api_key = os.getenv('MODIO_API_KEY')
|
||||
|
||||
# This must be generated manually from the website's interface.
|
||||
oauth2_token = os.getenv("MODIO_OAUTH2_TOKEN")
|
||||
@ -80,11 +72,9 @@ mod_name = os.getenv("MOD_NAME")
|
||||
community_mod_id = 2144305
|
||||
zeroad_id = 5
|
||||
|
||||
mod_json = None
|
||||
with open(MOD_PATH / "mod.json") as f:
|
||||
mod_json = json.load(f)
|
||||
mod_json = json.load(open(MOD_PATH / 'mod.json', 'r'))
|
||||
|
||||
tag = mod_json["version"]
|
||||
tag = mod_json['version']
|
||||
commit = get_commit_for_tag(tag)
|
||||
if not commit:
|
||||
print(f"Tag {tag} does not exist. Using current commit as fallback.")
|
||||
@ -93,35 +83,29 @@ if not commit:
|
||||
previous_tag = get_previous_tag_in_series(tag)
|
||||
changelog = get_changelog(commit, previous_tag)
|
||||
|
||||
print(changelog)
|
||||
headers = {
|
||||
'Authorization': f'Bearer {oauth2_token}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
headers = {"Authorization": f"Bearer {oauth2_token}", "Accept": "application/json"}
|
||||
|
||||
r = requests.get(
|
||||
"https://api.mod.io/v1/me/mods", timeout=5, params={"api_key": api_key}, headers=headers
|
||||
)
|
||||
r = requests.get(f'https://api.mod.io/v1/me/mods', params={
|
||||
'api_key': api_key
|
||||
}, headers = headers)
|
||||
|
||||
print(r.json())
|
||||
|
||||
with open(mod_file_path, "rb") as f:
|
||||
files = {"filedata": f}
|
||||
signature = ""
|
||||
with open(f"{mod_name}-{mod_version}.zip.minisign", encoding="utf-8") as f:
|
||||
signature = f.read()
|
||||
files = {'filedata': open(mod_file_path, 'rb')}
|
||||
|
||||
rq = requests.post(
|
||||
f"https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files",
|
||||
files=files,
|
||||
headers=headers,
|
||||
timeout=500,
|
||||
data={
|
||||
"version": mod_version,
|
||||
"active": True,
|
||||
"changelog": changelog,
|
||||
"metadata_blob": json.dumps(
|
||||
{"dependencies": mod_json["dependencies"], "minisigs": [signature]}
|
||||
),
|
||||
},
|
||||
)
|
||||
signature = open(f"{mod_name}-{mod_version}.zip.minisign", 'r', encoding="utf-8").read()
|
||||
|
||||
rq = requests.post(f'https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files', files=files, headers=headers, data={
|
||||
'version': mod_version,
|
||||
'active': True,
|
||||
'changelog': changelog,
|
||||
'metadata_blob' : json.dumps({
|
||||
'dependencies': mod_json['dependencies'],
|
||||
'minisigs': [signature]
|
||||
})
|
||||
})
|
||||
|
||||
print(rq.json())
|
@ -1,9 +1,9 @@
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
from os.path import exists
|
||||
from re import split
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from sys import stderr
|
||||
from xml.etree import ElementTree
|
||||
from os.path import exists
|
||||
|
||||
class SimulTemplateEntity:
|
||||
def __init__(self, vfs_root, logger):
|
||||
@ -12,11 +12,11 @@ class SimulTemplateEntity:
|
||||
|
||||
def get_file(self, base_path, vfs_path, mod):
|
||||
default_path = self.vfs_root / mod / base_path
|
||||
file = (default_path / "special" / "filter" / vfs_path).with_suffix(".xml")
|
||||
file = (default_path/ "special" / "filter" / vfs_path).with_suffix('.xml')
|
||||
if not exists(file):
|
||||
file = (default_path / "mixins" / vfs_path).with_suffix(".xml")
|
||||
file = (default_path / "mixins" / vfs_path).with_suffix('.xml')
|
||||
if not exists(file):
|
||||
file = (default_path / vfs_path).with_suffix(".xml")
|
||||
file = (default_path / vfs_path).with_suffix('.xml')
|
||||
return file
|
||||
|
||||
def get_main_mod(self, base_path, vfs_path, mods):
|
||||
@ -33,53 +33,55 @@ class SimulTemplateEntity:
|
||||
return main_mod
|
||||
|
||||
def apply_layer(self, base_tag, tag):
|
||||
"""Apply tag layer to base_tag."""
|
||||
if tag.get("datatype") == "tokens":
|
||||
base_tokens = split(r"\s+", base_tag.text or "")
|
||||
tokens = split(r"\s+", tag.text or "")
|
||||
"""
|
||||
apply tag layer to base_tag
|
||||
"""
|
||||
if tag.get('datatype') == 'tokens':
|
||||
base_tokens = split(r'\s+', base_tag.text or '')
|
||||
tokens = split(r'\s+', tag.text or '')
|
||||
final_tokens = base_tokens.copy()
|
||||
for token in tokens:
|
||||
if token.startswith("-"):
|
||||
if token.startswith('-'):
|
||||
token_to_remove = token[1:]
|
||||
if token_to_remove in final_tokens:
|
||||
final_tokens.remove(token_to_remove)
|
||||
elif token not in final_tokens:
|
||||
final_tokens.append(token)
|
||||
base_tag.text = " ".join(final_tokens)
|
||||
base_tag.text = ' '.join(final_tokens)
|
||||
base_tag.set("datatype", "tokens")
|
||||
elif tag.get("op"):
|
||||
op = tag.get("op")
|
||||
op1 = Decimal(base_tag.text or "0")
|
||||
op2 = Decimal(tag.text or "0")
|
||||
elif tag.get('op'):
|
||||
op = tag.get('op')
|
||||
op1 = Decimal(base_tag.text or '0')
|
||||
op2 = Decimal(tag.text or '0')
|
||||
# Try converting to integers if possible, to pass validation.
|
||||
if op == "add":
|
||||
if op == 'add':
|
||||
base_tag.text = str(int(op1 + op2) if int(op1 + op2) == op1 + op2 else op1 + op2)
|
||||
elif op == "mul":
|
||||
elif op == 'mul':
|
||||
base_tag.text = str(int(op1 * op2) if int(op1 * op2) == op1 * op2 else op1 * op2)
|
||||
elif op == "mul_round":
|
||||
elif op == 'mul_round':
|
||||
base_tag.text = str(round(op1 * op2))
|
||||
else:
|
||||
raise ValueError(f"Invalid operator '{op}'")
|
||||
else:
|
||||
base_tag.text = tag.text
|
||||
for prop in tag.attrib:
|
||||
if prop not in ("disable", "replace", "parent", "merge"):
|
||||
if prop not in ('disable', 'replace', 'parent', 'merge'):
|
||||
base_tag.set(prop, tag.get(prop))
|
||||
for child in tag:
|
||||
base_child = base_tag.find(child.tag)
|
||||
if "disable" in child.attrib:
|
||||
if 'disable' in child.attrib:
|
||||
if base_child is not None:
|
||||
base_tag.remove(base_child)
|
||||
elif ("merge" not in child.attrib) or (base_child is not None):
|
||||
if "replace" in child.attrib and base_child is not None:
|
||||
elif ('merge' not in child.attrib) or (base_child is not None):
|
||||
if 'replace' in child.attrib and base_child is not None:
|
||||
base_tag.remove(base_child)
|
||||
base_child = None
|
||||
if base_child is None:
|
||||
base_child = ET.Element(child.tag)
|
||||
base_child = ElementTree.Element(child.tag)
|
||||
base_tag.append(base_child)
|
||||
self.apply_layer(base_child, child)
|
||||
if "replace" in base_child.attrib:
|
||||
del base_child.attrib["replace"]
|
||||
if 'replace' in base_child.attrib:
|
||||
del base_child.attrib['replace']
|
||||
|
||||
def load_inherited(self, base_path, vfs_path, mods):
|
||||
entity = self._load_inherited(base_path, vfs_path, mods)
|
||||
@ -87,56 +89,51 @@ class SimulTemplateEntity:
|
||||
return entity
|
||||
|
||||
def _load_inherited(self, base_path, vfs_path, mods, base = None):
|
||||
"""vfs_path should be relative to base_path in a mod."""
|
||||
if "|" in vfs_path:
|
||||
"""
|
||||
vfs_path should be relative to base_path in a mod
|
||||
"""
|
||||
if '|' in vfs_path:
|
||||
paths = vfs_path.split("|", 1)
|
||||
self._load_inherited(base_path, paths[1], mods, base)
|
||||
return self._load_inherited(base_path, paths[0], mods, base)
|
||||
base = self._load_inherited(base_path, paths[1], mods, base);
|
||||
base = self._load_inherited(base_path, paths[0], mods, base);
|
||||
return base
|
||||
|
||||
main_mod = self.get_main_mod(base_path, vfs_path, mods)
|
||||
fp = self.get_file(base_path, vfs_path, main_mod)
|
||||
layer = ET.parse(fp).getroot()
|
||||
layer = ElementTree.parse(fp).getroot()
|
||||
for el in layer.iter():
|
||||
children = [x.tag for x in el]
|
||||
duplicates = [x for x, c in Counter(children).items() if c > 1]
|
||||
if duplicates:
|
||||
for dup in duplicates:
|
||||
self.logger.warning(
|
||||
"Duplicate child node '%s' in tag %s of %s", dup, el.tag, fp
|
||||
)
|
||||
if layer.get("parent"):
|
||||
parent = self._load_inherited(base_path, layer.get("parent"), mods, base)
|
||||
self.logger.warning(f"Duplicate child node '{dup}' in tag {el.tag} of {fp}")
|
||||
if layer.get('parent'):
|
||||
parent = self._load_inherited(base_path, layer.get('parent'), mods, base)
|
||||
self.apply_layer(parent, layer)
|
||||
return parent
|
||||
|
||||
else:
|
||||
if not base:
|
||||
return layer
|
||||
|
||||
else:
|
||||
self.apply_layer(base, layer)
|
||||
return base
|
||||
|
||||
|
||||
def find_files(vfs_root, mods, vfs_path, *ext_list):
|
||||
"""Return a list of 2-size tuples.
|
||||
|
||||
Each tuple contains:
|
||||
"""
|
||||
returns a list of 2-size tuple with:
|
||||
- Path relative to the mod base
|
||||
- full Path
|
||||
"""
|
||||
full_exts = ["." + ext for ext in ext_list]
|
||||
full_exts = ['.' + ext for ext in ext_list]
|
||||
|
||||
def find_recursive(dp, base):
|
||||
"""(relative Path, full Path) generator."""
|
||||
"""(relative Path, full Path) generator"""
|
||||
if dp.is_dir():
|
||||
if dp.name not in (".svn", ".git") and not dp.name.endswith("~"):
|
||||
if dp.name != '.svn' and dp.name != '.git' and not dp.name.endswith('~'):
|
||||
for fp in dp.iterdir():
|
||||
yield from find_recursive(fp, base)
|
||||
elif dp.suffix in full_exts:
|
||||
relative_file_path = dp.relative_to(base)
|
||||
yield (relative_file_path, dp.resolve())
|
||||
|
||||
return [
|
||||
(rp, fp)
|
||||
for mod in mods
|
||||
for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)
|
||||
]
|
||||
return [(rp, fp) for mod in mods for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)]
|
||||
|
Loading…
Reference in New Issue
Block a user