Compare commits

..

No commits in common. "main" and "sparta_adjustments" have entirely different histories.

34 changed files with 252 additions and 914 deletions

View File

@ -18,7 +18,6 @@ jobs:
- name: Get changed files
id: files
run: |
git fetch origin ${{ github.event.pull_request.base.ref }}
files=$(git diff-tree --no-commit-id --name-only -r HEAD origin/${{ github.event.pull_request.base.ref }})
files=$(echo "$files" | grep 'simulation/templates/' | grep '.xml' || true)
files=$(echo "$files" | awk '{ gsub("community-mod/","", $0); print $0 }')
@ -47,10 +46,9 @@ jobs:
needs: check_version_increment
env:
MOD_NAME: 0ad-community-mod
if: github.ref == 'refs/heads/signed' && github.event_name == 'push'
if: github.ref == 'refs/heads/signed'
steps:
- uses: actions/checkout@v4
fetch-depth: 0
- uses: actions/setup-python@v5
- run: pip3 install requests
- name: Install minisign
@ -74,12 +72,10 @@ jobs:
echo "Tag ${{ env.VERSION }} already exists"
exit 1
fi
- name: Package Pyromod
uses: https://gitea.wildfiregames.com/Stan/gitea-action-build-pyromod@f37c93f16d4992177c1115016d9a2501339972a0
- uses: https://gitea.wildfiregames.com/Stan/gitea-action-build-pyromod@main
with:
name: ${{ env.MOD_NAME }}
version: ${{ env.MOD_VERSION }}
directory: community-mod
- name: Create sha256sum
run: |
OUTPUT_FILE="${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.pyromod"
@ -90,20 +86,7 @@ jobs:
cp -v ${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.pyromod ../${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip
- name: Sign
run: |
echo ${{ secrets.MINISIGN_KEY_PW }} | minisign -S \
-s signature-file.pem \
-m "${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip" \
-x ${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip.minisign
echo ${{ secrets.MINISIGN_KEY_PW }} | minisign -S -s signature-file.pem -m "${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip" -x signature.minisign
- name: Upload to Modio
run: |
MOD_FILE_PATH="${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip" MOD_VERSION="${{ env.MOD_VERSION }}" python3 -m scripts.modio
- name: Release PyroMod
uses: akkuman/gitea-release-action@v1
with:
files: |-
$OUTPUT_FILE.sha256sum
${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip
${{ env.MOD_NAME }}-${{ env.MOD_VERSION }}.zip.minisign
target_commitish: ${{ env.MOD_VERSION }}
tag_name: ${{ env.MOD_VERSION }}
name: Community Mod ${{ env.MOD_VERSION }}

View File

@ -16,15 +16,3 @@ repos:
- id: yamllint
args:
- -s
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.1
hooks:
- id: ruff
args:
- --output-format=full
exclude: ^source/tools/webservices/
- id: ruff-format
args:
- --check
- --target-version
- py311

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<actor version="1">
<castshadow/>
<group>
<variant file="biped/base_healer_male.xml">
<mesh>skeletal/new/m_dress.dae</mesh>
<props>
<prop actor="props/units/heads/new/head_hele_healer.xml" attachpoint="head"/>
<prop actor="props/units/staff.xml" attachpoint="weapon_R"/>
</props>
</variant>
</group>
<group>
<variant frequency="1" name="healer-robe-1">
<props>
</props>
<textures>
<texture file="skeletal/athen/philosopher_01.png" name="baseTex"/>
<texture file="default_norm.png" name="normTex"/>
<texture file="skeletal/athen/philosopher_01_spec.png" name="specTex"/>
</textures>
</variant>
</group>
<group>
<variant frequency="1" name="Idle"/>
<variant file="biped/death_infantry.xml"/>
</group>
<material>player_trans_norm_spec.xml</material>
</actor>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1,204 +1,6 @@
[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"]
- Slighty damage buff to all archer units.
- Allow walls and palisades to be built over trees, destroying trees upon completion.
- Rams now has less pierce armor and increased hack armor.
- Training time 25% faster for all champions that are not trained from barracks and stables.
[icon="tech_loom" displace="0 8"][font="sans-bold-18"] The Loom
[font="sans-16"]
Cost: reduced from [icon="icon_food"] 150 to [icon="icon_food"] 100.
[icon="tech_cartography" displace="0 8"][font="sans-bold-18"] Cartography
[font="sans-16"]
Now available at both Market and CC
[font="sans-bold-18"] Heroes update
[icon="han_hero_wei_qing" displace="0 8"] [font="sans-bold-18"] Wei Qing Han Hero
[font="sans-16"]
[font="sans-bold-16"]Mandate of Heaven[font="sans-16"] aura (-10% melee and range enemy cavalry attack): increased from 30 to 45 meters.
[icon="pers_hero_xerxes" displace="0 8"] [font="sans-bold-18"] Xerxes Persian Hero
[font="sans-16"]
[font="sans-bold-16"]Administrator[font="sans-16"] aura: increased build rate from +15% to +25%. Aura radius increased to 100 meters.
Added [font="sans-bold-16"]Invader of Greece[font="sans-16"] aura: Siege Rams, Elephants, and Champion Infantry +25% Health.
[icon="kush_hero_arakamani" displace="0 8"] [font="sans-bold-18"] Arakamani Kushites Hero
[font="sans-16"]
Removed [font="sans-bold-16"]Defier of Tradition[font="sans-16"] aura.
[font="sans-bold-16"]Follower of Apedemak[font="sans-16"] aura: Temple of Apedemak −30% resource costs and build time. Apedemak Temple Guard −30% resource costs and training time.
[font="sans-bold-16"]Slaughter of the Faithful[font="sans-16"] aura: Enemy Healers healing strength from -50% to -100%
[icon="kush_hero_nastasen" displace="0 8"] [font="sans-bold-18"] Arakamani Kushites Hero
[font="sans-16"]
[font="sans-bold-16"]Savior of Kush[font="sans-16"] aura now affects Soldiers and Elephants. Increased melee and ranged damage from +10% to +15%. Gain +50% resource loot.
[icon="maur_pillar_ashoka" displace="0 8"] [font="sans-bold-18"] Pillar of Ashoka
[font="sans-16"]
[font="sans-bold-16"]Edict of Ashoka[font="sans-16"] aura: Now affects Traders and Humans. +10% movement speed, +15 Health. Decreased radius from 75 to 65.
[icon="rome_ship_trireme_big" displace="0 18"][font="sans-bold-18"] Naval rebalance
[font="sans-16"]
- Fireships now behave like a melee ship that can also apply a burning status to its enemies. Add fuel to the fire ship to get it burning really hot which causes the ship to deal area damage upon its destruction. From full HP, the fire ship burns up in about 10 seconds. If the ship has less HP, it will blow up sooner.
- Footprints are smaller for slightly better pathing.
- All ships reduced population cost from 2 to 1, except for Siege ships.
[font="sans-bold-18"] Scout ships
[font="sans-16"]
Attack damage and range increased.
Faster acceleration movement speed.
[font="sans-bold-18"] Arrow ships
[font="sans-16"]
Attack damage and range increased.
Faster prepare attack time from 0.5 to 0.25 seconds.
Cost: from [icon="icon_wood"] 120 [icon="icon_metal"] 80 to [icon="icon_wood"] 120 [icon="icon_metal"] 100
Loot: from [icon="icon_wood"] 12 [icon="icon_metal"] 8 to [icon="icon_wood"] 12 [icon="icon_metal"] 10
[font="sans-bold-18"] Ram ships
[font="sans-16"]
Hack damage increased from 280 to 350
Crush damage increased from 20 to 40
Cost: from [icon="icon_food"] 50 [icon="icon_wood"] 100 [icon="icon_metal"] 50 to [icon="icon_food"] 50 [icon="icon_wood"] 100 [icon="icon_metal"] 25
[font="sans-bold-18"] Siege ships
[font="sans-16"]
Attack range from 2 to 4
Crush damage increased from 50 to 100
[icon="emblem_athen" displace="0 18"][font="sans-bold-18"] Athenians update
[font="sans-16"]
Added new phase 2 healing hero unit and two uniques technologies
Spearmen may promote to champions with 200 xp
- Units
[icon="athen_hero_hippocrates" displace="0 8"] [font="sans-bold-18"] Hippocrates
[font="sans-16"]
[font="sans-bold-16"]Officer Accommodation[font="sans-16"] aura: Garrisoned Heroes +6 health regeneration rate.
[font="sans-bold-16"]Regeneration[font="sans-16"] aura: Humans +0.5 health regeneration rate.
[font="sans-bold-16"]Medicine Father[font="sans-16"] aura: Own and Allied Healers +2 heal health.
- Technologies
[icon="tech_ostracism" displace="0 8"] [font="sans-bold-18"] Ostracism
[font="sans-16"]
Citizen soldiers +5% health, but Heroes −40% health.
Cost: [icon="icon_food"] 300 [icon="icon_metal"] 300. [icon="icon_time"]: 60 seconds. Phase: [icon="phase_3" displace="0 2"]
[icon="tech_pheidan_workshop" displace="0 8"] [font="sans-bold-18"] Pheidan Workshop
[font="sans-16"]
Temples and Wonder −50% stone cost and build time.
Cost: [icon="icon_stone"] 300. [icon="icon_time"]: 40 seconds. Phase: [icon="phase_2" displace="0 2"]
[icon="emblem_romans" displace="0 18"][font="sans-bold-18"] Romans update
[font="sans-16"]
Roman army camp and siege walls do not decay
Unlock Onager from the arsenal
- Civ bonus
[icon="civbonus_fertility" displace="0 8"][font="sans-bold-18"] Fertility
[font="sans-16"]
Women may be trained from houses without fertility festival.
[icon="civbonus_engineers" displace="0 8"][font="sans-bold-18"] Legionary Engineers
[font="sans-16"]
Siege Catapults and Onagers 10% faster fire rate and +20% movement speed.
- Units
[icon="rome_siege_onager" displace="0 8"][font="sans-bold-18"] Onagers
[font="sans-16"]
New unit good versus units. Not so good versus buildings.
Shorter range. 4 meters area splash damage.
- Technologies
[icon="tech_roman_roads" displace="0 8"][font="sans-bold-18"] Roman Roads
[font="sans-16"]
All Land Units +5% movement speed.
Cost: [icon="icon_stone"] 500. [icon="icon_time"]: 60 seconds. Phase: [icon="phase_2" displace="0 2"]
[icon="tech_marian_reforms" displace="0 8"][font="sans-bold-18"] Marian Reforms
[font="sans-16"]
Convert Citizen Swordsmen and Skirmishers to Marian Legionaries, and Citizen Spearmen to conscripts which cannot promote. Allows to train Onagers and Centurions, lose access to Italic Heavy Infantry.
Cost: [icon="icon_food"] 1200. [icon="icon_metal"] 1000. [icon="icon_time"]: 60 seconds. Phase: [icon="phase_3" displace="0 2"]
[icon="emblem_spartans" displace="0 18"][font="sans-bold-18"] Spartans adjustments
[font="sans-16"]
- Units
[icon="spart_neodamodes" displace="0 8"] [font="sans-bold-18"] Neodamodes
[font="sans-16"]
[icon="icon_time"]: from 10 to 12 seconds.
[icon="spart_spartiates" displace="0 8"] [font="sans-bold-18"] Champion Spartiates
[font="sans-16"]
Removed phase [icon="phase_1" displace="0 2"] and [icon="phase_2" displace="0 2"] train time penalty
- Buildings
[icon="spart_syssition" displace="0 8"] [font="sans-bold-18"] Syssition
[font="sans-16"]
Cost: from [icon="icon_stone"] 200 [icon="icon_metal"] 200 to [icon="icon_stone"] 150 [icon="icon_metal"] 150
- Technologies
[icon="tech_tyrtean-paeans" displace="0 8"][font="sans-bold-18"] Tyrtean Paeans
[font="sans-16"]
Now available at [icon="phase_1" displace="0 2"]
Cost: from [icon="icon_food"] 400 [icon="icon_metal"] 200 to [icon="icon_food"] 200 [icon="icon_metal"] 100. [icon="icon_time"]: from 50 to 30 seconds.
[icon="emblem_macedonians" displace="0 18"][font="sans-bold-18"] Macedonians
[font="sans-16"]
Arsenal moved to [icon="phase_2" displace="0 2"] and allow to train Bolt Shooters.
[icon="emblem_han" displace="0 18"][font="sans-bold-18"] Hans
[font="sans-16"]
[icon="han_crossbowman" displace="0 8"][font="sans-bold-18"] Crossbowmen
[font="sans-16"]
Pierce damage increased from 23 to 24.
Movement speed increased from 9.6 to 10.0
[font="sans-bold-20"]
#Version 10 (July 15, 2024)
#Version 10 (July 16, 2024)
[font="sans-16"]
- Woodlines a little larger in badosu maps

View File

@ -33,22 +33,7 @@
sprite="stretched:session/portraits/emblems/emblem_carthaginians.png"
size="48 48"
/>
<icon name="emblem_athen"
sprite="stretched:session/portraits/emblems/emblem_athenians.png"
size="48 48"
/>
<icon name="emblem_romans"
sprite="stretched:session/portraits/emblems/emblem_romans.png"
size="48 48"
/>
<icon name="emblem_macedonians"
sprite="stretched:session/portraits/emblems/emblem_macedonians.png"
size="48 48"
/>
<icon name="emblem_han"
sprite="stretched:session/portraits/emblems/emblem_han.png"
size="48 48"
/>
<!-- Phases -->
@ -58,25 +43,17 @@
/>
<icon name="phase_2"
sprite="stretched:session/portraits/technologies/town_phase.png"
size="16 16"
size="32 32"
/>
<icon name="phase_3"
sprite="stretched:session/portraits/technologies/city_phase.png"
size="16 16"
size="32 32"
/>
<!-- Civ Bonuses -->
<icon name="civbonus_agoge"
sprite="stretched:session/portraits/technologies/agoge.png"
size="32 32"
/>
<icon name="civbonus_fertility"
sprite="stretched:session/portraits/technologies/wives_festival.png"
size="32 32"
/>
<icon name="civbonus_engineers"
sprite="stretched:session/portraits/technologies/engineering.png"
size="32 32"
/>
<!-- Structures -->
@ -84,10 +61,7 @@
sprite="stretched:session/portraits/structures/temple_epic.png"
size="32 32"
/>
<icon name="maur_pillar_ashoka"
sprite="stretched:session/portraits/structures/ashoka_pillar.png"
size="32 32"
/>
<icon name="wall"
sprite="stretched:session/portraits/structures/wall.png"
size="32 32"
@ -108,7 +82,8 @@
sprite="stretched:session/portraits/technologies/anvil.png"
size="32 32"
/>
<!-- Civ Bonuses -->
<icon name="civbonus_agoge"
@ -176,11 +151,6 @@
sprite="stretched:session/portraits/technologies/loom.png.cached.dds"
size="32 32"
/>
<icon name="tech_cartography"
sprite="stretched:session/portraits/technologies/cartography.png"
size="32 32"
/>
<icon name="tech_unlock-neodamodes"
sprite="stretched:session/portraits/technologies/helmet_corinthian_bronze_old.png"
size="32 32"
@ -201,50 +171,12 @@
sprite="stretched:session/portraits/technologies/accuracy_bolt.png"
size="32 32"
/>
<icon name="tech_ostracism"
sprite="stretched:session/portraits/technologies/patriotism.png"
size="32 32"
/>
<icon name="tech_pheidan_workshop"
sprite="stretched:session/portraits/technologies/vestals.png"
size="32 32"
/>
<icon name="tech_roman_roads"
sprite="stretched:session/portraits/technologies/masonry_marble.png"
size="32 32"
/>
<icon name="tech_marian_reforms"
sprite="stretched:session/portraits/technologies/shield_scutum.png"
size="32 32"
/>
<!-- Units -->
<icon name="han_hero_wei_qing"
sprite="stretched:session/portraits/units/han/hero_wei_qing.png"
size="32 32"
/>
<icon name="han_crossbowman"
sprite="stretched:session/portraits/units/han/infantry_crossbowman.png"
size="32 32"
/>
<icon name="kush_hero_arakamani"
sprite="stretched:session/portraits/units/kush_hero_arakamani.png"
size="32 32"
/>
<icon name="kush_hero_nastasen"
sprite="stretched:session/portraits/units/kush_hero_nastasen.png"
size="32 32"
/>
<icon name="pers_hero_xerxes"
sprite="stretched:session/portraits/units/pers_hero_xerxes.png"
size="32 32"
/>
<icon name="pers_immortal_inf"
sprite="stretched:session/portraits/units/pers_champion_infantry.png"
size="32 32"
@ -253,7 +185,10 @@
sprite="stretched:session/portraits/units/pers_champion_infantry_archer.png"
size="32 32"
/>
<icon name="pers_ship_bireme"
sprite="stretched:session/portraits/units/pers_ship_bireme.png"
size="32 32"
/>
<icon name="rome_siege_ballista"
sprite="stretched:session/portraits/units/rome_siege_ballista.png"
size="32 32"
@ -262,15 +197,6 @@
sprite="stretched:session/portraits/units/rome_siege_scorpio.png"
size="32 32"
/>
<icon name="rome_siege_ram.png"
sprite="stretched:session/portraits/units/rome_siege_ram.png"
size="32 32"
/>
<icon name="rome_siege_onager"
sprite="stretched:session/portraits/units/rome_siege_onager.png"
size="32 32"
/>
<icon name="spart_hero_agis"
sprite="stretched:session/portraits/units/spart_hero_agis.png"
size="32 32"
@ -279,58 +205,11 @@
sprite="stretched:session/portraits/units/spart/infantry_spearman_neodamodes.png"
size="32 32"
/>
<icon name="spart_spartiates"
sprite="stretched:session/portraits/units/spart_champion_infantry_spear.png"
size="32 32"
/>
<icon name="elephants"
sprite="stretched:session/portraits/structures/stable_elephant.png"
size="32 32"
/>
<icon name="athen_hero_hippocrates"
sprite="stretched:session/portraits/units/athen/hero_hippocrates.png"
size="32 32"
/>
<icon name="mace_bolts"
sprite="stretched:session/portraits/units/rome_siege_scorpio_packed.png"
size="32 32"
/>
<!-- Ships -->
<icon name="pers_ship_bireme"
sprite="stretched:session/portraits/units/pers_ship_bireme.png"
size="32 32"
/>
<icon name="rome_ship_bireme"
sprite="stretched:session/portraits/units/rome_ship_bireme.png"
size="32 32"
/>
<icon name="rome_ship_trireme"
sprite="stretched:session/portraits/units/rome_ship_trireme.png"
size="32 32"
/>
<icon name="rome_ship_trireme_big"
sprite="stretched:session/portraits/units/rome_ship_trireme.png"
size="48 48"
/>
<!-- Auras -->
<icon name="aura_buildgather_bonus"
sprite="stretched:session/auras/buildgather_bonus.png"
size="16 16"
/>
<icon name="aura_build_bonus"
sprite="stretched:session/auras/build_bonus.png"
size="16 16"
/>
<icon name="aura_health_bonus"
sprite="stretched:session/auras/health_bonus.png"
size="16 16"
/>
<!-- Old Icons -->

View File

@ -1,6 +1,6 @@
{
"name": "community-mod",
"version": "0.26.12",
"version": "0.26.11",
"label": "0 A.D. Community Mod",
"url": "https://gitlab.com/0ad/0ad-community-mod-a26",
"description": "The Community Mod is a community-led effort to improve the gameplay of 0 A.D., officially backed by the 0 A.D. team.",

View File

@ -81,8 +81,7 @@ BuildRestrictions.prototype.Init = function()
* (template name should be "preview|"+templateName), as otherwise territory
* checks for buildings with territory influence will not work as expected.
*/
BuildRestrictions.prototype.CheckPlacement = function()
{
BuildRestrictions.prototype.CheckPlacement = function () {
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building";
@ -101,8 +100,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; // Fail
// TODO: AI has no visibility info
if (!cmpPlayer.IsAI())
{
if (!cmpPlayer.IsAI()) {
// Check whether it's in a visible or fogged region
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
@ -110,8 +108,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; // Fail
var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden");
if (!explored)
{
if (!explored) {
result.message = markForTranslation("%(name)s cannot be built in unexplored area");
return result; // Fail
}
@ -119,21 +116,20 @@ BuildRestrictions.prototype.CheckPlacement = function()
// Check obstructions and terrain passability
var passClassName = "";
switch (this.template.PlacementType)
{
case "shore":
passClassName = "building-shore";
break;
switch (this.template.PlacementType) {
case "shore":
passClassName = "building-shore";
break;
case "land-shore":
// 'default-terrain-only' is everywhere a normal unit can go, ignoring
// obstructions (i.e. on passable land, and not too deep in the water)
passClassName = "default-terrain-only";
break;
case "land-shore":
// 'default-terrain-only' is everywhere a normal unit can go, ignoring
// obstructions (i.e. on passable land, and not too deep in the water)
passClassName = "default-terrain-only";
break;
case "land":
default:
passClassName = "building-land";
case "land":
default:
passClassName = "building-land";
}
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
@ -141,30 +137,38 @@ BuildRestrictions.prototype.CheckPlacement = function()
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
var ret = cmpObstruction.CheckFoundation(passClassName, true);
var ret = areWoodEnts ? "success" : cmpObstruction.CheckFoundation(passClassName, true);
}
else
{
else {
var ret = cmpObstruction.CheckFoundation(passClassName, false);
}
if (ret != "success")
{
switch (ret)
{
case "fail_error":
case "fail_no_obstruction":
error("CheckPlacement: Error returned from CheckFoundation");
break;
case "fail_obstructs_foundation":
result.message = markForTranslation("%(name)s cannot be built on another building or resource");
break;
case "fail_terrain_class":
// TODO: be more specific and/or list valid terrain?
result.message = markForTranslation("%(name)s cannot be built on invalid terrain");
if (ret != "success") {
switch (ret) {
case "fail_error":
case "fail_no_obstruction":
error("CheckPlacement: Error returned from CheckFoundation");
break;
case "fail_obstructs_foundation":
result.message = markForTranslation("%(name)s cannot be built on another building or resource");
break;
case "fail_terrain_class":
// TODO: be more specific and/or list valid terrain?
result.message = markForTranslation("%(name)s cannot be built on invalid terrain");
}
return result; // Fail
}
@ -183,8 +187,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
var isNeutral = tileOwner == 0;
var invalidTerritory = "";
if (isOwn)
{
if (isOwn) {
if (!this.HasTerritory("own"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "own");
@ -192,8 +195,7 @@ BuildRestrictions.prototype.CheckPlacement = function()
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own");
}
else if (isMutualAlly)
{
else if (isMutualAlly) {
if (!this.HasTerritory("ally"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "allied");
@ -201,22 +203,19 @@ BuildRestrictions.prototype.CheckPlacement = function()
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied");
}
else if (isNeutral)
{
else if (isNeutral) {
if (!this.HasTerritory("neutral"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "neutral");
}
else
{
else {
// consider everything else enemy territory
if (!this.HasTerritory("enemy"))
// Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
invalidTerritory = markForTranslationWithContext("Territory type", "enemy");
}
if (invalidTerritory)
{
if (invalidTerritory) {
result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s");
result.translateParameters.push("territoryType");
result.translateParameters.push("validTerritories");
@ -227,10 +226,8 @@ BuildRestrictions.prototype.CheckPlacement = function()
}
// Check special requirements
if (this.template.PlacementType == "shore")
{
if (!cmpObstruction.CheckShorePlacement())
{
if (this.template.PlacementType == "shore") {
if (!cmpObstruction.CheckShorePlacement()) {
result.message = markForTranslation("%(name)s must be built on a valid shoreline");
return result; // Fail
}
@ -242,22 +239,18 @@ BuildRestrictions.prototype.CheckPlacement = function()
let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName));
// Check distance restriction
if (this.template.Distance)
{
if (this.template.Distance) {
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cat = this.template.Distance.FromClass;
var filter = function(id)
{
var filter = function (id) {
var cmpIdentity = Engine.QueryInterface(id, IID_Identity);
return cmpIdentity.GetClassesList().indexOf(cat) > -1;
};
if (this.template.Distance.MinDistance !== undefined)
{
if (this.template.Distance.MinDistance !== undefined) {
let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template);
if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter))
{
if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) {
let result = markForPluralTranslation(
"%(name)s too close to a %(category)s, must be at least %(distance)s meter away",
"%(name)s too close to a %(category)s, must be at least %(distance)s meters away",
@ -274,11 +267,9 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result; // Fail
}
}
if (this.template.Distance.MaxDistance !== undefined)
{
if (this.template.Distance.MaxDistance !== undefined) {
let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template);
if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter))
{
if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) {
let result = markForPluralTranslation(
"%(name)s too far from a %(category)s, must be within %(distance)s meter",
"%(name)s too far from a %(category)s, must be within %(distance)s meters",
@ -303,6 +294,11 @@ BuildRestrictions.prototype.CheckPlacement = function()
return result;
};
BuildRestrictions.prototype.IsWoodEntity = function (entity) {
let cmpResource = Engine.QueryInterface(entity, IID_ResourceSupply);
return cmpResource && cmpResource.GetType().generic === "wood";
}
BuildRestrictions.prototype.GetCategory = function()
{
return this.template.Category;

View File

@ -5,8 +5,7 @@ Foundation.prototype.Schema =
"<ref name='nonNegativeDecimal'/>" +
"</element>";
Foundation.prototype.Init = function()
{
Foundation.prototype.Init = function () {
// Foundations are initially 'uncommitted' and do not block unit movement at all
// (to prevent players exploiting free foundations to confuse enemy units).
// The first builder to reach the uncommitted foundation will tell friendly units
@ -21,6 +20,7 @@ Foundation.prototype.Init = function()
this.buildTimeModifier = +this.template.BuildTimeModifier;
this.previewEntity = INVALID_ENTITY;
this.entsToDestroy;
};
Foundation.prototype.Serialize = function()
@ -101,9 +101,12 @@ Foundation.prototype.GetNumBuilders = function()
return this.builders.size;
};
Foundation.prototype.IsFinished = function()
{
return (this.GetBuildProgress() == 1.0);
Foundation.prototype.IsFinished = function () {
if (this.GetBuildProgress() == 1.0 && this.entsToDestroy)
for (let ent of this.entsToDestroy)
Engine.DestroyEntity(ent);
return this.GetBuildProgress() == 1.0;
};
Foundation.prototype.OnOwnershipChanged = function(msg)
@ -246,22 +249,18 @@ Foundation.prototype.GetBuildTime = function()
/**
* @return {boolean} - Whether the foundation has been committed sucessfully.
*/
Foundation.prototype.Commit = function()
{
Foundation.prototype.Commit = function () {
if (this.committed)
return false;
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
{
for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
Engine.DestroyEntity(ent);
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true)) {
this.entsToDestroy = cmpObstruction.GetEntitiesDeletedUponConstruction();
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
if (collisions.length)
{
for (let ent of collisions)
{
if (collisions.length) {
for (let ent of collisions) {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.LeaveFoundation(this.entity);
@ -273,10 +272,10 @@ Foundation.prototype.Commit = function()
// animation to indicate they're waiting for people to get
// out the way
return false;
}
}
// The obstruction always blocks new foundations/construction,
// but we've temporarily allowed units to walk all over it
// (via CCmpTemplateManager). Now we need to remove that temporary

View File

@ -1,12 +0,0 @@
{
"type": "garrisonedUnits",
"affects": [ "Hero" ],
"modifications": [
{
"value": "Health/RegenRate",
"add": 6
}
],
"auraName": "Officer Accommodation",
"auraDescription": "Garrisoned Heroes +6 health regeneration rate."
}

View File

@ -1,14 +0,0 @@
{
"type": "range",
"radius": 35,
"affects": [ "Human" ],
"modifications": [
{
"value": "Health/RegenRate",
"add": 0.5
}
],
"auraName": "Regeneration",
"auraDescription": "Humans +0.5 health regeneration rate.",
"overlayIcon": "art/textures/ui/session/auras/heal.png"
}

View File

@ -1,15 +0,0 @@
{
"type": "range",
"radius": 35,
"affects": [ "Healer" ],
"affectedPlayers": [ "MutualAlly" ],
"modifications": [
{
"value": "Heal/Health",
"add": 2.0
}
],
"auraName": "Medicine Father",
"auraDescription": "Own and Allied Healers +2 heal health.",
"overlayIcon": "art/textures/ui/session/auras/heal.png"
}

View File

@ -1,31 +0,0 @@
{
"genericName": "Ostracism",
"description": "In ancient Athens, ostracism was the process by which any citizen, including political leaders, could be expelled from the city-state for 10 years.",
"cost": {
"food": 300,
"metal": 300
},
"requirements": {
"all": [
{ "tech": "phase_city" },
{ "civ": "athen" }
]
},
"requirementsTooltip": "Unlocked in City Phase.",
"icon": "patriotism.png",
"researchTime": 60,
"tooltip": "Citizen soldiers +5% health, but Heroes −40% health.",
"modifications": [
{
"value": "Health/Max",
"multiply": 1.05,
"affects": "CitizenSoldier"
},
{
"value": "Health/Max",
"multiply": 0.6,
"affects": "Hero"
}
],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View File

@ -1,29 +0,0 @@
{
"genericName": "Pheidian Workshop",
"description": "Pheidias was a Greek sculptor, painter and architect, who lived in the 5th century BC, and is commonly regarded as one of the greatest of all sculptors of Classical Greece: Phidias' Statue of Zeus at Olympia was one of the Seven Wonders of the Ancient World. Phidias designed the statues of the goddess Athena on the Athenian Acropolis, namely the Athena Parthenos inside the Parthenon and the Athena Promachos, a colossal bronze statue of Athena which stood between it and the Propylaea, a monumental gateway that served as the entrance to the Acropolis in Athens.",
"cost": {
"stone": 300
},
"requirements": {
"all": [
{ "tech": "phase_town" },
{ "civ": "athen" }
]
},
"requirementsTooltip": "Unlocked in Town Phase.",
"icon": "vestals.png",
"researchTime": 40,
"tooltip": "Temples and Wonder −50% stone cost and build time.",
"modifications": [
{
"value": "Cost/BuildTime",
"multiply": 0.5
},
{
"value": "Cost/Resources/stone",
"multiply": 0.5
}
],
"affects": [ "Temple", "Wonder" ],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View File

@ -11,8 +11,7 @@
{ "notciv": "brit" },
{ "notciv": "han" },
{ "notciv": "maur" },
{ "notciv": "spart" },
{ "notciv": "cart" }
{ "notciv": "spart" }
]
},
"requirementsTooltip": "Unlocked in City Phase.",

View File

@ -2,7 +2,7 @@
<Entity>
<DeathDamage>
<Shape>Circular</Shape>
<Range>22</Range>
<Range>25</Range>
<FriendlyFire>false</FriendlyFire>
<Bonuses>
<BonusShip>

View File

@ -37,7 +37,6 @@
<Researcher>
<Technologies datatype="tokens">
long_walls
ostracism
</Technologies>
</Researcher>
<Sound>

View File

@ -11,11 +11,6 @@
<Obstruction>
<Static width="17.5" depth="27"/>
</Obstruction>
<Trainer>
<Entities datatype="tokens">
units/{civ}/hero_hippocrates
</Entities>
</Trainer>
<VisualActor>
<Actor>structures/athenians/temple.xml</Actor>
<FoundationActor>props/special/eyecandy/greek_temple_unfinished.xml</FoundationActor>

View File

@ -106,7 +106,6 @@
hellenistic_metropolis
apophora
roman_roads
pheidian_workshop
unlock_spies
spy_counter
</Technologies>

View File

@ -32,8 +32,7 @@
<BuildTime>18</BuildTime>
<Resources>
<wood>175</wood>
<food>50</food>
<metal>50</metal>
<food>75</food>
</Resources>
</Cost>
<Footprint>
@ -42,7 +41,7 @@
</Footprint>
<GarrisonHolder disable=""/>
<Health>
<Max>800</Max>
<Max>1000</Max>
<DamageVariants>
<lightdamage>1.0</lightdamage>
</DamageVariants>
@ -56,7 +55,7 @@
<Resistance>
<Entity>
<Damage>
<Pierce>1</Pierce>
<Pierce>2</Pierce>
</Damage>
</Entity>
</Resistance>

View File

@ -49,7 +49,7 @@
<Entity>
<Damage>
<Hack>7</Hack>
<Pierce>35</Pierce>
<Pierce>30</Pierce>
</Damage>
</Entity>
</Resistance>

View File

@ -41,6 +41,11 @@
<Height>5.0</Height>
</Footprint>
<Resistance>
<Entity>
<Damage>
<Pierce op="add">3</Pierce>
</Damage>
</Entity>
<Foundation>
<Damage>
<Hack>1</Hack>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit_hero_healer">
<Auras datatype="tokens">
units/heroes/athen_hero_hippocrates_1
units/heroes/athen_hero_hippocrates_2
</Auras>
<Health>
<Max>600</Max>
</Health>
<Identity>
<Civ>athen</Civ>
<GenericName>Hippocrates</GenericName>
<SpecificName>Hippocrates</SpecificName>
<Icon>units/athen/hero_hippocrates.png</Icon>
<RequiredTechnology>phase_town</RequiredTechnology>
</Identity>
<VisualActor>
<Actor>units/athenians/hero_healer_hippocrates.xml</Actor>
</VisualActor>
</Entity>

View File

@ -3,10 +3,7 @@
<Identity>
<Rank>Elite</Rank>
</Identity>
<Promotion>
<RequiredXp>400</RequiredXp>
<Entity>units/athen/champion_infantry</Entity>
</Promotion>
<Promotion disable=""/>
<VisualActor>
<Actor>units/athenians/infantry_spearman_e.xml</Actor>
</VisualActor>

View File

@ -5,7 +5,6 @@
<GenericName>Sacred Band Cavalry</GenericName>
<SpecificName>Sacred Band of Astarte</SpecificName>
<Icon>units/cart_champion_cavalry.png</Icon>
<RequiredTechnology>phase_city</RequiredTechnology>
</Identity>
<VisualActor>
<Actor>units/carthaginians/cavalry_spearman_c_m.xml</Actor>

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit_hero_infantry_archer">
<Auras datatype="tokens">
units/heroes/pers_hero_xerxes_i_1
units/heroes/pers_hero_xerxes_i_2
units/heroes/pers_hero_xerxes_i
</Auras>
<Identity>
<Civ>pers</Civ>

View File

@ -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"

View File

@ -1,17 +1,16 @@
import json
from pathlib import Path
MOD_PATH = Path("community-mod")
def check_cwd_is_correct():
try:
if not Path(".git").exists():
raise Exception("No .git in current folder.")
with open(MOD_PATH / "mod.json") as f:
mod = json.load(f)
if mod["name"] != "community-mod":
raise Exception("mod.json has incorrect name")
except Exception as e:
raise Exception("scripts.py must be called from the community mod repository") from e
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:
mod = json.load(f)
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")

View File

@ -1,42 +1,42 @@
import argparse
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", encoding="utf-8") as f:
mod = json.load(f)
if mod["name"] != "0ad":
raise Exception("mod.json has incorrect name")
except Exception as e:
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.") from e
return mod_path
mod_path = Path(path) / PUBLIC_PATH
try:
with open(mod_path / "mod.json", "r") as f:
mod = json.load(f)
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.")
return mod_path
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)
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()
args = parser.parse_args()
check_cwd_is_correct()
path = validate_path(getattr(args, "0ad"))
copy_0ad_files(path, args.folder or DEFAULT_COPY)
check_cwd_is_correct()
path = validate_path(getattr(args, '0ad'))
copy_0ad_files(path, args.folder or DEFAULT_COPY)
else:
raise Exception("Must be called directly")
raise Exception("Must be called directly")

View File

@ -1,13 +1,12 @@
#!/usr/bin/env python3
import argparse
import logging
from os import chdir
from pathlib import Path
from subprocess import CalledProcessError, run
from subprocess import run, CalledProcessError
from sys import exit
from xml.etree import ElementTree as ET
from xml.etree import ElementTree
from .scriptlib import SimulTemplateEntity, find_files
import logging
logger = logging.getLogger(__name__)
@ -16,54 +15,37 @@ relaxng_schema = root / "scripts" / "entity.rng"
vfs_root = root
mod = "community-mod"
def main():
if not relaxng_schema.exists():
print(f"""Relax NG schema non existant.
Please create the file {relaxng_schema.relative_to(root)}
You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""")
exit(1)
if run(["xmllint", "--version"], capture_output=True, check=False).returncode != 0:
if run(['xmllint', '--version'], capture_output=True).returncode != 0:
print("xmllint not found in your PATH, please install it (usually in libxml2 package)")
exit(2)
parser = argparse.ArgumentParser(description="Validate templates")
parser.add_argument(
"-p", "--path", nargs="*", help="Optionally, a list of templates to validate."
)
parser = argparse.ArgumentParser(description='Validate templates')
parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of templates to validate.')
args = parser.parse_args()
simul_templates_path = Path("simulation/templates")
simul_templates_path = Path('simulation/templates')
simul_template_entity = SimulTemplateEntity(vfs_root, logger)
count = 0
failed = 0
templates = []
if args.path is not None:
templates = sorted([(Path(p), None) for p in args.path])
else:
templates = sorted(find_files(vfs_root, [mod], "simulation/templates", "xml"))
templates = sorted([(Path(p), None) for p in args.path]) if args.path else sorted(find_files(vfs_root, [mod], 'simulation/templates', 'xml'))
for fp, _ in templates:
if fp.stem.startswith("template_"):
if fp.stem.startswith('template_'):
continue
path = fp.as_posix()
if path.startswith(("simulation/templates/mixins/", "simulation/templates/special/")):
if path.startswith('simulation/templates/mixins/') or path.startswith("simulation/templates/special/"):
continue
print(f"# {fp}...")
count += 1
entity = simul_template_entity.load_inherited(
simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod]
)
xmlcontent = ET.tostring(entity, encoding="unicode")
entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod])
xmlcontent = ElementTree.tostring(entity, encoding='unicode')
try:
run(
["xmllint", "--relaxng", str(relaxng_schema.resolve()), "-"],
input=xmlcontent,
capture_output=True,
text=True,
check=True,
)
run(['xmllint', '--relaxng', str(relaxng_schema.resolve()), '-'], input=xmlcontent, capture_output=True, text=True, check=True)
except CalledProcessError as e:
failed += 1
print(e.stderr)
@ -71,5 +53,5 @@ You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""
print(f"\nTotal: {count}; failed: {failed}")
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -1,16 +1,13 @@
import json
from . import MOD_PATH, check_cwd_is_correct
def get_version():
with open(MOD_PATH / "mod.json") as f:
mod = json.load(f)
print(mod["version"])
with open(MOD_PATH / "mod.json", "r") as f:
mod = json.load(f)
print(mod['version'])
if __name__ == "__main__":
check_cwd_is_correct()
get_version()
if __name__ == '__main__':
check_cwd_is_correct()
get_version()
else:
raise Exception("Must be called directly")
raise Exception("Must be called directly")

14
scripts/make_pyromod.py Normal file
View File

@ -0,0 +1,14 @@
import shutil
from pathlib import Path
from . import MOD_PATH, check_cwd_is_correct
def make_pyromod():
shutil.make_archive(MOD_PATH, 'zip', MOD_PATH)
if __name__ == '__main__':
check_cwd_is_correct()
make_pyromod()
else:
raise Exception("Must be called directly")

View File

@ -1,127 +1,44 @@
import json
import os
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, 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"])
def get_commit_for_tag(tag):
"""Get the commit hash for a tag, or return None if the tag doesn't exist."""
try:
return run_git_command(["git", "rev-list", "-n", "1", tag])
except Exception as e:
print(f"Failed to get commit for tag {tag}: {e}")
return None
def get_previous_tag_in_series(tag):
"""Get the previous tag in the same minor version series (e.g., 0.26.X-1)."""
try:
version_array = tag.split(".")
# Extract the major.minor part of the tag (e.g., '0.26' from '0.26.11')
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()
# 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:
current_index = filtered_tags.index(tag)
if current_index + 1 < len(filtered_tags):
return filtered_tags[current_index + 1]
version_array[2] = str(int(version_array[2]) - 1)
manual_tag = ".".join(version_array)
if manual_tag not in filtered_tags:
return None
current_index = filtered_tags.index(manual_tag)
return filtered_tags[current_index]
except Exception as e:
raise Exception(f"Failed to get previous tag in series: {e}") from e
def get_changelog(current_commit, previous_tag):
"""Get the changelog between the current commit and the previous tag."""
try:
if previous_tag is not None:
return run_git_command(
["git", "log", f"{previous_tag}..{current_commit}", "--oneline"]
)
# No previous tag, so get the log from the start of the repository
return run_git_command(["git", "log", current_commit, "--oneline"])
except Exception as 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")
mod_file_path = os.getenv("MOD_FILE_PATH")
mod_version = os.getenv("MOD_VERSION")
mod_name = os.getenv("MOD_NAME")
community_mod_id = 2144305
zeroad_id = 5
mod_json = None
with open(MOD_PATH / "mod.json") as f:
mod_json = json.load(f)
headers = {
'Authorization': f'Bearer {oauth2_token}',
'Accept': 'application/json'
}
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.")
commit = get_current_commit()
previous_tag = get_previous_tag_in_series(tag)
changelog = get_changelog(commit, previous_tag)
print(changelog)
headers = {"Authorization": f"Bearer {oauth2_token}", "Accept": "application/json"}
r = requests.get(
"https://api.mod.io/v1/me/mods", timeout=5, params={"api_key": api_key}, headers=headers
)
r = requests.get(f'https://api.mod.io/v1/me/mods', params={
'api_key': api_key
}, headers = headers)
print(r.json())
with open(mod_file_path, "rb") as f:
files = {"filedata": f}
signature = ""
with open(f"{mod_name}-{mod_version}.zip.minisign", encoding="utf-8") as f:
signature = f.read()
files = {'filedata': open(mod_file_path, 'rb')}
rq = requests.post(
f"https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files",
files=files,
headers=headers,
timeout=500,
data={
"version": mod_version,
"active": True,
"changelog": changelog,
"metadata_blob": json.dumps(
{"dependencies": mod_json["dependencies"], "minisigs": [signature]}
),
},
)
mod_json = json.load(open(MOD_PATH / 'mod.json', 'r'))
print(rq.json())
signature = open('signature.minisign', 'r').read()
rq = requests.post(f'https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files', files=files, headers=headers, data={
'version': mod_version,
'active': True,
'metadata_blob' : json.dumps({
'dependencies': mod_json['dependencies'],
'minisigs': [signature]
})
})
print(rq.json())

View File

@ -1,9 +1,9 @@
from collections import Counter
from decimal import Decimal
from os.path import exists
from re import split
from xml.etree import ElementTree as ET
from sys import stderr
from xml.etree import ElementTree
from os.path import exists
class SimulTemplateEntity:
def __init__(self, vfs_root, logger):
@ -12,11 +12,11 @@ class SimulTemplateEntity:
def get_file(self, base_path, vfs_path, mod):
default_path = self.vfs_root / mod / base_path
file = (default_path / "special" / "filter" / vfs_path).with_suffix(".xml")
file = (default_path/ "special" / "filter" / vfs_path).with_suffix('.xml')
if not exists(file):
file = (default_path / "mixins" / vfs_path).with_suffix(".xml")
file = (default_path / "mixins" / vfs_path).with_suffix('.xml')
if not exists(file):
file = (default_path / vfs_path).with_suffix(".xml")
file = (default_path / vfs_path).with_suffix('.xml')
return file
def get_main_mod(self, base_path, vfs_path, mods):
@ -33,110 +33,107 @@ class SimulTemplateEntity:
return main_mod
def apply_layer(self, base_tag, tag):
"""Apply tag layer to base_tag."""
if tag.get("datatype") == "tokens":
base_tokens = split(r"\s+", base_tag.text or "")
tokens = split(r"\s+", tag.text or "")
"""
apply tag layer to base_tag
"""
if tag.get('datatype') == 'tokens':
base_tokens = split(r'\s+', base_tag.text or '')
tokens = split(r'\s+', tag.text or '')
final_tokens = base_tokens.copy()
for token in tokens:
if token.startswith("-"):
if token.startswith('-'):
token_to_remove = token[1:]
if token_to_remove in final_tokens:
final_tokens.remove(token_to_remove)
elif token not in final_tokens:
final_tokens.append(token)
base_tag.text = " ".join(final_tokens)
base_tag.text = ' '.join(final_tokens)
base_tag.set("datatype", "tokens")
elif tag.get("op"):
op = tag.get("op")
op1 = Decimal(base_tag.text or "0")
op2 = Decimal(tag.text or "0")
elif tag.get('op'):
op = tag.get('op')
op1 = Decimal(base_tag.text or '0')
op2 = Decimal(tag.text or '0')
# Try converting to integers if possible, to pass validation.
if op == "add":
if op == 'add':
base_tag.text = str(int(op1 + op2) if int(op1 + op2) == op1 + op2 else op1 + op2)
elif op == "mul":
elif op == 'mul':
base_tag.text = str(int(op1 * op2) if int(op1 * op2) == op1 * op2 else op1 * op2)
elif op == "mul_round":
elif op == 'mul_round':
base_tag.text = str(round(op1 * op2))
else:
raise ValueError(f"Invalid operator '{op}'")
else:
base_tag.text = tag.text
for prop in tag.attrib:
if prop not in ("disable", "replace", "parent", "merge"):
if prop not in ('disable', 'replace', 'parent', 'merge'):
base_tag.set(prop, tag.get(prop))
for child in tag:
base_child = base_tag.find(child.tag)
if "disable" in child.attrib:
if 'disable' in child.attrib:
if base_child is not None:
base_tag.remove(base_child)
elif ("merge" not in child.attrib) or (base_child is not None):
if "replace" in child.attrib and base_child is not None:
elif ('merge' not in child.attrib) or (base_child is not None):
if 'replace' in child.attrib and base_child is not None:
base_tag.remove(base_child)
base_child = None
if base_child is None:
base_child = ET.Element(child.tag)
base_child = ElementTree.Element(child.tag)
base_tag.append(base_child)
self.apply_layer(base_child, child)
if "replace" in base_child.attrib:
del base_child.attrib["replace"]
if 'replace' in base_child.attrib:
del base_child.attrib['replace']
def load_inherited(self, base_path, vfs_path, mods):
entity = self._load_inherited(base_path, vfs_path, mods)
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)
self._load_inherited(base_path, paths[1], mods, base)
return self._load_inherited(base_path, paths[0], mods, base)
base = self._load_inherited(base_path, paths[1], mods, base);
base = self._load_inherited(base_path, paths[0], mods, base);
return base
main_mod = self.get_main_mod(base_path, vfs_path, mods)
fp = self.get_file(base_path, vfs_path, main_mod)
layer = ET.parse(fp).getroot()
layer = ElementTree.parse(fp).getroot()
for el in layer.iter():
children = [x.tag for x in el]
duplicates = [x for x, c in Counter(children).items() if c > 1]
if duplicates:
for dup in duplicates:
self.logger.warning(
"Duplicate child node '%s' in tag %s of %s", dup, el.tag, fp
)
if layer.get("parent"):
parent = self._load_inherited(base_path, layer.get("parent"), mods, base)
self.logger.warning(f"Duplicate child node '{dup}' in tag {el.tag} of {fp}")
if layer.get('parent'):
parent = self._load_inherited(base_path, layer.get('parent'), mods, base)
self.apply_layer(parent, layer)
return parent
if not base:
return layer
self.apply_layer(base, layer)
return base
else:
if not base:
return layer
else:
self.apply_layer(base, layer)
return base
def find_files(vfs_root, mods, vfs_path, *ext_list):
"""Return a list of 2-size tuples.
Each tuple contains:
- 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):
"""(relative Path, full Path) generator."""
"""(relative Path, full Path) generator"""
if dp.is_dir():
if dp.name not in (".svn", ".git") and not dp.name.endswith("~"):
if dp.name != '.svn' and dp.name != '.git' and not dp.name.endswith('~'):
for fp in dp.iterdir():
yield from find_recursive(fp, base)
elif dp.suffix in full_exts:
relative_file_path = dp.relative_to(base)
yield (relative_file_path, dp.resolve())
return [
(rp, fp)
for mod in mods
for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)
]
return [(rp, fp) for mod in mods for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)]