Compare commits
26 Commits
04aa01a39b
...
57308bb847
Author | SHA1 | Date | |
---|---|---|---|
57308bb847 | |||
c59030857d | |||
8d70ced693 | |||
0ea6d32fa5 | |||
c0232c6b5f | |||
265ed76131 | |||
668ae8a20e | |||
b15eb6909e | |||
798cff1f6f | |||
230c7ca27d | |||
e56ebb3f46 | |||
cd8b4266a4 | |||
8c7cc7373d | |||
0d3e3fbc29 | |||
661328ab15 | |||
616f2e134b | |||
ea4b580527 | |||
0e84957979 | |||
eeb502c115 | |||
f4c40b740c | |||
20ab96a0f4 | |||
ac48b72550 | |||
4d3be23bac | |||
f856a7663f | |||
7575dfe3c8 | |||
e86fd58524 |
@ -5,6 +5,10 @@ charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
||||
[*.sh]
|
||||
indent_style = tab
|
||||
function_next_line = true
|
||||
|
@ -3,6 +3,7 @@
|
||||
\\.gitea/.* @Stan @Itms
|
||||
## Linting
|
||||
\\.pre-commit-config\\.yaml @Dunedan
|
||||
ruff\\.toml @Dunedan
|
||||
|
||||
## == Build & Libraries
|
||||
(build|libraries)/.* @Itms @Stan
|
||||
|
@ -30,8 +30,6 @@ repos:
|
||||
- id: ruff-format
|
||||
args:
|
||||
- --check
|
||||
- --target-version
|
||||
- py311
|
||||
exclude: ^source/tools/webservices/
|
||||
- repo: local
|
||||
hooks:
|
||||
|
@ -1219,16 +1219,28 @@ function getResourceDropsiteTooltip(template)
|
||||
});
|
||||
}
|
||||
|
||||
function showTemplateViewerOnRightClickTooltip()
|
||||
function getFocusOnLeftClickTooltip()
|
||||
{
|
||||
// Translation: Appears in a tooltip to indicate that right-clicking the corresponding GUI element will open the Template Details GUI page.
|
||||
return translate("Right-click to view more information.");
|
||||
// Translation: Appears in a tooltip to indicate that left-clicking the corresponding GUI element will center the view on the selected entity.
|
||||
return translate("Left-click to focus.");
|
||||
}
|
||||
|
||||
function showTemplateViewerOnClickTooltip()
|
||||
function getFollowOnLeftClickTooltip()
|
||||
{
|
||||
// Translation: Appears in a tooltip to indicate that left-clicking the corresponding GUI element will make the camera follow the selected unit.
|
||||
return translate("Left-click to follow.");
|
||||
}
|
||||
|
||||
function getTemplateViewerOnRightClickTooltip()
|
||||
{
|
||||
// Translation: Appears in a tooltip to indicate that right-clicking the corresponding GUI element will open the Template Details GUI page.
|
||||
return translate("Right-click for more information.");
|
||||
}
|
||||
|
||||
function getTemplateViewerOnClickTooltip()
|
||||
{
|
||||
// Translation: Appears in a tooltip to indicate that clicking the corresponding GUI element will open the Template Details GUI page.
|
||||
return translate("Click to view more information.");
|
||||
return translate("Click for more information.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +45,7 @@ class EntityBox
|
||||
|
||||
static compileTooltip(template)
|
||||
{
|
||||
return ReferencePage.buildText(template, this.prototype.TooltipFunctions) + "\n" + showTemplateViewerOnClickTooltip();
|
||||
return ReferencePage.buildText(template, this.prototype.TooltipFunctions) + "\n" + getTemplateViewerOnClickTooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,7 @@
|
||||
<!-- Background circle -->
|
||||
<object
|
||||
type="image"
|
||||
size="4 4 100%-4 100%-4"
|
||||
size="4 6 100%-4 100%-6"
|
||||
sprite="stretched:session/minimap_circle_modern.png"
|
||||
ghost="true"
|
||||
/>
|
||||
@ -24,7 +24,7 @@
|
||||
<!-- Idle Worker Button -->
|
||||
<object name="idleWorkerButton"
|
||||
type="button"
|
||||
size="100%-119 100%-120 100%-4 100%-5"
|
||||
size="100%-119 100%-121 100%-4 100%-6"
|
||||
tooltip_style="sessionToolTip"
|
||||
hotkey="selection.idleworker"
|
||||
sprite="stretched:session/minimap-idle.png"
|
||||
@ -45,7 +45,7 @@
|
||||
<!-- Diplomacy Colors Button -->
|
||||
<object name="diplomacyColorsButton"
|
||||
type="button"
|
||||
size="4 100%-120 119 100%-5"
|
||||
size="3 100%-121 118 100%-6"
|
||||
tooltip_style="sessionToolTip"
|
||||
hotkey="session.diplomacycolors"
|
||||
/>
|
||||
@ -54,7 +54,7 @@
|
||||
<object
|
||||
name="flareButton"
|
||||
type="button"
|
||||
size="3 3 118 118"
|
||||
size="2 4 117 119"
|
||||
tooltip_style="sessionToolTip"
|
||||
hotkey="session.flareactivate"
|
||||
sprite="stretched:session/minimap-flare.png"
|
||||
@ -66,7 +66,7 @@
|
||||
<!-- MiniMap -->
|
||||
<object
|
||||
name="minimap"
|
||||
size="8 8 100%-8 100%-8"
|
||||
size="8 10 100%-8 100%-10"
|
||||
type="minimap"
|
||||
mask="true"
|
||||
flare_texture_count="16"
|
||||
|
@ -479,8 +479,8 @@ EntitySelection.prototype.selectAndMoveTo = function(entityID)
|
||||
this.reset();
|
||||
this.addList([entityID]);
|
||||
|
||||
Engine.CameraMoveTo(entState.position.x, entState.position.z);
|
||||
}
|
||||
setCameraFollow(entityID);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the formation members of a selected entities to the selection.
|
||||
|
@ -324,9 +324,17 @@ function displaySingle(entState)
|
||||
// TODO: we should require all entities to have icons
|
||||
Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "BackgroundBlack";
|
||||
if (template.icon)
|
||||
Engine.GetGUIObjectByName("iconBorder").onPressRight = () => {
|
||||
{
|
||||
const iconBorder = Engine.GetGUIObjectByName("iconBorder");
|
||||
|
||||
iconBorder.onPress = () => {
|
||||
setCameraFollow(entState.id);
|
||||
};
|
||||
|
||||
iconBorder.onPressRight = () => {
|
||||
showTemplateDetails(entState.template, playerState.civ);
|
||||
};
|
||||
}
|
||||
|
||||
let detailedTooltip = [
|
||||
getAttackTooltip,
|
||||
@ -357,10 +365,12 @@ function displaySingle(entState)
|
||||
getVisibleEntityClassesFormatted,
|
||||
getAurasTooltip,
|
||||
getEntityTooltip,
|
||||
getTreasureTooltip,
|
||||
showTemplateViewerOnRightClickTooltip
|
||||
getTreasureTooltip
|
||||
].map(func => func(template)));
|
||||
|
||||
const leftClickTooltip = hasClass(entState, "Unit") ? getFollowOnLeftClickTooltip() : getFocusOnLeftClickTooltip();
|
||||
iconTooltips.push(leftClickTooltip + " " + getTemplateViewerOnRightClickTooltip());
|
||||
|
||||
Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltips.filter(tip => tip).join("\n");
|
||||
|
||||
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
|
||||
|
@ -207,7 +207,7 @@ g_SelectionPanels.Construction = {
|
||||
getGarrisonTooltip(template),
|
||||
getTurretsTooltip(template),
|
||||
getPopulationBonusTooltip(template),
|
||||
showTemplateViewerOnRightClickTooltip(template)
|
||||
getTemplateViewerOnRightClickTooltip(template)
|
||||
);
|
||||
|
||||
|
||||
@ -578,7 +578,7 @@ g_SelectionPanels.Queue = {
|
||||
"neededSlots": queuedItem.neededSlots
|
||||
}));
|
||||
}
|
||||
tooltips.push(showTemplateViewerOnRightClickTooltip(template));
|
||||
tooltips.push(getTemplateViewerOnRightClickTooltip(template));
|
||||
data.button.tooltip = tooltips.join("\n");
|
||||
|
||||
data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";
|
||||
@ -763,7 +763,7 @@ g_SelectionPanels.Research = {
|
||||
getEntityNamesFormatted,
|
||||
getEntityTooltip,
|
||||
getEntityCostTooltip,
|
||||
showTemplateViewerOnRightClickTooltip
|
||||
getTemplateViewerOnRightClickTooltip
|
||||
].map(func => func(template));
|
||||
|
||||
if (!requirementsPassed)
|
||||
@ -1058,7 +1058,7 @@ g_SelectionPanels.Training = {
|
||||
getResourceDropsiteTooltip
|
||||
].map(func => func(template)));
|
||||
|
||||
tooltips.push(showTemplateViewerOnRightClickTooltip());
|
||||
tooltips.push(getTemplateViewerOnRightClickTooltip());
|
||||
tooltips.push(
|
||||
formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch),
|
||||
getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ),
|
||||
@ -1180,7 +1180,8 @@ g_SelectionPanels.Upgrade = {
|
||||
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
|
||||
getRequirementsTooltip(requirementsMet, data.item.requirements, GetSimState().players[data.player].civ),
|
||||
getNeededResourcesTooltip(neededResources),
|
||||
showTemplateViewerOnRightClickTooltip());
|
||||
getTemplateViewerOnRightClickTooltip()
|
||||
);
|
||||
|
||||
tooltip = tooltips.filter(tip => tip).join("\n");
|
||||
|
||||
|
@ -386,14 +386,16 @@ function cancelUpgradeEntity()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the camera to follow the given entity if it's a unit.
|
||||
* Otherwise stop following.
|
||||
* Focus the camera on the entity and follow if it's a unit.
|
||||
* If that's not possible, stop any current follow.
|
||||
*/
|
||||
function setCameraFollow(entity)
|
||||
{
|
||||
let entState = entity && GetEntityState(entity);
|
||||
const entState = entity && GetEntityState(entity);
|
||||
if (entState && hasClass(entState, "Unit"))
|
||||
Engine.CameraFollow(entity);
|
||||
else if (entState?.position)
|
||||
Engine.CameraMoveTo(entState.position.x, entState.position.z);
|
||||
else
|
||||
Engine.CameraFollow(0);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
>
|
||||
|
||||
<!-- Names and civilization emblem etc. (This must come before the attack and resistance icon to avoid clipping issues.) -->
|
||||
<object size="0 92 100% 100%" name="statsArea" type="image" sprite="edgedPanelShader">
|
||||
<object size="0 96 100% 100%" name="statsArea" type="image" sprite="edgedPanelShader">
|
||||
<!-- Civilization tooltip. -->
|
||||
<object size="0 38 100% 62" tooltip_style="sessionToolTip">
|
||||
<!-- Civilization emblem. -->
|
||||
@ -26,14 +26,14 @@
|
||||
</object>
|
||||
|
||||
<!-- Stats Bars -->
|
||||
<object size= "0 0 100% 96" type="image" tooltip_style="sessionToolTip">
|
||||
<object size= "0 0 100% 100" type="image" tooltip_style="sessionToolTip">
|
||||
|
||||
<object size="0 0 100% 60" type="image" sprite="topEdgedPanelShader">
|
||||
<object size="0 0 100% 65" type="image" sprite="topEdgedPanelShader">
|
||||
|
||||
<!-- Placeholders storing the position for the bars. -->
|
||||
<object size="96 1 100% 24" name="sectionPosTop" hidden="true"/>
|
||||
<object size="96 11 100% 34" name="sectionPosMiddle" hidden="true"/>
|
||||
<object size="96 32 100% 55" name="sectionPosBottom" hidden="true"/>
|
||||
<object size="96 2 100% 24" name="sectionPosTop" hidden="true"/>
|
||||
<object size="96 12 100% 34" name="sectionPosMiddle" hidden="true"/>
|
||||
<object size="96 35 100% 55" name="sectionPosBottom" hidden="true"/>
|
||||
|
||||
<!-- Capture bar -->
|
||||
<object name="captureSection">
|
||||
@ -78,15 +78,15 @@
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object size="0 59 100% 95" type="image" sprite="edgedPanelShader">
|
||||
<object size="0 63 100% 98" type="image" sprite="edgedPanelShader">
|
||||
<!-- Attack and Resistance -->
|
||||
<object size="96 0 128 32" name="attackAndResistanceStats" type="image" sprite="stretched:session/icons/stances/defensive.png" tooltip_style="sessionToolTipInstantly">
|
||||
<object size="97 0 129 32" name="attackAndResistanceStats" type="image" sprite="stretched:session/icons/stances/defensive.png" tooltip_style="sessionToolTipInstantly">
|
||||
<translatableAttribute id="tooltip">Attack and Resistance</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<!-- Resource carrying icon/counter -->
|
||||
<!-- Used also for number of gatherers/builders -->
|
||||
<object size="100%-96 0 100%-36 32" type="text" name="resourceCarryingText" style="CarryingTextRight"/>
|
||||
<object size="100%-96 0 100%-33 32" type="text" name="resourceCarryingText" style="CarryingTextRight"/>
|
||||
<object size="100%-32 0 100% 32" type="image" name="resourceCarryingIcon" tooltip_style="sessionToolTip"/>
|
||||
</object>
|
||||
|
||||
|
@ -79,7 +79,7 @@
|
||||
<!-- Limit to the minimal supported width of 1024 pixels. -->
|
||||
<object size="50%-512 0 50%+512 100%">
|
||||
|
||||
<object size="50%-512 100%-200 50%-312 100%">
|
||||
<object size="50%-512 100%-204 50%-312 100%">
|
||||
<include file="gui/session/minimap/MiniMap.xml"/>
|
||||
</object>
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
|
||||
<!-- Selection Details Panel (middle). -->
|
||||
<object name="selectionDetails"
|
||||
size="50%-114 100%-200 50%+114 100%"
|
||||
size="50%-114 100%-204 50%+114 100%"
|
||||
sprite="selectionDetailsPanel"
|
||||
type="image"
|
||||
>
|
||||
|
@ -53,7 +53,10 @@ pipeline {
|
||||
|
||||
stage("Check for shader changes") {
|
||||
when {
|
||||
changeset 'binaries/data/mods/**/shaders/**/*.xml'
|
||||
anyOf {
|
||||
changeset 'binaries/data/mods/**/shaders/**/*.xml'
|
||||
changeset 'source/tools/spirv/compile.py'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
script { buildSPIRV = true }
|
||||
|
14
ruff.toml
14
ruff.toml
@ -1,5 +1,7 @@
|
||||
line-length = 99
|
||||
|
||||
target-version = "py311"
|
||||
|
||||
[format]
|
||||
line-ending = "lf"
|
||||
|
||||
@ -10,36 +12,29 @@ ignore = [
|
||||
"C90",
|
||||
"COM812",
|
||||
"D10",
|
||||
"DTZ005",
|
||||
"EM",
|
||||
"FA",
|
||||
"FIX",
|
||||
"FBT",
|
||||
"ISC001",
|
||||
"N",
|
||||
"PERF203",
|
||||
"N817",
|
||||
"PERF401",
|
||||
"PLR0912",
|
||||
"PLR0913",
|
||||
"PLR0915",
|
||||
"PLR2004",
|
||||
"PLW2901",
|
||||
"PT",
|
||||
"PTH",
|
||||
"RUF012",
|
||||
"S101",
|
||||
"S310",
|
||||
"S314",
|
||||
"S324",
|
||||
"S320",
|
||||
"S603",
|
||||
"S607",
|
||||
"T20",
|
||||
"TD002",
|
||||
"TD003",
|
||||
"TRY002",
|
||||
"TRY003",
|
||||
"TRY004",
|
||||
"UP038",
|
||||
"W505"
|
||||
]
|
||||
@ -52,3 +47,6 @@ max-doc-length = 72
|
||||
|
||||
[lint.pydocstyle]
|
||||
convention = "pep257"
|
||||
|
||||
[lint.pylint]
|
||||
max-args = 8
|
||||
|
1
source/tools/dist/dmgbuild-settings.py
vendored
1
source/tools/dist/dmgbuild-settings.py
vendored
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os.path
|
||||
|
||||
#
|
||||
|
@ -8,7 +8,6 @@ from json import load, loads
|
||||
from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger
|
||||
from pathlib import Path
|
||||
from struct import calcsize, unpack
|
||||
from typing import Dict, List, Set, Tuple
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from scriptlib import SimulTemplateEntity, find_files
|
||||
@ -27,9 +26,9 @@ class SingleLevelFilter(Filter):
|
||||
|
||||
class CheckRefs:
|
||||
def __init__(self):
|
||||
self.files: List[Path] = []
|
||||
self.roots: List[Path] = []
|
||||
self.deps: List[Tuple[Path, Path]] = []
|
||||
self.files: list[Path] = []
|
||||
self.roots: list[Path] = []
|
||||
self.deps: list[tuple[Path, Path]] = []
|
||||
self.vfs_root = Path(__file__).resolve().parents[3] / "binaries" / "data" / "mods"
|
||||
self.supportedTextureFormats = ("dds", "png")
|
||||
self.supportedMeshesFormats = ("pmd", "dae")
|
||||
@ -387,8 +386,8 @@ class CheckRefs:
|
||||
|
||||
cmp_auras = entity.find("Auras")
|
||||
if cmp_auras is not None:
|
||||
auraString = cmp_auras.text
|
||||
for aura in auraString.split():
|
||||
aura_string = cmp_auras.text
|
||||
for aura in aura_string.split():
|
||||
if not aura:
|
||||
continue
|
||||
if aura.startswith("-"):
|
||||
@ -397,33 +396,33 @@ class CheckRefs:
|
||||
|
||||
cmp_identity = entity.find("Identity")
|
||||
if cmp_identity is not None:
|
||||
reqTag = cmp_identity.find("Requirements")
|
||||
if reqTag is not None:
|
||||
req_tag = cmp_identity.find("Requirements")
|
||||
if req_tag is not None:
|
||||
|
||||
def parse_requirements(fp, req, recursionDepth=1):
|
||||
techsTag = req.find("Techs")
|
||||
if techsTag is not None:
|
||||
for techTag in techsTag.text.split():
|
||||
def parse_requirements(fp, req, recursion_depth=1):
|
||||
techs_tag = req.find("Techs")
|
||||
if techs_tag is not None:
|
||||
for tech_tag in techs_tag.text.split():
|
||||
self.deps.append(
|
||||
(fp, Path(f"simulation/data/technologies/{techTag}.json"))
|
||||
(fp, Path(f"simulation/data/technologies/{tech_tag}.json"))
|
||||
)
|
||||
|
||||
if recursionDepth > 0:
|
||||
recursionDepth -= 1
|
||||
allReqTag = req.find("All")
|
||||
if allReqTag is not None:
|
||||
parse_requirements(fp, allReqTag, recursionDepth)
|
||||
anyReqTag = req.find("Any")
|
||||
if anyReqTag is not None:
|
||||
parse_requirements(fp, anyReqTag, recursionDepth)
|
||||
if recursion_depth > 0:
|
||||
recursion_depth -= 1
|
||||
all_req_tag = req.find("All")
|
||||
if all_req_tag is not None:
|
||||
parse_requirements(fp, all_req_tag, recursion_depth)
|
||||
any_req_tag = req.find("Any")
|
||||
if any_req_tag is not None:
|
||||
parse_requirements(fp, any_req_tag, recursion_depth)
|
||||
|
||||
parse_requirements(fp, reqTag)
|
||||
parse_requirements(fp, req_tag)
|
||||
|
||||
cmp_researcher = entity.find("Researcher")
|
||||
if cmp_researcher is not None:
|
||||
techString = cmp_researcher.find("Technologies")
|
||||
if techString is not None:
|
||||
for tech in techString.text.split():
|
||||
tech_string = cmp_researcher.find("Technologies")
|
||||
if tech_string is not None:
|
||||
for tech in tech_string.text.split():
|
||||
if not tech:
|
||||
continue
|
||||
if tech.startswith("-"):
|
||||
@ -794,7 +793,7 @@ class CheckRefs:
|
||||
uniq_files = {r.as_posix() for r in self.files}
|
||||
lower_case_files = {f.lower(): f for f in uniq_files}
|
||||
|
||||
missing_files: Dict[str, Set[str]] = defaultdict(set)
|
||||
missing_files: dict[str, set[str]] = defaultdict(set)
|
||||
|
||||
for parent, dep in self.deps:
|
||||
dep_str = dep.as_posix()
|
||||
|
@ -7,7 +7,6 @@ import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError, run
|
||||
from typing import Sequence
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from scriptlib import SimulTemplateEntity, find_files
|
||||
@ -50,7 +49,7 @@ errorch.setFormatter(logging.Formatter("%(levelname)s - %(message)s"))
|
||||
logger.addHandler(errorch)
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate templates")
|
||||
parser.add_argument("-m", "--mod-name", required=True, help="The name of the mod to validate.")
|
||||
parser.add_argument(
|
||||
@ -73,7 +72,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", help="Be verbose about the output.", default=False)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.relaxng_schema.exists():
|
||||
logging.error(RELAXNG_SCHEMA_ERROR_MSG.format(args.relaxng_schema))
|
||||
|
@ -1,7 +1,10 @@
|
||||
# Adapted from http://cairographics.org/freetypepython/
|
||||
|
||||
# ruff: noqa: TRY002
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
from typing import ClassVar
|
||||
|
||||
import cairo
|
||||
|
||||
@ -40,7 +43,7 @@ _surface = cairo.ImageSurface(cairo.FORMAT_A8, 0, 0)
|
||||
|
||||
|
||||
class PycairoContext(ctypes.Structure):
|
||||
_fields_ = [
|
||||
_fields_: ClassVar = [
|
||||
("PyObject_HEAD", ctypes.c_byte * object.__basicsize__),
|
||||
("ctx", ctypes.c_void_p),
|
||||
("base", ctypes.c_void_p),
|
||||
|
@ -22,7 +22,6 @@ import io
|
||||
import os
|
||||
import subprocess
|
||||
from itertools import islice
|
||||
from typing import List
|
||||
|
||||
from i18n_helper import PROJECT_ROOT_DIRECTORY
|
||||
|
||||
@ -38,7 +37,7 @@ def get_diff():
|
||||
return io.StringIO(diff_process.stdout.decode("utf-8"))
|
||||
|
||||
|
||||
def check_diff(diff: io.StringIO) -> List[str]:
|
||||
def check_diff(diff: io.StringIO) -> list[str]:
|
||||
"""Check a diff of .po files for meaningful changes.
|
||||
|
||||
Run through a diff of .po files and check that some of the changes
|
||||
@ -87,7 +86,7 @@ def check_diff(diff: io.StringIO) -> List[str]:
|
||||
return list(files.difference(keep))
|
||||
|
||||
|
||||
def revert_files(files: List[str], verbose=False):
|
||||
def revert_files(files: list[str], verbose=False):
|
||||
def batched(iterable, n):
|
||||
"""Split an iterable in equally sized chunks.
|
||||
|
||||
|
@ -27,51 +27,64 @@ However that needs to be fixed on the transifex side, see rP25896. For now
|
||||
strip the e-mails using this script.
|
||||
"""
|
||||
|
||||
import fileinput
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from i18n_helper import L10N_FOLDER_NAME, PROJECT_ROOT_DIRECTORY, TRANSIFEX_CLIENT_FOLDER
|
||||
|
||||
|
||||
TRANSLATOR_REGEX = re.compile(r"^(#\s+[^,<]*)\s+<.*>(.*)$")
|
||||
LAST_TRANSLATION_REGEX = re.compile(r"^(\"Last-Translator:[^,<]*)\s+<.*>(.*)$")
|
||||
|
||||
|
||||
def main():
|
||||
translator_match = re.compile(r"^(#\s+[^,<]*)\s+<.*>(.*)")
|
||||
last_translator_match = re.compile(r"^(\"Last-Translator:[^,<]*)\s+<.*>(.*)")
|
||||
for folder in glob.iglob(
|
||||
f"**/{L10N_FOLDER_NAME}/{TRANSIFEX_CLIENT_FOLDER}/",
|
||||
root_dir=PROJECT_ROOT_DIRECTORY,
|
||||
recursive=True,
|
||||
):
|
||||
for file in glob.iglob(
|
||||
f"{os.path.join(folder, os.pardir)}/*.po", root_dir=PROJECT_ROOT_DIRECTORY
|
||||
):
|
||||
absolute_file_path = os.path.abspath(f"{PROJECT_ROOT_DIRECTORY}/{file}")
|
||||
|
||||
for root, folders, _ in os.walk(PROJECT_ROOT_DIRECTORY):
|
||||
for folder in folders:
|
||||
if folder != L10N_FOLDER_NAME:
|
||||
continue
|
||||
|
||||
if not os.path.exists(os.path.join(root, folder, TRANSIFEX_CLIENT_FOLDER)):
|
||||
continue
|
||||
|
||||
path = os.path.join(root, folder, "*.po")
|
||||
files = glob.glob(path)
|
||||
for file in files:
|
||||
usernames = []
|
||||
reached = False
|
||||
for line in fileinput.input(
|
||||
file.replace("\\", "/"), inplace=True, encoding="utf-8"
|
||||
):
|
||||
if reached:
|
||||
file_content = []
|
||||
usernames = []
|
||||
changes = False
|
||||
in_translators = False
|
||||
found_last_translator = False
|
||||
with open(absolute_file_path, "r+", encoding="utf-8") as fd:
|
||||
for line in fd:
|
||||
if line.strip() == "# Translators:":
|
||||
in_translators = True
|
||||
elif not line.strip().startswith("#"):
|
||||
in_translators = False
|
||||
elif in_translators:
|
||||
if line == "# \n":
|
||||
line = ""
|
||||
m = translator_match.match(line)
|
||||
if m:
|
||||
if m.group(1) in usernames:
|
||||
line = ""
|
||||
else:
|
||||
line = m.group(1) + m.group(2) + "\n"
|
||||
usernames.append(m.group(1))
|
||||
m2 = last_translator_match.match(line)
|
||||
if m2:
|
||||
line = re.sub(last_translator_match, r"\1\2", line)
|
||||
elif line.strip() == "# Translators:":
|
||||
reached = True
|
||||
sys.stdout.write(line)
|
||||
changes = True
|
||||
continue
|
||||
translator_match = TRANSLATOR_REGEX.match(line)
|
||||
if translator_match:
|
||||
changes = True
|
||||
if translator_match.group(1) in usernames:
|
||||
continue
|
||||
line = TRANSLATOR_REGEX.sub(r"\1\2", line)
|
||||
usernames.append(translator_match.group(1))
|
||||
|
||||
if not in_translators and not found_last_translator:
|
||||
last_translator_match = LAST_TRANSLATION_REGEX.match(line)
|
||||
if last_translator_match:
|
||||
found_last_translator = True
|
||||
changes = True
|
||||
line = LAST_TRANSLATION_REGEX.sub(r"\1\2", line)
|
||||
|
||||
file_content.append(line)
|
||||
|
||||
if changes:
|
||||
fd.seek(0)
|
||||
fd.truncate()
|
||||
fd.writelines(file_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Wrapper around babel Catalog / .po handling."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from babel.messages.catalog import Catalog as BabelCatalog
|
||||
from babel.messages.pofile import read_po, write_po
|
||||
@ -10,7 +10,7 @@ class Catalog(BabelCatalog):
|
||||
"""Wraps a BabelCatalog for convenience."""
|
||||
|
||||
def __init__(self, *args, project=None, copyright_holder=None, **other_kwargs):
|
||||
date = datetime.now()
|
||||
date = datetime.now(tz=UTC)
|
||||
super().__init__(
|
||||
*args,
|
||||
header_comment=(
|
||||
|
@ -23,13 +23,32 @@
|
||||
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import codecs
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from textwrap import dedent
|
||||
|
||||
from babel.messages.jslexer import tokenize, unquote_string
|
||||
from lxml import etree
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_mask_pattern(mask: str) -> re.Pattern:
|
||||
"""Build a regex pattern for matching file paths."""
|
||||
parts = re.split(r"([*][*]?)", mask)
|
||||
pattern = ""
|
||||
for i, part in enumerate(parts):
|
||||
if i % 2 != 0:
|
||||
pattern += "[^/]+"
|
||||
if len(part) == 2:
|
||||
pattern += "(/[^/]+)*"
|
||||
else:
|
||||
pattern += re.escape(part)
|
||||
pattern += "$"
|
||||
return re.compile(pattern)
|
||||
|
||||
|
||||
def pathmatch(mask, path):
|
||||
"""Match paths to a mask, where the mask supports * and **.
|
||||
@ -38,32 +57,22 @@ def pathmatch(mask, path):
|
||||
* matches a sequence of characters without /.
|
||||
** matches a sequence of characters without / followed by a / and
|
||||
sequence of characters without /
|
||||
:return: true iff path matches the mask, false otherwise
|
||||
:return: true if path matches the mask, false otherwise
|
||||
"""
|
||||
s = re.split(r"([*][*]?)", mask)
|
||||
p = ""
|
||||
for i in range(len(s)):
|
||||
if i % 2 != 0:
|
||||
p = p + "[^/]+"
|
||||
if len(s[i]) == 2:
|
||||
p = p + "(/[^/]+)*"
|
||||
else:
|
||||
p = p + re.escape(s[i])
|
||||
p = p + "$"
|
||||
return re.match(p, path) is not None
|
||||
return get_mask_pattern(mask).match(path) is not None
|
||||
|
||||
|
||||
class Extractor:
|
||||
def __init__(self, directory_path, filemasks, options):
|
||||
self.directoryPath = directory_path
|
||||
self.directory_path = directory_path
|
||||
self.options = options
|
||||
|
||||
if isinstance(filemasks, dict):
|
||||
self.includeMasks = filemasks["includeMasks"]
|
||||
self.excludeMasks = filemasks["excludeMasks"]
|
||||
self.include_masks = filemasks["includeMasks"]
|
||||
self.exclude_masks = filemasks["excludeMasks"]
|
||||
else:
|
||||
self.includeMasks = filemasks
|
||||
self.excludeMasks = []
|
||||
self.include_masks = filemasks
|
||||
self.exclude_masks = []
|
||||
|
||||
def run(self):
|
||||
"""Extract messages.
|
||||
@ -73,7 +82,7 @@ class Extractor:
|
||||
:rtype: ``iterator``
|
||||
"""
|
||||
empty_string_pattern = re.compile(r"^\s*$")
|
||||
directory_absolute_path = os.path.abspath(self.directoryPath)
|
||||
directory_absolute_path = os.path.abspath(self.directory_path)
|
||||
for root, folders, filenames in os.walk(directory_absolute_path):
|
||||
for subdir in folders:
|
||||
if subdir.startswith((".", "_")):
|
||||
@ -82,28 +91,26 @@ class Extractor:
|
||||
filenames.sort()
|
||||
for filename in filenames:
|
||||
filename = os.path.relpath(
|
||||
os.path.join(root, filename), self.directoryPath
|
||||
os.path.join(root, filename), self.directory_path
|
||||
).replace(os.sep, "/")
|
||||
for filemask in self.excludeMasks:
|
||||
for filemask in self.exclude_masks:
|
||||
if pathmatch(filemask, filename):
|
||||
break
|
||||
else:
|
||||
for filemask in self.includeMasks:
|
||||
if pathmatch(filemask, filename):
|
||||
filepath = os.path.join(directory_absolute_path, filename)
|
||||
for (
|
||||
message,
|
||||
plural,
|
||||
context,
|
||||
position,
|
||||
comments,
|
||||
) in self.extract_from_file(filepath):
|
||||
if empty_string_pattern.match(message):
|
||||
continue
|
||||
for filemask in self.include_masks:
|
||||
if not pathmatch(filemask, filename):
|
||||
continue
|
||||
|
||||
if " " in filename or "\t" in filename:
|
||||
filename = "\u2068" + filename + "\u2069"
|
||||
yield message, plural, context, (filename, position), comments
|
||||
filepath = os.path.join(directory_absolute_path, filename)
|
||||
for message, plural, context, position, comments in self.extract_from_file(
|
||||
filepath
|
||||
):
|
||||
if empty_string_pattern.match(message):
|
||||
continue
|
||||
|
||||
if " " in filename or "\t" in filename:
|
||||
filename = "\u2068" + filename + "\u2069"
|
||||
yield message, plural, context, (filename, position), comments
|
||||
|
||||
def extract_from_file(self, filepath):
|
||||
"""Extract messages from a specific file.
|
||||
@ -122,8 +129,6 @@ class JavascriptExtractor(Extractor):
|
||||
)
|
||||
|
||||
def extract_javascript_from_file(self, file_object):
|
||||
from babel.messages.jslexer import tokenize, unquote_string
|
||||
|
||||
funcname = message_lineno = None
|
||||
messages = []
|
||||
last_argument = None
|
||||
@ -237,7 +242,7 @@ class JavascriptExtractor(Extractor):
|
||||
last_token = token
|
||||
|
||||
def extract_from_file(self, filepath):
|
||||
with codecs.open(filepath, "r", encoding="utf-8-sig") as file_object:
|
||||
with open(filepath, encoding="utf-8-sig") as file_object:
|
||||
for lineno, funcname, messages, comments in self.extract_javascript_from_file(
|
||||
file_object
|
||||
):
|
||||
@ -301,10 +306,8 @@ class TxtExtractor(Extractor):
|
||||
"""Extract messages from plain text files."""
|
||||
|
||||
def extract_from_file(self, filepath):
|
||||
with codecs.open(filepath, "r", encoding="utf-8-sig") as file_object:
|
||||
for lineno, line in enumerate(
|
||||
[line.strip("\n\r") for line in file_object.readlines()], start=1
|
||||
):
|
||||
with open(filepath, encoding="utf-8-sig") as file_object:
|
||||
for lineno, line in enumerate([line.strip("\n\r") for line in file_object], start=1):
|
||||
if line:
|
||||
yield line, None, None, lineno, []
|
||||
|
||||
@ -329,66 +332,38 @@ class JsonExtractor(Extractor):
|
||||
self.comments = self.options.get("comments", [])
|
||||
|
||||
def extract_from_file(self, filepath):
|
||||
with codecs.open(filepath, "r", "utf-8") as file_object:
|
||||
with open(filepath, encoding="utf-8") as file_object:
|
||||
for message, context in self.extract_from_string(file_object.read()):
|
||||
yield message, None, context, None, self.comments
|
||||
|
||||
def extract_from_string(self, string):
|
||||
json_document = json.loads(string)
|
||||
if isinstance(json_document, list):
|
||||
for message, context in self.parse_list(json_document):
|
||||
if message: # Skip empty strings.
|
||||
yield message, context
|
||||
elif isinstance(json_document, dict):
|
||||
for message, context in self.parse_dictionary(json_document):
|
||||
if message: # Skip empty strings.
|
||||
yield message, context
|
||||
else:
|
||||
raise Exception(
|
||||
"Unexpected JSON document parent structure (not a list or a dictionary). "
|
||||
"You must extend the JSON extractor to support it."
|
||||
)
|
||||
yield from self.parse(json_document)
|
||||
|
||||
def parse_list(self, items_list):
|
||||
for list_item in items_list:
|
||||
if isinstance(list_item, list):
|
||||
for message, context in self.parse_list(list_item):
|
||||
yield message, context
|
||||
elif isinstance(list_item, dict):
|
||||
for message, context in self.parse_dictionary(list_item):
|
||||
yield message, context
|
||||
|
||||
def parse_dictionary(self, dictionary):
|
||||
for keyword in dictionary:
|
||||
if keyword in self.keywords:
|
||||
if isinstance(dictionary[keyword], str):
|
||||
yield self.extract_string(dictionary[keyword], keyword)
|
||||
elif isinstance(dictionary[keyword], list):
|
||||
for message, context in self.extract_list(dictionary[keyword], keyword):
|
||||
yield message, context
|
||||
elif isinstance(dictionary[keyword], dict):
|
||||
extract = None
|
||||
if (
|
||||
"extractFromInnerKeys" in self.keywords[keyword]
|
||||
and self.keywords[keyword]["extractFromInnerKeys"]
|
||||
):
|
||||
for message, context in self.extract_dictionary_inner_keys(
|
||||
dictionary[keyword], keyword
|
||||
):
|
||||
yield message, context
|
||||
else:
|
||||
extract = self.extract_dictionary(dictionary[keyword], keyword)
|
||||
if extract:
|
||||
yield extract
|
||||
elif isinstance(dictionary[keyword], list):
|
||||
for message, context in self.parse_list(dictionary[keyword]):
|
||||
yield message, context
|
||||
elif isinstance(dictionary[keyword], dict):
|
||||
for message, context in self.parse_dictionary(dictionary[keyword]):
|
||||
yield message, context
|
||||
def parse(self, data, key=None):
|
||||
"""Recursively parse JSON data and extract strings."""
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
yield from self.parse(item)
|
||||
elif isinstance(data, dict):
|
||||
for key2, value in data.items():
|
||||
if key2 in self.keywords:
|
||||
if isinstance(value, str):
|
||||
yield self.extract_string(value, key2)
|
||||
elif isinstance(value, list):
|
||||
yield from self.extract_list(value, key2)
|
||||
elif isinstance(value, dict):
|
||||
if self.keywords[key2].get("extractFromInnerKeys"):
|
||||
for value2 in value.values():
|
||||
yield from self.parse(value2, key2)
|
||||
else:
|
||||
yield from self.extract_dictionary(value, key2)
|
||||
else:
|
||||
yield from self.parse(value, key2)
|
||||
elif isinstance(data, str) and key in self.keywords:
|
||||
yield self.extract_string(data, key)
|
||||
|
||||
def extract_string(self, string, keyword):
|
||||
context = None
|
||||
if "tagAsContext" in self.keywords[keyword]:
|
||||
context = keyword
|
||||
elif "customContext" in self.keywords[keyword]:
|
||||
@ -409,7 +384,6 @@ class JsonExtractor(Extractor):
|
||||
def extract_dictionary(self, dictionary, keyword):
|
||||
message = dictionary.get("_string", None)
|
||||
if message and isinstance(message, str):
|
||||
context = None
|
||||
if "context" in dictionary:
|
||||
context = str(dictionary["context"])
|
||||
elif "tagAsContext" in self.keywords[keyword]:
|
||||
@ -418,19 +392,7 @@ class JsonExtractor(Extractor):
|
||||
context = self.keywords[keyword]["customContext"]
|
||||
else:
|
||||
context = self.context
|
||||
return message, context
|
||||
return None
|
||||
|
||||
def extract_dictionary_inner_keys(self, dictionary, keyword):
|
||||
for inner_keyword in dictionary:
|
||||
if isinstance(dictionary[inner_keyword], str):
|
||||
yield self.extract_string(dictionary[inner_keyword], keyword)
|
||||
elif isinstance(dictionary[inner_keyword], list):
|
||||
yield from self.extract_list(dictionary[inner_keyword], keyword)
|
||||
elif isinstance(dictionary[inner_keyword], dict):
|
||||
extract = self.extract_dictionary(dictionary[inner_keyword], keyword)
|
||||
if extract:
|
||||
yield extract
|
||||
yield message, context
|
||||
|
||||
|
||||
class XmlExtractor(Extractor):
|
||||
@ -439,86 +401,50 @@ class XmlExtractor(Extractor):
|
||||
def __init__(self, directory_path, filemasks, options):
|
||||
super().__init__(directory_path, filemasks, options)
|
||||
self.keywords = self.options.get("keywords", {})
|
||||
self.jsonExtractor = None
|
||||
self.json_extractor = None
|
||||
|
||||
def get_json_extractor(self):
|
||||
if not self.jsonExtractor:
|
||||
self.jsonExtractor = JsonExtractor()
|
||||
return self.jsonExtractor
|
||||
if not self.json_extractor:
|
||||
self.json_extractor = JsonExtractor(self.directory_path)
|
||||
return self.json_extractor
|
||||
|
||||
def extract_from_file(self, filepath):
|
||||
from lxml import etree
|
||||
|
||||
with codecs.open(filepath, "r", encoding="utf-8-sig") as file_object:
|
||||
with open(filepath, encoding="utf-8-sig") as file_object:
|
||||
xml_document = etree.parse(file_object)
|
||||
for keyword in self.keywords:
|
||||
for element in xml_document.iter(keyword):
|
||||
lineno = element.sourceline
|
||||
if element.text is not None:
|
||||
comments = []
|
||||
if "extractJson" in self.keywords[keyword]:
|
||||
json_extractor = self.get_json_extractor()
|
||||
json_extractor.set_options(self.keywords[keyword]["extractJson"])
|
||||
for message, context in json_extractor.extract_from_string(
|
||||
element.text
|
||||
):
|
||||
yield message, None, context, lineno, comments
|
||||
else:
|
||||
context = None
|
||||
if "context" in element.attrib:
|
||||
context = str(element.get("context"))
|
||||
elif "tagAsContext" in self.keywords[keyword]:
|
||||
context = keyword
|
||||
elif "customContext" in self.keywords[keyword]:
|
||||
context = self.keywords[keyword]["customContext"]
|
||||
if "comment" in element.attrib:
|
||||
comment = element.get("comment")
|
||||
comment = " ".join(
|
||||
comment.split()
|
||||
) # Remove tabs, line breaks and unecessary spaces.
|
||||
comments.append(comment)
|
||||
if "splitOnWhitespace" in self.keywords[keyword]:
|
||||
for split_text in element.text.split():
|
||||
# split on whitespace is used for token lists, there, a
|
||||
# leading '-' means the token has to be removed, so it's not
|
||||
# to be processed here either
|
||||
if split_text[0] != "-":
|
||||
yield str(split_text), None, context, lineno, comments
|
||||
else:
|
||||
yield str(element.text), None, context, lineno, comments
|
||||
|
||||
for element in xml_document.iter(*self.keywords.keys()):
|
||||
keyword = element.tag
|
||||
|
||||
# Hack from http://stackoverflow.com/a/2819788
|
||||
class FakeSectionHeader:
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.sechead = "[root]\n"
|
||||
lineno = element.sourceline
|
||||
if element.text is None:
|
||||
continue
|
||||
|
||||
def readline(self):
|
||||
if self.sechead:
|
||||
try:
|
||||
return self.sechead
|
||||
finally:
|
||||
self.sechead = None
|
||||
else:
|
||||
return self.fp.readline()
|
||||
|
||||
|
||||
class IniExtractor(Extractor):
|
||||
"""Extract messages from INI files."""
|
||||
|
||||
def __init__(self, directory_path, filemasks, options):
|
||||
super().__init__(directory_path, filemasks, options)
|
||||
self.keywords = self.options.get("keywords", [])
|
||||
|
||||
def extract_from_file(self, filepath):
|
||||
import ConfigParser
|
||||
|
||||
config = ConfigParser.RawConfigParser()
|
||||
with open(filepath, encoding="utf-8") as fd:
|
||||
config.read_file(FakeSectionHeader(fd))
|
||||
for keyword in self.keywords:
|
||||
message = config.get("root", keyword).strip('"').strip("'")
|
||||
context = None
|
||||
comments = []
|
||||
yield message, None, context, None, comments
|
||||
if "extractJson" in self.keywords[keyword]:
|
||||
json_extractor = self.get_json_extractor()
|
||||
json_extractor.set_options(self.keywords[keyword]["extractJson"])
|
||||
for message, context in json_extractor.extract_from_string(element.text):
|
||||
yield message, None, context, lineno, comments
|
||||
else:
|
||||
context = None
|
||||
if "context" in element.attrib:
|
||||
context = str(element.get("context"))
|
||||
elif "tagAsContext" in self.keywords[keyword]:
|
||||
context = keyword
|
||||
elif "customContext" in self.keywords[keyword]:
|
||||
context = self.keywords[keyword]["customContext"]
|
||||
if "comment" in element.attrib:
|
||||
comment = element.get("comment")
|
||||
comment = " ".join(
|
||||
comment.split()
|
||||
) # Remove tabs, line breaks and unnecessary spaces.
|
||||
comments.append(comment)
|
||||
if "splitOnWhitespace" in self.keywords[keyword]:
|
||||
for split_text in element.text.split():
|
||||
# split on whitespace is used for token lists, there, a
|
||||
# leading '-' means the token has to be removed, so it's not
|
||||
# to be processed here either
|
||||
if split_text[0] != "-":
|
||||
yield str(split_text), None, context, lineno, comments
|
||||
else:
|
||||
yield str(element.text), None, context, lineno, comments
|
@ -1,12 +1,11 @@
|
||||
"""Utils to list .po."""
|
||||
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
from i18n_helper.catalog import Catalog
|
||||
|
||||
|
||||
def get_catalogs(input_file_path, filters: Optional[List[str]] = None) -> List[Catalog]:
|
||||
def get_catalogs(input_file_path, filters: list[str] | None = None) -> list[Catalog]:
|
||||
"""Return a list of "real" catalogs (.po) in the given folder."""
|
||||
existing_translation_catalogs = []
|
||||
l10n_folder_path = os.path.dirname(input_file_path)
|
||||
|
@ -97,7 +97,7 @@ PATCHES_EXPECT_REVERT = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(params=zip(PATCHES, PATCHES_EXPECT_REVERT))
|
||||
@pytest.fixture(params=zip(PATCHES, PATCHES_EXPECT_REVERT, strict=False))
|
||||
def patch(request):
|
||||
return [io.StringIO(request.param[0]), request.param[1]]
|
||||
|
||||
|
@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
@ -33,18 +35,21 @@ def warn_about_untouched_mods():
|
||||
mods_root_folder = os.path.join(PROJECT_ROOT_DIRECTORY, "binaries", "data", "mods")
|
||||
untouched_mods = {}
|
||||
for mod_folder in os.listdir(mods_root_folder):
|
||||
if mod_folder[0] != "_" and mod_folder[0] != ".":
|
||||
if not os.path.exists(os.path.join(mods_root_folder, mod_folder, L10N_FOLDER_NAME)):
|
||||
untouched_mods[mod_folder] = (
|
||||
f"There is no '{L10N_FOLDER_NAME}' folder in the root folder of this mod."
|
||||
)
|
||||
elif not os.path.exists(
|
||||
os.path.join(mods_root_folder, mod_folder, L10N_FOLDER_NAME, messages_filename)
|
||||
):
|
||||
untouched_mods[mod_folder] = (
|
||||
f"There is no '{messages_filename}' file within the '{L10N_FOLDER_NAME}' "
|
||||
f"folder in the root folder of this mod."
|
||||
)
|
||||
if mod_folder.startswith(("_", ".")):
|
||||
continue
|
||||
|
||||
if not os.path.exists(os.path.join(mods_root_folder, mod_folder, L10N_FOLDER_NAME)):
|
||||
untouched_mods[mod_folder] = (
|
||||
f"There is no '{L10N_FOLDER_NAME}' folder in the root folder of this mod."
|
||||
)
|
||||
elif not os.path.exists(
|
||||
os.path.join(mods_root_folder, mod_folder, L10N_FOLDER_NAME, messages_filename)
|
||||
):
|
||||
untouched_mods[mod_folder] = (
|
||||
f"There is no '{messages_filename}' file within the '{L10N_FOLDER_NAME}' "
|
||||
f"folder in the root folder of this mod."
|
||||
)
|
||||
|
||||
if untouched_mods:
|
||||
print("Warning: No messages were extracted from the following mods:")
|
||||
for mod in untouched_mods:
|
||||
@ -78,7 +83,7 @@ def generate_pot(template_settings, root_path):
|
||||
|
||||
options = rule.get("options", {})
|
||||
extractor_class = getattr(
|
||||
import_module("extractors.extractors"), f'{rule["extractor"].title()}Extractor'
|
||||
import_module("i18n_helper.extractors"), f'{rule["extractor"].title()}Extractor'
|
||||
)
|
||||
extractor = extractor_class(input_root_path, rule["filemasks"], options)
|
||||
format_flag = None
|
||||
@ -111,8 +116,6 @@ def generate_templates_for_messages_file(messages_file_path):
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--scandir",
|
||||
@ -120,12 +123,13 @@ def main():
|
||||
"Type '.' for current working directory",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
for root, folders, _filenames in os.walk(args.scandir or PROJECT_ROOT_DIRECTORY):
|
||||
for folder in folders:
|
||||
if folder == L10N_FOLDER_NAME:
|
||||
messages_file_path = os.path.join(root, folder, messages_filename)
|
||||
if os.path.exists(messages_file_path):
|
||||
generate_templates_for_messages_file(messages_file_path)
|
||||
dir_to_scan = args.scandir or PROJECT_ROOT_DIRECTORY
|
||||
for messages_file_path in glob.glob(
|
||||
f"**/{L10N_FOLDER_NAME}/{messages_filename}", root_dir=dir_to_scan, recursive=True
|
||||
):
|
||||
generate_templates_for_messages_file(
|
||||
os.path.abspath(f"{dir_to_scan}/{messages_file_path}")
|
||||
)
|
||||
|
||||
warn_about_untouched_mods()
|
||||
|
||||
|
@ -56,13 +56,13 @@ args = parser.parse_args()
|
||||
|
||||
HEIGHTMAP_BIT_SHIFT = 3
|
||||
|
||||
for xmlFile in args.files:
|
||||
pmpFile = xmlFile[:-3] + "pmp"
|
||||
for xml_file in args.files:
|
||||
pmp_file = xml_file[:-3] + "pmp"
|
||||
|
||||
print("Processing " + xmlFile + " ...")
|
||||
print("Processing " + xml_file + " ...")
|
||||
|
||||
if os.path.isfile(pmpFile):
|
||||
with open(pmpFile, "rb") as f1, open(pmpFile + "~", "wb") as f2:
|
||||
if os.path.isfile(pmp_file):
|
||||
with open(pmp_file, "rb") as f1, open(pmp_file + "~", "wb") as f2:
|
||||
# 4 bytes PSMP to start the file
|
||||
f2.write(f1.read(4))
|
||||
|
||||
@ -73,7 +73,7 @@ for xmlFile in args.files:
|
||||
elif args.reverse:
|
||||
if version != 6:
|
||||
print(
|
||||
f"Warning: File {pmpFile} was not at version 6, while a negative version "
|
||||
f"Warning: File {pmp_file} was not at version 6, while a negative version "
|
||||
f"bump was requested.\nABORTING ..."
|
||||
)
|
||||
continue
|
||||
@ -81,7 +81,7 @@ for xmlFile in args.files:
|
||||
else:
|
||||
if version != 5:
|
||||
print(
|
||||
f"Warning: File {pmpFile} was not at version 5, while a version bump was "
|
||||
f"Warning: File {pmp_file} was not at version 5, while a version bump was "
|
||||
f"requested.\nABORTING ..."
|
||||
)
|
||||
continue
|
||||
@ -122,13 +122,13 @@ for xmlFile in args.files:
|
||||
f1.close()
|
||||
|
||||
# replace the old file, comment to see both files
|
||||
os.remove(pmpFile)
|
||||
os.rename(pmpFile + "~", pmpFile)
|
||||
os.remove(pmp_file)
|
||||
os.rename(pmp_file + "~", pmp_file)
|
||||
|
||||
if os.path.isfile(xmlFile):
|
||||
if os.path.isfile(xml_file):
|
||||
with (
|
||||
open(xmlFile, encoding="utf-8") as f1,
|
||||
open(xmlFile + "~", "w", encoding="utf-8") as f2,
|
||||
open(xml_file, encoding="utf-8") as f1,
|
||||
open(xml_file + "~", "w", encoding="utf-8") as f2,
|
||||
):
|
||||
data = f1.read()
|
||||
|
||||
@ -137,7 +137,7 @@ for xmlFile in args.files:
|
||||
if args.reverse:
|
||||
if data.find('<Scenario version="6">') == -1:
|
||||
print(
|
||||
f"Warning: File {xmlFile} was not at version 6, while a negative "
|
||||
f"Warning: File {xml_file} was not at version 6, while a negative "
|
||||
f"version bump was requested.\nABORTING ..."
|
||||
)
|
||||
sys.exit()
|
||||
@ -145,7 +145,7 @@ for xmlFile in args.files:
|
||||
data = data.replace('<Scenario version="6">', '<Scenario version="5">')
|
||||
elif data.find('<Scenario version="5">') == -1:
|
||||
print(
|
||||
f"Warning: File {xmlFile} was not at version 5, while a version bump "
|
||||
f"Warning: File {xml_file} was not at version 5, while a version bump "
|
||||
f"was requested.\nABORTING ..."
|
||||
)
|
||||
sys.exit()
|
||||
@ -164,5 +164,5 @@ for xmlFile in args.files:
|
||||
f2.close()
|
||||
|
||||
# replace the old file, comment to see both files
|
||||
os.remove(xmlFile)
|
||||
os.rename(xmlFile + "~", xmlFile)
|
||||
os.remove(xml_file)
|
||||
os.rename(xml_file + "~", xml_file)
|
||||
|
@ -58,4 +58,4 @@ actions = [zero_ad.actions.attack(my_units, enemy_units[0])]
|
||||
state = game.step(actions)
|
||||
```
|
||||
|
||||
For a more thorough example, check out samples/simple-example.py!
|
||||
For a more thorough example, check out samples/simple_example.py!
|
||||
|
@ -7,11 +7,11 @@ import zero_ad
|
||||
|
||||
|
||||
def dist(p1, p2):
|
||||
return math.sqrt(sum(math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2)))
|
||||
return math.sqrt(sum(math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2, strict=False)))
|
||||
|
||||
|
||||
def center(units):
|
||||
sum_position = map(sum, zip(*(u.position() for u in units)))
|
||||
sum_position = map(sum, zip(*(u.position() for u in units), strict=False))
|
||||
return [x / len(units) for x in sum_position]
|
||||
|
||||
|
@ -11,11 +11,11 @@ with open(path.join(scriptdir, "..", "samples", "arcadia.json"), encoding="utf-8
|
||||
|
||||
|
||||
def dist(p1, p2):
|
||||
return math.sqrt(sum(math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2)))
|
||||
return math.sqrt(sum(math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2, strict=False)))
|
||||
|
||||
|
||||
def center(units):
|
||||
sum_position = map(sum, zip(*(u.position() for u in units)))
|
||||
sum_position = map(sum, zip(*(u.position() for u in units), strict=False))
|
||||
return [x / len(units) for x in sum_position]
|
||||
|
||||
|
||||
|
@ -26,7 +26,7 @@ class RLAPI:
|
||||
def get_templates(self, names):
|
||||
post_data = "\n".join(names)
|
||||
response = self.post("templates", post_data)
|
||||
return zip(names, response.decode().split("\n"))
|
||||
return zip(names, response.decode().split("\n"), strict=False)
|
||||
|
||||
def evaluate(self, code):
|
||||
response = self.post("evaluate", code)
|
||||
|
@ -17,7 +17,7 @@ class ZeroAD:
|
||||
actions = []
|
||||
player_ids = cycle([self.player_id]) if player is None else cycle(player)
|
||||
|
||||
cmds = zip(player_ids, actions)
|
||||
cmds = zip(player_ids, actions, strict=False)
|
||||
cmds = ((player, action) for (player, action) in cmds if action is not None)
|
||||
state_json = self.api.step(cmds)
|
||||
self.current_state = GameState(json.loads(state_json), self)
|
||||
|
@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||
#
|
||||
# Copyright (C) 2023 Wildfire Games.
|
||||
# Copyright (C) 2024 Wildfire Games.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@ -29,6 +28,7 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from enum import Enum
|
||||
|
||||
import yaml
|
||||
|
||||
@ -46,15 +46,7 @@ def execute(command):
|
||||
def calculate_hash(path):
|
||||
assert os.path.isfile(path)
|
||||
with open(path, "rb") as handle:
|
||||
return hashlib.sha1(handle.read()).hexdigest()
|
||||
|
||||
|
||||
def compare_spirv(path1, path2):
|
||||
with open(path1, "rb") as handle:
|
||||
spirv1 = handle.read()
|
||||
with open(path2, "rb") as handle:
|
||||
spirv2 = handle.read()
|
||||
return spirv1 == spirv2
|
||||
return hashlib.sha256(handle.read()).hexdigest()
|
||||
|
||||
|
||||
def resolve_if(defines, expression):
|
||||
@ -169,18 +161,21 @@ def compile_and_reflect(input_mod_path, dependencies, stage, path, out_path, def
|
||||
add_push_constants(module["push_constants"][0], push_constants)
|
||||
descriptor_sets = []
|
||||
if module.get("descriptor_sets"):
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER = 1
|
||||
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE = 3
|
||||
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER = 6
|
||||
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER = 7
|
||||
|
||||
class VkDescriptorType(Enum):
|
||||
COMBINED_IMAGE_SAMPLER = 1
|
||||
STORAGE_IMAGE = 3
|
||||
UNIFORM_BUFFER = 6
|
||||
STORAGE_BUFFER = 7
|
||||
|
||||
for descriptor_set in module["descriptor_sets"]:
|
||||
UNIFORM_SET = 1 if use_descriptor_indexing else 0
|
||||
STORAGE_SET = 2
|
||||
uniform_set = 1 if use_descriptor_indexing else 0
|
||||
storage_set = 2
|
||||
bindings = []
|
||||
if descriptor_set["set"] == UNIFORM_SET:
|
||||
if descriptor_set["set"] == uniform_set:
|
||||
assert descriptor_set["binding_count"] > 0
|
||||
for binding in descriptor_set["bindings"]:
|
||||
assert binding["set"] == UNIFORM_SET
|
||||
assert binding["set"] == uniform_set
|
||||
block = binding["block"]
|
||||
members = []
|
||||
for member in block["members"]:
|
||||
@ -200,15 +195,15 @@ def compile_and_reflect(input_mod_path, dependencies, stage, path, out_path, def
|
||||
}
|
||||
)
|
||||
binding = descriptor_set["bindings"][0]
|
||||
assert binding["descriptor_type"] == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
|
||||
elif descriptor_set["set"] == STORAGE_SET:
|
||||
assert binding["descriptor_type"] == VkDescriptorType.UNIFORM_BUFFER.value
|
||||
elif descriptor_set["set"] == storage_set:
|
||||
assert descriptor_set["binding_count"] > 0
|
||||
for binding in descriptor_set["bindings"]:
|
||||
is_storage_image = (
|
||||
binding["descriptor_type"] == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
|
||||
binding["descriptor_type"] == VkDescriptorType.STORAGE_IMAGE.value
|
||||
)
|
||||
is_storage_buffer = (
|
||||
binding["descriptor_type"] == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
|
||||
binding["descriptor_type"] == VkDescriptorType.STORAGE_BUFFER.value
|
||||
)
|
||||
assert is_storage_image or is_storage_buffer
|
||||
assert (
|
||||
@ -217,13 +212,13 @@ def compile_and_reflect(input_mod_path, dependencies, stage, path, out_path, def
|
||||
)
|
||||
assert binding["image"]["arrayed"] == 0
|
||||
assert binding["image"]["ms"] == 0
|
||||
bindingType = "storageImage"
|
||||
binding_type = "storageImage"
|
||||
if is_storage_buffer:
|
||||
bindingType = "storageBuffer"
|
||||
binding_type = "storageBuffer"
|
||||
bindings.append(
|
||||
{
|
||||
"binding": binding["binding"],
|
||||
"type": bindingType,
|
||||
"type": binding_type,
|
||||
"name": binding["name"],
|
||||
}
|
||||
)
|
||||
@ -232,7 +227,8 @@ def compile_and_reflect(input_mod_path, dependencies, stage, path, out_path, def
|
||||
assert descriptor_set["binding_count"] >= 1
|
||||
for binding in descriptor_set["bindings"]:
|
||||
assert (
|
||||
binding["descriptor_type"] == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
|
||||
binding["descriptor_type"]
|
||||
== VkDescriptorType.COMBINED_IMAGE_SAMPLER.value
|
||||
)
|
||||
assert binding["array"]["dims"][0] == 16384
|
||||
if binding["binding"] == 0:
|
||||
@ -246,7 +242,9 @@ def compile_and_reflect(input_mod_path, dependencies, stage, path, out_path, def
|
||||
else:
|
||||
assert descriptor_set["binding_count"] > 0
|
||||
for binding in descriptor_set["bindings"]:
|
||||
assert binding["descriptor_type"] == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
|
||||
assert (
|
||||
binding["descriptor_type"] == VkDescriptorType.COMBINED_IMAGE_SAMPLER.value
|
||||
)
|
||||
assert binding["image"]["sampled"] == 1
|
||||
assert binding["image"]["arrayed"] == 0
|
||||
assert binding["image"]["ms"] == 0
|
||||
@ -450,19 +448,10 @@ def build(rules, input_mod_path, output_mod_path, dependencies, program_name):
|
||||
|
||||
spirv_hash = calculate_hash(output_spirv_path)
|
||||
if spirv_hash not in hashed_cache:
|
||||
hashed_cache[spirv_hash] = [file_name]
|
||||
hashed_cache[spirv_hash] = file_name
|
||||
else:
|
||||
found_candidate = False
|
||||
for candidate_name in hashed_cache[spirv_hash]:
|
||||
candidate_path = os.path.join(output_spirv_mod_path, candidate_name)
|
||||
if compare_spirv(output_spirv_path, candidate_path):
|
||||
found_candidate = True
|
||||
file_name = candidate_name
|
||||
break
|
||||
if found_candidate:
|
||||
os.remove(output_spirv_path)
|
||||
else:
|
||||
hashed_cache[spirv_hash].append(file_name)
|
||||
file_name = hashed_cache[spirv_hash]
|
||||
os.remove(output_spirv_path)
|
||||
|
||||
shader_element = ET.SubElement(program_root, shader["type"])
|
||||
shader_element.set("file", "spirv/" + file_name)
|
||||
|
@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
||||
#
|
||||
# Copyright (C) 2023 Wildfire Games.
|
||||
# Copyright (C) 2024 Wildfire Games.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -21,7 +21,7 @@ class SingleLevelFilter(Filter):
|
||||
return record.levelno == self.passlevel
|
||||
|
||||
|
||||
class VFS_File:
|
||||
class VFSFile:
|
||||
def __init__(self, mod_name, vfs_path):
|
||||
self.mod_name = mod_name
|
||||
self.vfs_path = vfs_path
|
||||
@ -201,7 +201,7 @@ class RelaxNGValidator:
|
||||
try:
|
||||
doc = lxml.etree.parse(str(file[1]))
|
||||
relaxng.assertValid(doc)
|
||||
except Exception:
|
||||
except (lxml.etree.DocumentInvalid, lxml.etree.XMLSyntaxError):
|
||||
error_count = error_count + 1
|
||||
self.logger.exception(file[1])
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user