Compare commits
9 Commits
5082d39fc7
...
307fd81888
Author | SHA1 | Date | |
---|---|---|---|
307fd81888 | |||
eafcb0db2c | |||
c7064ca6f5 | |||
f99ad88998 | |||
6c752be00e | |||
97d04be759 | |||
c252e1c219 | |||
b0cf0e8600 | |||
ecf91837f1 |
@ -11,7 +11,6 @@ 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
|
||||
@ -51,6 +50,7 @@ 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,3 +16,15 @@ 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
|
||||
|
BIN
community-mod/art/textures/cursors/action-target.png
Normal file
BIN
community-mod/art/textures/cursors/action-target.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
1
community-mod/art/textures/cursors/action-target.txt
Normal file
1
community-mod/art/textures/cursors/action-target.txt
Normal file
@ -0,0 +1 @@
|
||||
1 1
|
@ -1,5 +1,11 @@
|
||||
[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"]
|
||||
|
1989
community-mod/gui/session/unit_actions.js
Normal file
1989
community-mod/gui/session/unit_actions.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "community-mod",
|
||||
"version": "0.26.11",
|
||||
"version": "0.26.12",
|
||||
"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,7 +81,8 @@ 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";
|
||||
|
||||
@ -100,7 +101,8 @@ 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);
|
||||
@ -108,7 +110,8 @@ 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
|
||||
}
|
||||
@ -116,7 +119,8 @@ 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;
|
||||
@ -137,28 +141,20 @@ BuildRestrictions.prototype.CheckPlacement = function () {
|
||||
return result; // Fail
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (this.template.Category == "Wall")
|
||||
{
|
||||
// for walls, only test the center point
|
||||
var ret = areWoodEnts ? "success" : cmpObstruction.CheckFoundation(passClassName, true);
|
||||
var ret = cmpObstruction.CheckFoundation(passClassName, true);
|
||||
}
|
||||
else {
|
||||
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");
|
||||
@ -187,7 +183,8 @@ 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");
|
||||
@ -195,7 +192,8 @@ 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");
|
||||
@ -203,19 +201,22 @@ 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");
|
||||
@ -226,8 +227,10 @@ 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
|
||||
}
|
||||
@ -239,18 +242,22 @@ 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",
|
||||
@ -267,9 +274,11 @@ 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",
|
||||
@ -294,11 +303,6 @@ 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;
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Number of rounds of firing per 2 seconds.
|
||||
const roundCount = 10;
|
||||
const roundCount = 20;
|
||||
const attackType = "Ranged";
|
||||
|
||||
function BuildingAI() { }
|
||||
function BuildingAI() {}
|
||||
|
||||
BuildingAI.prototype.Schema =
|
||||
"<element name='DefaultArrowCount'>" +
|
||||
@ -22,34 +22,42 @@ BuildingAI.prototype.Schema =
|
||||
|
||||
BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2;
|
||||
|
||||
BuildingAI.prototype.Init = function () {
|
||||
BuildingAI.prototype.Init = function()
|
||||
{
|
||||
this.currentRound = 0;
|
||||
this.archersGarrisoned = 0;
|
||||
this.arrowsLeft = 0;
|
||||
this.targetUnits = [];
|
||||
this.focusTargets = [];
|
||||
};
|
||||
|
||||
BuildingAI.prototype.OnGarrisonedUnitsChanged = function (msg) {
|
||||
BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
|
||||
{
|
||||
let classes = this.template.GarrisonArrowClasses;
|
||||
for (let ent of msg.added) {
|
||||
for (let ent of msg.added)
|
||||
{
|
||||
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
||||
if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
|
||||
++this.archersGarrisoned;
|
||||
}
|
||||
for (let ent of msg.removed) {
|
||||
for (let ent of msg.removed)
|
||||
{
|
||||
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
||||
if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
|
||||
--this.archersGarrisoned;
|
||||
}
|
||||
};
|
||||
|
||||
BuildingAI.prototype.OnOwnershipChanged = function (msg) {
|
||||
BuildingAI.prototype.OnOwnershipChanged = function(msg)
|
||||
{
|
||||
this.targetUnits = [];
|
||||
this.focusTargets = [];
|
||||
this.SetupRangeQuery();
|
||||
this.SetupGaiaRangeQuery();
|
||||
};
|
||||
|
||||
BuildingAI.prototype.OnDiplomacyChanged = function (msg) {
|
||||
BuildingAI.prototype.OnDiplomacyChanged = function(msg)
|
||||
{
|
||||
if (!IsOwnedByPlayer(msg.player, this.entity))
|
||||
return;
|
||||
|
||||
@ -59,8 +67,10 @@ BuildingAI.prototype.OnDiplomacyChanged = function (msg) {
|
||||
this.SetupGaiaRangeQuery();
|
||||
};
|
||||
|
||||
BuildingAI.prototype.OnDestroy = function () {
|
||||
if (this.timer) {
|
||||
BuildingAI.prototype.OnDestroy = function()
|
||||
{
|
||||
if (this.timer)
|
||||
{
|
||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
cmpTimer.CancelTimer(this.timer);
|
||||
this.timer = undefined;
|
||||
@ -77,7 +87,8 @@ BuildingAI.prototype.OnDestroy = function () {
|
||||
/**
|
||||
* React on Attack value modifications, as it might influence the range.
|
||||
*/
|
||||
BuildingAI.prototype.OnValueModification = function (msg) {
|
||||
BuildingAI.prototype.OnValueModification = function(msg)
|
||||
{
|
||||
if (msg.component != "Attack")
|
||||
return;
|
||||
|
||||
@ -89,13 +100,15 @@ BuildingAI.prototype.OnValueModification = function (msg) {
|
||||
/**
|
||||
* Setup the Range Query to detect units coming in & out of range.
|
||||
*/
|
||||
BuildingAI.prototype.SetupRangeQuery = function () {
|
||||
BuildingAI.prototype.SetupRangeQuery = function()
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return;
|
||||
|
||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
if (this.enemyUnitsQuery) {
|
||||
if (this.enemyUnitsQuery)
|
||||
{
|
||||
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
|
||||
this.enemyUnitsQuery = undefined;
|
||||
}
|
||||
@ -124,13 +137,15 @@ BuildingAI.prototype.SetupRangeQuery = function () {
|
||||
|
||||
// Set up a range query for Gaia units within LOS range which can be attacked.
|
||||
// This should be called whenever our ownership changes.
|
||||
BuildingAI.prototype.SetupGaiaRangeQuery = function () {
|
||||
BuildingAI.prototype.SetupGaiaRangeQuery = function()
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return;
|
||||
|
||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
if (this.gaiaUnitsQuery) {
|
||||
if (this.gaiaUnitsQuery)
|
||||
{
|
||||
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
|
||||
this.gaiaUnitsQuery = undefined;
|
||||
}
|
||||
@ -154,14 +169,16 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function () {
|
||||
/**
|
||||
* Called when units enter or leave range.
|
||||
*/
|
||||
BuildingAI.prototype.OnRangeUpdate = function (msg) {
|
||||
BuildingAI.prototype.OnRangeUpdate = function(msg)
|
||||
{
|
||||
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return;
|
||||
|
||||
// Target enemy units except non-dangerous animals.
|
||||
if (msg.tag == this.gaiaUnitsQuery) {
|
||||
if (msg.tag == this.gaiaUnitsQuery)
|
||||
{
|
||||
msg.added = msg.added.filter(e => {
|
||||
let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
|
||||
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
|
||||
@ -176,7 +193,8 @@ BuildingAI.prototype.OnRangeUpdate = function (msg) {
|
||||
this.targetUnits.push(entity);
|
||||
|
||||
// Remove targets outside of vision-range.
|
||||
for (let entity of msg.removed) {
|
||||
for (let entity of msg.removed)
|
||||
{
|
||||
let index = this.targetUnits.indexOf(entity);
|
||||
if (index > -1)
|
||||
this.targetUnits.splice(index, 1);
|
||||
@ -186,7 +204,8 @@ BuildingAI.prototype.OnRangeUpdate = function (msg) {
|
||||
this.StartTimer();
|
||||
};
|
||||
|
||||
BuildingAI.prototype.StartTimer = function () {
|
||||
BuildingAI.prototype.StartTimer = function()
|
||||
{
|
||||
if (this.timer)
|
||||
return;
|
||||
|
||||
@ -201,12 +220,14 @@ BuildingAI.prototype.StartTimer = function () {
|
||||
attackTimers.prepare, attackTimers.repeat / roundCount, null);
|
||||
};
|
||||
|
||||
BuildingAI.prototype.GetDefaultArrowCount = function () {
|
||||
BuildingAI.prototype.GetDefaultArrowCount = function()
|
||||
{
|
||||
var arrowCount = +this.template.DefaultArrowCount;
|
||||
return Math.round(ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity));
|
||||
};
|
||||
|
||||
BuildingAI.prototype.GetMaxArrowCount = function () {
|
||||
BuildingAI.prototype.GetMaxArrowCount = function()
|
||||
{
|
||||
if (!this.template.MaxArrowCount)
|
||||
return Infinity;
|
||||
|
||||
@ -214,12 +235,14 @@ BuildingAI.prototype.GetMaxArrowCount = function () {
|
||||
return Math.round(ApplyValueModificationsToEntity("BuildingAI/MaxArrowCount", maxArrowCount, this.entity));
|
||||
};
|
||||
|
||||
BuildingAI.prototype.GetGarrisonArrowMultiplier = function () {
|
||||
BuildingAI.prototype.GetGarrisonArrowMultiplier = function()
|
||||
{
|
||||
var arrowMult = +this.template.GarrisonArrowMultiplier;
|
||||
return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity);
|
||||
};
|
||||
|
||||
BuildingAI.prototype.GetGarrisonArrowClasses = function () {
|
||||
BuildingAI.prototype.GetGarrisonArrowClasses = function()
|
||||
{
|
||||
var string = this.template.GarrisonArrowClasses;
|
||||
if (string)
|
||||
return string.split(/\s+/);
|
||||
@ -231,25 +254,45 @@ BuildingAI.prototype.GetGarrisonArrowClasses = function () {
|
||||
* DefaultArrowCount + Garrisoned Archers (i.e., any unit capable
|
||||
* of shooting arrows from inside buildings).
|
||||
*/
|
||||
BuildingAI.prototype.GetArrowCount = function () {
|
||||
BuildingAI.prototype.GetArrowCount = function()
|
||||
{
|
||||
let count = this.GetDefaultArrowCount() +
|
||||
Math.round(this.archersGarrisoned * this.GetGarrisonArrowMultiplier());
|
||||
|
||||
return Math.min(count, this.GetMaxArrowCount());
|
||||
};
|
||||
|
||||
BuildingAI.prototype.SetUnitAITarget = function (ent) {
|
||||
BuildingAI.prototype.SetUnitAITarget = function(ent)
|
||||
{
|
||||
this.unitAITarget = ent;
|
||||
if (ent)
|
||||
this.StartTimer();
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds index to keep track of the user-targeted units supporting a queue
|
||||
* @param {ent} - Target of rallypoint selection when selection is an enemy unit from unit_actions.js
|
||||
*/
|
||||
BuildingAI.prototype.AddFocusTarget = function(ent, queued, push)
|
||||
{
|
||||
if (!ent || this.targetUnits.indexOf(ent) === -1)
|
||||
return;
|
||||
if (queued)
|
||||
this.focusTargets.push({"entityId": ent});
|
||||
else if (push)
|
||||
this.focusTargets.unshift({"entityId": ent});
|
||||
else
|
||||
this.focusTargets = [{"entityId": ent}];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fire arrows with random temporal distribution on prefered targets.
|
||||
* Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range.
|
||||
*/
|
||||
BuildingAI.prototype.FireArrows = function () {
|
||||
if (!this.targetUnits.length && !this.unitAITarget) {
|
||||
BuildingAI.prototype.FireArrows = function()
|
||||
{
|
||||
if (!this.targetUnits.length && !this.unitAITarget)
|
||||
{
|
||||
if (!this.timer)
|
||||
return;
|
||||
|
||||
@ -274,33 +317,42 @@ BuildingAI.prototype.FireArrows = function () {
|
||||
arrowsToFire = this.arrowsLeft;
|
||||
else
|
||||
arrowsToFire = Math.min(
|
||||
randIntInclusive(0, 2 * this.GetArrowCount() / roundCount),
|
||||
this.GetArrowCount()/(roundCount/4),
|
||||
this.arrowsLeft
|
||||
);
|
||||
|
||||
if (arrowsToFire <= 0) {
|
||||
if (arrowsToFire <= 0)
|
||||
{
|
||||
++this.currentRound;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add targets to a weighted list, to allow preferences.
|
||||
let targets = new WeightedList();
|
||||
let maxPreference = this.MAX_PREFERENCE_BONUS;
|
||||
let addTarget = function (target) {
|
||||
let preference = cmpAttack.GetPreference(target);
|
||||
let weight = 1;
|
||||
|
||||
if (preference !== null && preference !== undefined)
|
||||
weight += maxPreference / (1 + preference);
|
||||
|
||||
targets.push(target, weight);
|
||||
// Add targets to a list.
|
||||
let targets = [];
|
||||
let addTarget = function(target)
|
||||
{
|
||||
const pref = (cmpAttack.GetPreference(target) ?? 49);
|
||||
targets.push({"entityId": target, "preference": pref});
|
||||
};
|
||||
|
||||
// Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ.
|
||||
if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1)
|
||||
addTarget(this.unitAITarget);
|
||||
else if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) != -1)
|
||||
this.focusTargets = [{"entityId": this.unitAITarget}];
|
||||
if (!this.focusTargets.length)
|
||||
{
|
||||
for (let target of this.targetUnits)
|
||||
addTarget(target);
|
||||
// Sort targets by preference and then by proximity.
|
||||
targets.sort( (a,b) => {
|
||||
if (a.preference > b.preference) return 1;
|
||||
else if (a.preference < b.preference) return -1;
|
||||
else if (PositionHelper.DistanceBetweenEntities(this.entity,a.entityId) > PositionHelper.DistanceBetweenEntities(this.entity,b.entityId)) return 1;
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
else
|
||||
targets = this.focusTargets;
|
||||
|
||||
// The obstruction manager performs approximate range checks.
|
||||
// so we need to verify them here.
|
||||
@ -310,25 +362,30 @@ BuildingAI.prototype.FireArrows = function () {
|
||||
const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
|
||||
|
||||
let firedArrows = 0;
|
||||
while (firedArrows < arrowsToFire && targets.length()) {
|
||||
const selectedTarget = targets.randomItem();
|
||||
let targetIndex = 0;
|
||||
while (firedArrows < arrowsToFire && targetIndex < targets.length)
|
||||
{
|
||||
|
||||
let selectedTarget = targets[targetIndex].entityId;
|
||||
if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange(
|
||||
this.entity,
|
||||
selectedTarget,
|
||||
range.min,
|
||||
range.max,
|
||||
yOrigin,
|
||||
false)) {
|
||||
false))
|
||||
{
|
||||
cmpAttack.PerformAttack(attackType, selectedTarget);
|
||||
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
|
||||
++firedArrows;
|
||||
continue;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// Could not attack target, try a different target.
|
||||
targets.remove(selectedTarget);
|
||||
++targetIndex
|
||||
}
|
||||
|
||||
}
|
||||
targets.splice(0, targetIndex);
|
||||
this.arrowsLeft -= firedArrows;
|
||||
++this.currentRound;
|
||||
};
|
||||
@ -336,7 +393,8 @@ BuildingAI.prototype.FireArrows = function () {
|
||||
/**
|
||||
* Returns true if the target entity is visible through the FoW/SoD.
|
||||
*/
|
||||
BuildingAI.prototype.CheckTargetVisible = function (target) {
|
||||
BuildingAI.prototype.CheckTargetVisible = function(target)
|
||||
{
|
||||
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
||||
if (!cmpOwnership)
|
||||
return false;
|
||||
|
@ -5,7 +5,8 @@ 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
|
||||
@ -20,7 +21,6 @@ Foundation.prototype.Init = function () {
|
||||
this.buildTimeModifier = +this.template.BuildTimeModifier;
|
||||
|
||||
this.previewEntity = INVALID_ENTITY;
|
||||
this.entsToDestroy;
|
||||
};
|
||||
|
||||
Foundation.prototype.Serialize = function()
|
||||
@ -101,12 +101,9 @@ Foundation.prototype.GetNumBuilders = function()
|
||||
return this.builders.size;
|
||||
};
|
||||
|
||||
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.IsFinished = function()
|
||||
{
|
||||
return (this.GetBuildProgress() == 1.0);
|
||||
};
|
||||
|
||||
Foundation.prototype.OnOwnershipChanged = function(msg)
|
||||
@ -249,18 +246,22 @@ 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)) {
|
||||
this.entsToDestroy = cmpObstruction.GetEntitiesDeletedUponConstruction();
|
||||
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
|
||||
{
|
||||
for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
|
||||
Engine.DestroyEntity(ent);
|
||||
|
||||
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);
|
||||
@ -272,10 +273,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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -102,6 +102,8 @@ function GetRallyPointCommands(cmpRallyPoint, spawnedEnts)
|
||||
"queued": true,
|
||||
});
|
||||
break;
|
||||
case "attack-only":
|
||||
break;
|
||||
case "trade":
|
||||
ret.push({
|
||||
"type": "setup-trade-route",
|
||||
|
56
ruff.toml
Normal file
56
ruff.toml
Normal file
@ -0,0 +1,56 @@
|
||||
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,16 +1,17 @@
|
||||
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", "r") as f:
|
||||
with open(MOD_PATH / "mod.json") as f:
|
||||
mod = json.load(f)
|
||||
if mod['name'] != "community-mod":
|
||||
if mod["name"] != "community-mod":
|
||||
raise Exception("mod.json has incorrect name")
|
||||
except:
|
||||
raise Exception("scripts.py must be called from the community mod repository")
|
||||
except Exception as e:
|
||||
raise Exception("scripts.py must be called from the community mod repository") from e
|
||||
|
@ -1,43 +1,42 @@
|
||||
import argparse
|
||||
import shutil
|
||||
import json
|
||||
import shutil
|
||||
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", "r", encoding="utf-8") as f:
|
||||
with open(mod_path / "mod.json", 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:
|
||||
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.")
|
||||
except Exception as e:
|
||||
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.") from e
|
||||
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:
|
||||
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,12 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
from os import chdir
|
||||
from pathlib import Path
|
||||
from subprocess import run, CalledProcessError
|
||||
from sys import exit
|
||||
from xml.etree import ElementTree
|
||||
from .scriptlib import SimulTemplateEntity, find_files
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError, run
|
||||
from sys import exit
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from .scriptlib import SimulTemplateEntity, find_files
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -15,37 +16,54 @@ 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).returncode != 0:
|
||||
if run(["xmllint", "--version"], capture_output=True, check=False).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 = 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:
|
||||
if fp.stem.startswith('template_'):
|
||||
if fp.stem.startswith("template_"):
|
||||
continue
|
||||
path = fp.as_posix()
|
||||
if path.startswith('simulation/templates/mixins/') or path.startswith("simulation/templates/special/"):
|
||||
if path.startswith(("simulation/templates/mixins/", "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 = ElementTree.tostring(entity, encoding='unicode')
|
||||
entity = simul_template_entity.load_inherited(
|
||||
simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod]
|
||||
)
|
||||
xmlcontent = ET.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)
|
||||
@ -53,5 +71,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,12 +1,15 @@
|
||||
import json
|
||||
|
||||
from . import MOD_PATH, check_cwd_is_correct
|
||||
|
||||
def get_version():
|
||||
with open(MOD_PATH / "mod.json", "r") as f:
|
||||
mod = json.load(f)
|
||||
print(mod['version'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
def get_version():
|
||||
with open(MOD_PATH / "mod.json") as f:
|
||||
mod = json.load(f)
|
||||
print(mod["version"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_cwd_is_correct()
|
||||
get_version()
|
||||
else:
|
||||
|
112
scripts/modio.py
112
scripts/modio.py
@ -1,36 +1,42 @@
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
from . import MOD_PATH
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
|
||||
from . import MOD_PATH
|
||||
|
||||
|
||||
def run_git_command(command):
|
||||
""" Run a git command and return its output. """
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
"""Run a git command and return its output."""
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
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'])
|
||||
"""Get the current commit hash."""
|
||||
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. """
|
||||
"""Get the commit hash for a tag, or return None if the tag doesn't exist."""
|
||||
try:
|
||||
commit_hash = run_git_command(['git', 'rev-list', '-n', '1', tag])
|
||||
return commit_hash
|
||||
except Exception:
|
||||
return run_git_command(["git", "rev-list", "-n", "1", tag])
|
||||
except Exception as e:
|
||||
print(f"Failed to get commit for tag {tag}: {e}")
|
||||
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). """
|
||||
"""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:
|
||||
@ -39,28 +45,30 @@ 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 in filtered_tags:
|
||||
manual_tag = ".".join(version_array)
|
||||
if manual_tag not in filtered_tags:
|
||||
return None
|
||||
|
||||
current_index = filtered_tags.index(manual_tag)
|
||||
return filtered_tags[current_index]
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to get previous tag in series: {e}")
|
||||
raise Exception(f"Failed to get previous tag in series: {e}") from e
|
||||
|
||||
|
||||
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:
|
||||
if previous_tag is not None:
|
||||
changelog = run_git_command(['git', 'log', f'{previous_tag}..{current_commit}', '--oneline'])
|
||||
else:
|
||||
return run_git_command(
|
||||
["git", "log", f"{previous_tag}..{current_commit}", "--oneline"]
|
||||
)
|
||||
# No previous tag, so get the log from the start of the repository
|
||||
changelog = run_git_command(['git', 'log', current_commit, '--oneline'])
|
||||
return changelog
|
||||
return run_git_command(["git", "log", current_commit, "--oneline"])
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to generate changelog: {e}")
|
||||
raise Exception(f"Failed to generate changelog: {e}") from 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")
|
||||
@ -72,9 +80,11 @@ mod_name = os.getenv("MOD_NAME")
|
||||
community_mod_id = 2144305
|
||||
zeroad_id = 5
|
||||
|
||||
mod_json = json.load(open(MOD_PATH / 'mod.json', 'r'))
|
||||
mod_json = None
|
||||
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)
|
||||
if not commit:
|
||||
print(f"Tag {tag} does not exist. Using current commit as fallback.")
|
||||
@ -83,29 +93,35 @@ if not commit:
|
||||
previous_tag = get_previous_tag_in_series(tag)
|
||||
changelog = get_changelog(commit, previous_tag)
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {oauth2_token}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
print(changelog)
|
||||
|
||||
r = requests.get(f'https://api.mod.io/v1/me/mods', params={
|
||||
'api_key': api_key
|
||||
}, headers = headers)
|
||||
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
|
||||
)
|
||||
|
||||
print(r.json())
|
||||
|
||||
files = {'filedata': open(mod_file_path, 'rb')}
|
||||
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()
|
||||
|
||||
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,
|
||||
timeout=500,
|
||||
data={
|
||||
"version": mod_version,
|
||||
"active": True,
|
||||
"changelog": changelog,
|
||||
"metadata_blob": json.dumps(
|
||||
{"dependencies": mod_json["dependencies"], "minisigs": [signature]}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
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())
|
||||
print(rq.json())
|
||||
|
@ -1,9 +1,9 @@
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
from re import split
|
||||
from sys import stderr
|
||||
from xml.etree import ElementTree
|
||||
from os.path import exists
|
||||
from re import split
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
|
||||
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,107 +33,110 @@ 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 = ElementTree.Element(child.tag)
|
||||
base_child = ET.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)
|
||||
entity[:] = sorted(entity[:], key=lambda x: x.tag)
|
||||
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:
|
||||
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:
|
||||
paths = vfs_path.split("|", 1)
|
||||
base = self._load_inherited(base_path, paths[1], mods, base);
|
||||
base = self._load_inherited(base_path, paths[0], mods, base);
|
||||
return base
|
||||
self._load_inherited(base_path, paths[1], mods, base)
|
||||
return self._load_inherited(base_path, paths[0], mods, base)
|
||||
|
||||
main_mod = self.get_main_mod(base_path, vfs_path, mods)
|
||||
fp = self.get_file(base_path, vfs_path, main_mod)
|
||||
layer = ElementTree.parse(fp).getroot()
|
||||
layer = ET.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(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.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.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):
|
||||
"""
|
||||
returns a list of 2-size tuple with:
|
||||
"""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]
|
||||
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 != '.svn' and dp.name != '.git' and not dp.name.endswith('~'):
|
||||
if dp.name not in (".svn", ".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