Compare commits
No commits in common. "main" and "main" have entirely different histories.
@ -11,6 +11,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
fetch-depth: 0
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
- run: apt-get update && apt-get install libxml2-utils -qqy
|
- run: apt-get update && apt-get install libxml2-utils -qqy
|
||||||
- name: Fetch main branch
|
- name: Fetch main branch
|
||||||
@ -50,7 +51,6 @@ jobs:
|
|||||||
if: github.ref == 'refs/heads/signed' && github.event_name == 'push'
|
if: github.ref == 'refs/heads/signed' && github.event_name == 'push'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
- run: pip3 install requests
|
- run: pip3 install requests
|
||||||
- name: Install minisign
|
- name: Install minisign
|
||||||
|
@ -16,15 +16,3 @@ repos:
|
|||||||
- id: yamllint
|
- id: yamllint
|
||||||
args:
|
args:
|
||||||
- -s
|
- -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-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"]
|
[font="sans-bold-20"]
|
||||||
#Version 11 (September 9, 2024)
|
#Version 11 (September 9, 2024)
|
||||||
[font="sans-16"]
|
[font="sans-16"]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "community-mod",
|
"name": "community-mod",
|
||||||
"version": "0.26.12",
|
"version": "0.26.11",
|
||||||
"label": "0 A.D. Community Mod",
|
"label": "0 A.D. Community Mod",
|
||||||
"url": "https://gitlab.com/0ad/0ad-community-mod-a26",
|
"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.",
|
"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
|
* (template name should be "preview|"+templateName), as otherwise territory
|
||||||
* checks for buildings with territory influence will not work as expected.
|
* 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 cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
|
||||||
var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building";
|
var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building";
|
||||||
|
|
||||||
@ -101,8 +100,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
return result; // Fail
|
return result; // Fail
|
||||||
|
|
||||||
// TODO: AI has no visibility info
|
// TODO: AI has no visibility info
|
||||||
if (!cmpPlayer.IsAI())
|
if (!cmpPlayer.IsAI()) {
|
||||||
{
|
|
||||||
// Check whether it's in a visible or fogged region
|
// Check whether it's in a visible or fogged region
|
||||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||||
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
||||||
@ -110,8 +108,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
return result; // Fail
|
return result; // Fail
|
||||||
|
|
||||||
var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden");
|
var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden");
|
||||||
if (!explored)
|
if (!explored) {
|
||||||
{
|
|
||||||
result.message = markForTranslation("%(name)s cannot be built in unexplored area");
|
result.message = markForTranslation("%(name)s cannot be built in unexplored area");
|
||||||
return result; // Fail
|
return result; // Fail
|
||||||
}
|
}
|
||||||
@ -119,21 +116,20 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
|
|
||||||
// Check obstructions and terrain passability
|
// Check obstructions and terrain passability
|
||||||
var passClassName = "";
|
var passClassName = "";
|
||||||
switch (this.template.PlacementType)
|
switch (this.template.PlacementType) {
|
||||||
{
|
case "shore":
|
||||||
case "shore":
|
passClassName = "building-shore";
|
||||||
passClassName = "building-shore";
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
case "land-shore":
|
case "land-shore":
|
||||||
// 'default-terrain-only' is everywhere a normal unit can go, ignoring
|
// 'default-terrain-only' is everywhere a normal unit can go, ignoring
|
||||||
// obstructions (i.e. on passable land, and not too deep in the water)
|
// obstructions (i.e. on passable land, and not too deep in the water)
|
||||||
passClassName = "default-terrain-only";
|
passClassName = "default-terrain-only";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "land":
|
case "land":
|
||||||
default:
|
default:
|
||||||
passClassName = "building-land";
|
passClassName = "building-land";
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
||||||
@ -141,30 +137,38 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
return result; // Fail
|
return result; // Fail
|
||||||
|
|
||||||
|
|
||||||
if (this.template.Category == "Wall")
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
// for walls, only test the center point
|
// for walls, only test the center point
|
||||||
var ret = cmpObstruction.CheckFoundation(passClassName, true);
|
var ret = areWoodEnts ? "success" : cmpObstruction.CheckFoundation(passClassName, true);
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
var ret = cmpObstruction.CheckFoundation(passClassName, false);
|
var ret = cmpObstruction.CheckFoundation(passClassName, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret != "success")
|
if (ret != "success") {
|
||||||
{
|
switch (ret) {
|
||||||
switch (ret)
|
case "fail_error":
|
||||||
{
|
case "fail_no_obstruction":
|
||||||
case "fail_error":
|
error("CheckPlacement: Error returned from CheckFoundation");
|
||||||
case "fail_no_obstruction":
|
break;
|
||||||
error("CheckPlacement: Error returned from CheckFoundation");
|
case "fail_obstructs_foundation":
|
||||||
break;
|
result.message = markForTranslation("%(name)s cannot be built on another building or resource");
|
||||||
case "fail_obstructs_foundation":
|
break;
|
||||||
result.message = markForTranslation("%(name)s cannot be built on another building or resource");
|
case "fail_terrain_class":
|
||||||
break;
|
// TODO: be more specific and/or list valid terrain?
|
||||||
case "fail_terrain_class":
|
result.message = markForTranslation("%(name)s cannot be built on invalid terrain");
|
||||||
// TODO: be more specific and/or list valid terrain?
|
|
||||||
result.message = markForTranslation("%(name)s cannot be built on invalid terrain");
|
|
||||||
}
|
}
|
||||||
return result; // Fail
|
return result; // Fail
|
||||||
}
|
}
|
||||||
@ -183,8 +187,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
var isNeutral = tileOwner == 0;
|
var isNeutral = tileOwner == 0;
|
||||||
|
|
||||||
var invalidTerritory = "";
|
var invalidTerritory = "";
|
||||||
if (isOwn)
|
if (isOwn) {
|
||||||
{
|
|
||||||
if (!this.HasTerritory("own"))
|
if (!this.HasTerritory("own"))
|
||||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||||
invalidTerritory = markForTranslationWithContext("Territory type", "own");
|
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.".
|
// 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");
|
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own");
|
||||||
}
|
}
|
||||||
else if (isMutualAlly)
|
else if (isMutualAlly) {
|
||||||
{
|
|
||||||
if (!this.HasTerritory("ally"))
|
if (!this.HasTerritory("ally"))
|
||||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||||
invalidTerritory = markForTranslationWithContext("Territory type", "allied");
|
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.".
|
// 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");
|
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied");
|
||||||
}
|
}
|
||||||
else if (isNeutral)
|
else if (isNeutral) {
|
||||||
{
|
|
||||||
if (!this.HasTerritory("neutral"))
|
if (!this.HasTerritory("neutral"))
|
||||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||||
invalidTerritory = markForTranslationWithContext("Territory type", "neutral");
|
invalidTerritory = markForTranslationWithContext("Territory type", "neutral");
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
// consider everything else enemy territory
|
// consider everything else enemy territory
|
||||||
if (!this.HasTerritory("enemy"))
|
if (!this.HasTerritory("enemy"))
|
||||||
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
|
||||||
invalidTerritory = markForTranslationWithContext("Territory type", "enemy");
|
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.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s");
|
||||||
result.translateParameters.push("territoryType");
|
result.translateParameters.push("territoryType");
|
||||||
result.translateParameters.push("validTerritories");
|
result.translateParameters.push("validTerritories");
|
||||||
@ -227,10 +226,8 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check special requirements
|
// Check special requirements
|
||||||
if (this.template.PlacementType == "shore")
|
if (this.template.PlacementType == "shore") {
|
||||||
{
|
if (!cmpObstruction.CheckShorePlacement()) {
|
||||||
if (!cmpObstruction.CheckShorePlacement())
|
|
||||||
{
|
|
||||||
result.message = markForTranslation("%(name)s must be built on a valid shoreline");
|
result.message = markForTranslation("%(name)s must be built on a valid shoreline");
|
||||||
return result; // Fail
|
return result; // Fail
|
||||||
}
|
}
|
||||||
@ -242,22 +239,18 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName));
|
let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName));
|
||||||
|
|
||||||
// Check distance restriction
|
// Check distance restriction
|
||||||
if (this.template.Distance)
|
if (this.template.Distance) {
|
||||||
{
|
|
||||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||||
var cat = this.template.Distance.FromClass;
|
var cat = this.template.Distance.FromClass;
|
||||||
|
|
||||||
var filter = function(id)
|
var filter = function (id) {
|
||||||
{
|
|
||||||
var cmpIdentity = Engine.QueryInterface(id, IID_Identity);
|
var cmpIdentity = Engine.QueryInterface(id, IID_Identity);
|
||||||
return cmpIdentity.GetClassesList().indexOf(cat) > -1;
|
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);
|
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(
|
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 meter away",
|
||||||
"%(name)s too close to a %(category)s, must be at least %(distance)s meters 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
|
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);
|
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(
|
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 meter",
|
||||||
"%(name)s too far from a %(category)s, must be within %(distance)s meters",
|
"%(name)s too far from a %(category)s, must be within %(distance)s meters",
|
||||||
@ -303,6 +294,11 @@ BuildRestrictions.prototype.CheckPlacement = function()
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BuildRestrictions.prototype.IsWoodEntity = function (entity) {
|
||||||
|
let cmpResource = Engine.QueryInterface(entity, IID_ResourceSupply);
|
||||||
|
return cmpResource && cmpResource.GetType().generic === "wood";
|
||||||
|
}
|
||||||
|
|
||||||
BuildRestrictions.prototype.GetCategory = function()
|
BuildRestrictions.prototype.GetCategory = function()
|
||||||
{
|
{
|
||||||
return this.template.Category;
|
return this.template.Category;
|
||||||
|
@ -5,8 +5,7 @@ Foundation.prototype.Schema =
|
|||||||
"<ref name='nonNegativeDecimal'/>" +
|
"<ref name='nonNegativeDecimal'/>" +
|
||||||
"</element>";
|
"</element>";
|
||||||
|
|
||||||
Foundation.prototype.Init = function()
|
Foundation.prototype.Init = function () {
|
||||||
{
|
|
||||||
// Foundations are initially 'uncommitted' and do not block unit movement at all
|
// Foundations are initially 'uncommitted' and do not block unit movement at all
|
||||||
// (to prevent players exploiting free foundations to confuse enemy units).
|
// (to prevent players exploiting free foundations to confuse enemy units).
|
||||||
// The first builder to reach the uncommitted foundation will tell friendly 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.buildTimeModifier = +this.template.BuildTimeModifier;
|
||||||
|
|
||||||
this.previewEntity = INVALID_ENTITY;
|
this.previewEntity = INVALID_ENTITY;
|
||||||
|
this.entsToDestroy;
|
||||||
};
|
};
|
||||||
|
|
||||||
Foundation.prototype.Serialize = function()
|
Foundation.prototype.Serialize = function()
|
||||||
@ -101,9 +101,12 @@ Foundation.prototype.GetNumBuilders = function()
|
|||||||
return this.builders.size;
|
return this.builders.size;
|
||||||
};
|
};
|
||||||
|
|
||||||
Foundation.prototype.IsFinished = function()
|
Foundation.prototype.IsFinished = function () {
|
||||||
{
|
if (this.GetBuildProgress() == 1.0 && this.entsToDestroy)
|
||||||
return (this.GetBuildProgress() == 1.0);
|
for (let ent of this.entsToDestroy)
|
||||||
|
Engine.DestroyEntity(ent);
|
||||||
|
|
||||||
|
return this.GetBuildProgress() == 1.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
Foundation.prototype.OnOwnershipChanged = function(msg)
|
Foundation.prototype.OnOwnershipChanged = function(msg)
|
||||||
@ -246,22 +249,18 @@ Foundation.prototype.GetBuildTime = function()
|
|||||||
/**
|
/**
|
||||||
* @return {boolean} - Whether the foundation has been committed sucessfully.
|
* @return {boolean} - Whether the foundation has been committed sucessfully.
|
||||||
*/
|
*/
|
||||||
Foundation.prototype.Commit = function()
|
Foundation.prototype.Commit = function () {
|
||||||
{
|
|
||||||
if (this.committed)
|
if (this.committed)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
||||||
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
||||||
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
|
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true)) {
|
||||||
{
|
this.entsToDestroy = cmpObstruction.GetEntitiesDeletedUponConstruction();
|
||||||
for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
|
|
||||||
Engine.DestroyEntity(ent);
|
|
||||||
|
|
||||||
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
|
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
|
||||||
if (collisions.length)
|
if (collisions.length) {
|
||||||
{
|
for (let ent of collisions) {
|
||||||
for (let ent of collisions)
|
|
||||||
{
|
|
||||||
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
||||||
if (cmpUnitAI)
|
if (cmpUnitAI)
|
||||||
cmpUnitAI.LeaveFoundation(this.entity);
|
cmpUnitAI.LeaveFoundation(this.entity);
|
||||||
@ -273,10 +272,10 @@ Foundation.prototype.Commit = function()
|
|||||||
// animation to indicate they're waiting for people to get
|
// animation to indicate they're waiting for people to get
|
||||||
// out the way
|
// out the way
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The obstruction always blocks new foundations/construction,
|
// The obstruction always blocks new foundations/construction,
|
||||||
// but we've temporarily allowed units to walk all over it
|
// but we've temporarily allowed units to walk all over it
|
||||||
// (via CCmpTemplateManager). Now we need to remove that temporary
|
// (via CCmpTemplateManager). Now we need to remove that temporary
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<Rank>Elite</Rank>
|
<Rank>Elite</Rank>
|
||||||
</Identity>
|
</Identity>
|
||||||
<Promotion>
|
<Promotion>
|
||||||
<RequiredXp>400</RequiredXp>
|
<RequiredXp>200</RequiredXp>
|
||||||
<Entity>units/athen/champion_infantry</Entity>
|
<Entity>units/athen/champion_infantry</Entity>
|
||||||
</Promotion>
|
</Promotion>
|
||||||
<VisualActor>
|
<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
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
MOD_PATH = Path("community-mod")
|
MOD_PATH = Path("community-mod")
|
||||||
|
|
||||||
|
|
||||||
def check_cwd_is_correct():
|
def check_cwd_is_correct():
|
||||||
try:
|
cwd = Path().resolve()
|
||||||
if not Path(".git").exists():
|
try:
|
||||||
raise Exception("No .git in current folder.")
|
if not Path(".git").exists():
|
||||||
with open(MOD_PATH / "mod.json") as f:
|
raise Exception("No .git in current folder.")
|
||||||
mod = json.load(f)
|
with open(MOD_PATH / "mod.json", "r") as f:
|
||||||
if mod["name"] != "community-mod":
|
mod = json.load(f)
|
||||||
raise Exception("mod.json has incorrect name")
|
if mod['name'] != "community-mod":
|
||||||
except Exception as e:
|
raise Exception("mod.json has incorrect name")
|
||||||
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,42 +1,43 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import MOD_PATH, check_cwd_is_correct
|
from . import MOD_PATH, check_cwd_is_correct
|
||||||
|
|
||||||
|
|
||||||
PUBLIC_PATH = Path("binaries/data/mods/public/")
|
PUBLIC_PATH = Path("binaries/data/mods/public/")
|
||||||
|
|
||||||
DEFAULT_COPY = ["simulation/data", "simulation/templates"]
|
DEFAULT_COPY = [
|
||||||
|
"simulation/data",
|
||||||
|
"simulation/templates"
|
||||||
|
]
|
||||||
|
|
||||||
def validate_path(path: str):
|
def validate_path(path: str):
|
||||||
mod_path = Path(path) / PUBLIC_PATH
|
mod_path = Path(path) / PUBLIC_PATH
|
||||||
try:
|
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)
|
mod = json.load(f)
|
||||||
if mod["name"] != "0ad":
|
if mod['name'] != "0ad":
|
||||||
raise Exception("mod.json has incorrect name")
|
raise Exception("mod.json has incorrect name")
|
||||||
except Exception as e:
|
except:
|
||||||
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.") from e
|
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.")
|
||||||
return mod_path
|
return mod_path
|
||||||
|
|
||||||
|
|
||||||
def copy_0ad_files(path_0ad: Path, to_copy=DEFAULT_COPY):
|
def copy_0ad_files(path_0ad: Path, to_copy = DEFAULT_COPY):
|
||||||
for path in to_copy:
|
for path in to_copy:
|
||||||
shutil.copytree(path_0ad / path, MOD_PATH / path, dirs_exist_ok=False)
|
shutil.copytree(path_0ad / path, MOD_PATH / path, dirs_exist_ok=False)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description="Copy files from 0 A.D. to the community mod.")
|
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('-0ad', help='Path to the 0 A.D. folder')
|
||||||
parser.add_argument("-p", "--path", nargs="*", help="Optionally, a list of paths to copy.")
|
parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of paths to copy.')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
check_cwd_is_correct()
|
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)
|
copy_0ad_files(path, args.folder or DEFAULT_COPY)
|
||||||
else:
|
else:
|
||||||
raise Exception("Must be called directly")
|
raise Exception("Must be called directly")
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
from os import chdir
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import CalledProcessError, run
|
from subprocess import run, CalledProcessError
|
||||||
from sys import exit
|
from sys import exit
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
from .scriptlib import SimulTemplateEntity, find_files
|
from .scriptlib import SimulTemplateEntity, find_files
|
||||||
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -16,54 +15,37 @@ relaxng_schema = root / "scripts" / "entity.rng"
|
|||||||
vfs_root = root
|
vfs_root = root
|
||||||
mod = "community-mod"
|
mod = "community-mod"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if not relaxng_schema.exists():
|
if not relaxng_schema.exists():
|
||||||
print(f"""Relax NG schema non existant.
|
print(f"""Relax NG schema non existant.
|
||||||
Please create the file {relaxng_schema.relative_to(root)}
|
Please create the file {relaxng_schema.relative_to(root)}
|
||||||
You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""")
|
You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""")
|
||||||
exit(1)
|
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)")
|
print("xmllint not found in your PATH, please install it (usually in libxml2 package)")
|
||||||
exit(2)
|
exit(2)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Validate templates")
|
parser = argparse.ArgumentParser(description='Validate templates')
|
||||||
parser.add_argument(
|
parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of templates to validate.')
|
||||||
"-p", "--path", nargs="*", help="Optionally, a list of templates to validate."
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
simul_templates_path = Path("simulation/templates")
|
simul_templates_path = Path('simulation/templates')
|
||||||
simul_template_entity = SimulTemplateEntity(vfs_root, logger)
|
simul_template_entity = SimulTemplateEntity(vfs_root, logger)
|
||||||
count = 0
|
count = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
|
templates = sorted([(Path(p), None) for p in args.path]) if args.path else sorted(find_files(vfs_root, [mod], 'simulation/templates', 'xml'))
|
||||||
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"))
|
|
||||||
|
|
||||||
for fp, _ in templates:
|
for fp, _ in templates:
|
||||||
if fp.stem.startswith("template_"):
|
if fp.stem.startswith('template_'):
|
||||||
continue
|
continue
|
||||||
path = fp.as_posix()
|
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
|
continue
|
||||||
print(f"# {fp}...")
|
print(f"# {fp}...")
|
||||||
count += 1
|
count += 1
|
||||||
entity = simul_template_entity.load_inherited(
|
entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod])
|
||||||
simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod]
|
xmlcontent = ElementTree.tostring(entity, encoding='unicode')
|
||||||
)
|
|
||||||
xmlcontent = ET.tostring(entity, encoding="unicode")
|
|
||||||
try:
|
try:
|
||||||
run(
|
run(['xmllint', '--relaxng', str(relaxng_schema.resolve()), '-'], input=xmlcontent, capture_output=True, text=True, check=True)
|
||||||
["xmllint", "--relaxng", str(relaxng_schema.resolve()), "-"],
|
|
||||||
input=xmlcontent,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
failed += 1
|
failed += 1
|
||||||
print(e.stderr)
|
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}")
|
print(f"\nTotal: {count}; failed: {failed}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from . import MOD_PATH, check_cwd_is_correct
|
from . import MOD_PATH, check_cwd_is_correct
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
with open(MOD_PATH / "mod.json") as f:
|
with open(MOD_PATH / "mod.json", "r") as f:
|
||||||
mod = json.load(f)
|
mod = json.load(f)
|
||||||
print(mod["version"])
|
print(mod['version'])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
check_cwd_is_correct()
|
||||||
check_cwd_is_correct()
|
get_version()
|
||||||
get_version()
|
|
||||||
else:
|
else:
|
||||||
raise Exception("Must be called directly")
|
raise Exception("Must be called directly")
|
||||||
|
118
scripts/modio.py
118
scripts/modio.py
@ -1,42 +1,36 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
|
from . import MOD_PATH
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from . import MOD_PATH
|
|
||||||
|
|
||||||
|
|
||||||
def run_git_command(command):
|
def run_git_command(command):
|
||||||
"""Run a git command and return its output."""
|
""" 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:
|
if result.returncode != 0:
|
||||||
raise Exception(f"Git command failed: {result.stderr}")
|
raise Exception(f"Git command failed: {result.stderr}")
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
def get_current_commit():
|
def get_current_commit():
|
||||||
"""Get the current commit hash."""
|
""" 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):
|
def get_commit_for_tag(tag):
|
||||||
"""Get the commit hash for a tag, or return None if the tag doesn't exist."""
|
""" Get the commit hash for a tag, or return None if the tag doesn't exist. """
|
||||||
try:
|
try:
|
||||||
return run_git_command(["git", "rev-list", "-n", "1", tag])
|
commit_hash = run_git_command(['git', 'rev-list', '-n', '1', tag])
|
||||||
except Exception as e:
|
return commit_hash
|
||||||
print(f"Failed to get commit for tag {tag}: {e}")
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_previous_tag_in_series(tag):
|
def get_previous_tag_in_series(tag):
|
||||||
"""Get the previous tag in the same minor version series (e.g., 0.26.X-1)."""
|
""" Get the previous tag in the same minor version series (e.g., 0.26.X-1). """
|
||||||
try:
|
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')
|
# 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
|
# 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
|
# Filter tags that match the major.minor version
|
||||||
filtered_tags = [t for t in tags if t.startswith(major_minor_version)]
|
filtered_tags = [t for t in tags if t.startswith(major_minor_version)]
|
||||||
if tag in filtered_tags:
|
if tag in filtered_tags:
|
||||||
@ -45,30 +39,28 @@ def get_previous_tag_in_series(tag):
|
|||||||
return filtered_tags[current_index + 1]
|
return filtered_tags[current_index + 1]
|
||||||
|
|
||||||
version_array[2] = str(int(version_array[2]) - 1)
|
version_array[2] = str(int(version_array[2]) - 1)
|
||||||
manual_tag = ".".join(version_array)
|
manual_tag = '.'.join(version_array)
|
||||||
if manual_tag not in filtered_tags:
|
if manual_tag in filtered_tags:
|
||||||
return None
|
current_index = filtered_tags.index(manual_tag)
|
||||||
|
return filtered_tags[current_index]
|
||||||
current_index = filtered_tags.index(manual_tag)
|
|
||||||
return filtered_tags[current_index]
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to get previous tag in series: {e}") from e
|
raise Exception(f"Failed to get previous tag in series: {e}")
|
||||||
|
|
||||||
|
|
||||||
def get_changelog(current_commit, previous_tag):
|
def get_changelog(current_commit, previous_tag):
|
||||||
"""Get the changelog between the current commit and the previous tag."""
|
""" Get the changelog between the current commit and the previous tag. """
|
||||||
try:
|
try:
|
||||||
if previous_tag is not None:
|
if previous_tag is not None:
|
||||||
return run_git_command(
|
changelog = run_git_command(['git', 'log', f'{previous_tag}..{current_commit}', '--oneline'])
|
||||||
["git", "log", f"{previous_tag}..{current_commit}", "--oneline"]
|
else:
|
||||||
)
|
# No previous tag, so get the log from the start of the repository
|
||||||
# No previous tag, so get the log from the start of the repository
|
changelog = run_git_command(['git', 'log', current_commit, '--oneline'])
|
||||||
return run_git_command(["git", "log", current_commit, "--oneline"])
|
return changelog
|
||||||
except Exception as e:
|
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.
|
# This must be generated manually from the website's interface.
|
||||||
oauth2_token = os.getenv("MODIO_OAUTH2_TOKEN")
|
oauth2_token = os.getenv("MODIO_OAUTH2_TOKEN")
|
||||||
@ -80,11 +72,9 @@ mod_name = os.getenv("MOD_NAME")
|
|||||||
community_mod_id = 2144305
|
community_mod_id = 2144305
|
||||||
zeroad_id = 5
|
zeroad_id = 5
|
||||||
|
|
||||||
mod_json = None
|
mod_json = json.load(open(MOD_PATH / 'mod.json', 'r'))
|
||||||
with open(MOD_PATH / "mod.json") as f:
|
|
||||||
mod_json = json.load(f)
|
|
||||||
|
|
||||||
tag = mod_json["version"]
|
tag = mod_json['version']
|
||||||
commit = get_commit_for_tag(tag)
|
commit = get_commit_for_tag(tag)
|
||||||
if not commit:
|
if not commit:
|
||||||
print(f"Tag {tag} does not exist. Using current commit as fallback.")
|
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)
|
previous_tag = get_previous_tag_in_series(tag)
|
||||||
changelog = get_changelog(commit, previous_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(f'https://api.mod.io/v1/me/mods', params={
|
||||||
|
'api_key': api_key
|
||||||
r = requests.get(
|
}, headers = headers)
|
||||||
"https://api.mod.io/v1/me/mods", timeout=5, params={"api_key": api_key}, headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
print(r.json())
|
print(r.json())
|
||||||
|
|
||||||
with open(mod_file_path, "rb") as f:
|
files = {'filedata': open(mod_file_path, 'rb')}
|
||||||
files = {"filedata": f}
|
|
||||||
signature = ""
|
|
||||||
with open(f"{mod_name}-{mod_version}.zip.minisign", encoding="utf-8") as f:
|
|
||||||
signature = f.read()
|
|
||||||
|
|
||||||
rq = requests.post(
|
signature = open(f"{mod_name}-{mod_version}.zip.minisign", 'r', encoding="utf-8").read()
|
||||||
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]}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
print(rq.json())
|
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 collections import Counter
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from os.path import exists
|
|
||||||
from re import split
|
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:
|
class SimulTemplateEntity:
|
||||||
def __init__(self, vfs_root, logger):
|
def __init__(self, vfs_root, logger):
|
||||||
@ -12,11 +12,11 @@ class SimulTemplateEntity:
|
|||||||
|
|
||||||
def get_file(self, base_path, vfs_path, mod):
|
def get_file(self, base_path, vfs_path, mod):
|
||||||
default_path = self.vfs_root / mod / base_path
|
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):
|
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):
|
if not exists(file):
|
||||||
file = (default_path / vfs_path).with_suffix(".xml")
|
file = (default_path / vfs_path).with_suffix('.xml')
|
||||||
return file
|
return file
|
||||||
|
|
||||||
def get_main_mod(self, base_path, vfs_path, mods):
|
def get_main_mod(self, base_path, vfs_path, mods):
|
||||||
@ -33,110 +33,107 @@ class SimulTemplateEntity:
|
|||||||
return main_mod
|
return main_mod
|
||||||
|
|
||||||
def apply_layer(self, base_tag, tag):
|
def apply_layer(self, base_tag, tag):
|
||||||
"""Apply tag layer to base_tag."""
|
"""
|
||||||
if tag.get("datatype") == "tokens":
|
apply tag layer to base_tag
|
||||||
base_tokens = split(r"\s+", base_tag.text or "")
|
"""
|
||||||
tokens = split(r"\s+", tag.text or "")
|
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()
|
final_tokens = base_tokens.copy()
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token.startswith("-"):
|
if token.startswith('-'):
|
||||||
token_to_remove = token[1:]
|
token_to_remove = token[1:]
|
||||||
if token_to_remove in final_tokens:
|
if token_to_remove in final_tokens:
|
||||||
final_tokens.remove(token_to_remove)
|
final_tokens.remove(token_to_remove)
|
||||||
elif token not in final_tokens:
|
elif token not in final_tokens:
|
||||||
final_tokens.append(token)
|
final_tokens.append(token)
|
||||||
base_tag.text = " ".join(final_tokens)
|
base_tag.text = ' '.join(final_tokens)
|
||||||
base_tag.set("datatype", "tokens")
|
base_tag.set("datatype", "tokens")
|
||||||
elif tag.get("op"):
|
elif tag.get('op'):
|
||||||
op = tag.get("op")
|
op = tag.get('op')
|
||||||
op1 = Decimal(base_tag.text or "0")
|
op1 = Decimal(base_tag.text or '0')
|
||||||
op2 = Decimal(tag.text or "0")
|
op2 = Decimal(tag.text or '0')
|
||||||
# Try converting to integers if possible, to pass validation.
|
# 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)
|
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)
|
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))
|
base_tag.text = str(round(op1 * op2))
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid operator '{op}'")
|
raise ValueError(f"Invalid operator '{op}'")
|
||||||
else:
|
else:
|
||||||
base_tag.text = tag.text
|
base_tag.text = tag.text
|
||||||
for prop in tag.attrib:
|
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))
|
base_tag.set(prop, tag.get(prop))
|
||||||
for child in tag:
|
for child in tag:
|
||||||
base_child = base_tag.find(child.tag)
|
base_child = base_tag.find(child.tag)
|
||||||
if "disable" in child.attrib:
|
if 'disable' in child.attrib:
|
||||||
if base_child is not None:
|
if base_child is not None:
|
||||||
base_tag.remove(base_child)
|
base_tag.remove(base_child)
|
||||||
elif ("merge" not in child.attrib) or (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:
|
if 'replace' in child.attrib and base_child is not None:
|
||||||
base_tag.remove(base_child)
|
base_tag.remove(base_child)
|
||||||
base_child = None
|
base_child = None
|
||||||
if base_child is None:
|
if base_child is None:
|
||||||
base_child = ET.Element(child.tag)
|
base_child = ElementTree.Element(child.tag)
|
||||||
base_tag.append(base_child)
|
base_tag.append(base_child)
|
||||||
self.apply_layer(base_child, child)
|
self.apply_layer(base_child, child)
|
||||||
if "replace" in base_child.attrib:
|
if 'replace' in base_child.attrib:
|
||||||
del base_child.attrib["replace"]
|
del base_child.attrib['replace']
|
||||||
|
|
||||||
def load_inherited(self, base_path, vfs_path, mods):
|
def load_inherited(self, base_path, vfs_path, mods):
|
||||||
entity = self._load_inherited(base_path, vfs_path, mods)
|
entity = self._load_inherited(base_path, vfs_path, mods)
|
||||||
entity[:] = sorted(entity[:], key=lambda x: x.tag)
|
entity[:] = sorted(entity[:], key=lambda x: x.tag)
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
def _load_inherited(self, base_path, vfs_path, mods, base=None):
|
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)
|
paths = vfs_path.split("|", 1)
|
||||||
self._load_inherited(base_path, paths[1], mods, base)
|
base = 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[0], mods, base);
|
||||||
|
return base
|
||||||
|
|
||||||
main_mod = self.get_main_mod(base_path, vfs_path, mods)
|
main_mod = self.get_main_mod(base_path, vfs_path, mods)
|
||||||
fp = self.get_file(base_path, vfs_path, main_mod)
|
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():
|
for el in layer.iter():
|
||||||
children = [x.tag for x in el]
|
children = [x.tag for x in el]
|
||||||
duplicates = [x for x, c in Counter(children).items() if c > 1]
|
duplicates = [x for x, c in Counter(children).items() if c > 1]
|
||||||
if duplicates:
|
if duplicates:
|
||||||
for dup in duplicates:
|
for dup in duplicates:
|
||||||
self.logger.warning(
|
self.logger.warning(f"Duplicate child node '{dup}' in tag {el.tag} of {fp}")
|
||||||
"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)
|
||||||
if layer.get("parent"):
|
|
||||||
parent = self._load_inherited(base_path, layer.get("parent"), mods, base)
|
|
||||||
self.apply_layer(parent, layer)
|
self.apply_layer(parent, layer)
|
||||||
return parent
|
return parent
|
||||||
|
else:
|
||||||
if not base:
|
if not base:
|
||||||
return layer
|
return layer
|
||||||
|
else:
|
||||||
self.apply_layer(base, layer)
|
self.apply_layer(base, layer)
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
def find_files(vfs_root, mods, vfs_path, *ext_list):
|
def find_files(vfs_root, mods, vfs_path, *ext_list):
|
||||||
"""Return a list of 2-size tuples.
|
|
||||||
|
|
||||||
Each tuple contains:
|
|
||||||
- Path relative to the mod base
|
|
||||||
- full Path
|
|
||||||
"""
|
"""
|
||||||
full_exts = ["." + ext for ext in ext_list]
|
returns a list of 2-size tuple with:
|
||||||
|
- Path relative to the mod base
|
||||||
|
- full Path
|
||||||
|
"""
|
||||||
|
full_exts = ['.' + ext for ext in ext_list]
|
||||||
|
|
||||||
def find_recursive(dp, base):
|
def find_recursive(dp, base):
|
||||||
"""(relative Path, full Path) generator."""
|
"""(relative Path, full Path) generator"""
|
||||||
if dp.is_dir():
|
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():
|
for fp in dp.iterdir():
|
||||||
yield from find_recursive(fp, base)
|
yield from find_recursive(fp, base)
|
||||||
elif dp.suffix in full_exts:
|
elif dp.suffix in full_exts:
|
||||||
relative_file_path = dp.relative_to(base)
|
relative_file_path = dp.relative_to(base)
|
||||||
yield (relative_file_path, dp.resolve())
|
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