diff --git a/.clang-format b/.clang-format deleted file mode 100644 index 25b88ee..0000000 --- a/.clang-format +++ /dev/null @@ -1,67 +0,0 @@ ---- -# Atrinik uses a conservative LLVM-derived style for all authored C and C++. -# Keep this file compatible with the clang-format release in the devcontainer. -BasedOnStyle: LLVM -Language: Cpp - -IndentWidth: 4 -ContinuationIndentWidth: 4 -TabWidth: 4 -UseTab: Never -ColumnLimit: 100 - -BreakBeforeBraces: Attach -BreakAfterReturnType: None -IndentCaseLabels: true -IndentPPDirectives: None -PointerAlignment: Right -DerivePointerAlignment: false - -AlignAfterOpenBracket: Align -AlignEscapedNewlines: Left -AlignOperands: Align -AlignTrailingComments: - Kind: Never - -AllowAllArgumentsOnNextLine: false -AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: Empty -AllowShortCaseLabelsOnASingleLine: false -AllowShortEnumsOnASingleLine: false -AllowShortFunctionsOnASingleLine: Empty -AllowShortIfStatementsOnASingleLine: Never -AllowShortLoopsOnASingleLine: false -BinPackArguments: false -BinPackParameters: false - -IncludeBlocks: Preserve -SortIncludes: Never -ReflowComments: true - -SpaceBeforeParens: Custom -SpaceBeforeParensOptions: - AfterControlStatements: true - AfterForeachMacros: false - AfterFunctionDeclarationName: false - AfterFunctionDefinitionName: false - AfterIfMacros: false - AfterOverloadedOperator: false - BeforeNonEmptyParentheses: false - -ForEachMacros: - - CDL_FOREACH - - DL_FOREACH - - DL_FOREACH_SAFE - - FOR_ATRINIK_ITERATOR_BEGIN - - FOR_CLIOPTIONS_BEGIN - - FOR_EACH - - FOR_INV_PREPARE - - FOR_MAP_LAYER_BEGIN - - FOR_MAP_PREPARE - - FOR_OB_AND_ABOVE_PREPARE - - FOR_OB_AND_BELOW_PREPARE - - HASH_ITER - - LL_FOREACH - - LL_FOREACH_SAFE -... - diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b744c9d..21aabe5 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,15 +21,14 @@ jobs: - name: Validate Python syntax run: python3 -W error -m compileall -q -f . + - name: Validate diagnostic utilities + run: | + python3 -m unittest discover -s tests -v + shellcheck split_symbols.sh + - name: Validate map checker catalog integration run: | python3 -m unittest discover -s map-checker-qt/tests -v python3 map-checker-qt/dependencies.py validate python3 map-checker-qt/dependencies.py sync python3 map-checker-qt/dependencies.py verify - - - name: Build randomizer - run: | - mkdir -p build - cc -std=gnu99 -Wall -Wextra -Werror \ - -o build/randomizer randomizer/randomizer.c diff --git a/AGENTS.md b/AGENTS.md index f2c2f55..bcec912 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,21 +17,12 @@ - New Python work must document its supported runtime and dependencies. Do not duplicate client/server command IDs in packet-aware tools; consume a released protocol binding when such tooling becomes maintained here. -- Source in this repository remains GPL as distributed under `COPYING`. That - is not a blanket outbound reuse ban. Under the - [canonical provenance policy](https://github.com/atrinik/atrinik/blob/main/docs/PROVENANCE.md), - an MIT destination may inspect exact, independently separable material as - source reference, copy it, migrate or port it, translate or adapt it, or - relicense it only after a complete audit proves each selected contribution - is the applicable named grantor's original work. Each contribution must be - solely authored by that grantor and fall within the row's temporal scope. - Distinct contributions may cite different rows only when each independently - satisfies one row. Rows cannot be combined to cover jointly authored - contributions, generated output, or inseparable mixed work. Later material - needs contemporaneous compatible permission. The grants authorize only - proven destination use. They neither change this repository's source license - nor by themselves approve a GPL dependency, linked or combined binary, - bundle, or surrounding material. +- Source is MIT by default under root `LICENSE`. The complete + `map-checker-qt/` subtree remains GPL-2.0-or-later under its own `LICENSE`, + including generated PyQt files, tests, configuration, launchers, and docs. + Never move or copy material across that boundary without a complete + provenance and license review. Preserve the separate PyQt5 and downloaded + content-catalog dependency terms. - Autonomous gameplay testing belongs in the independent [`atrinik/playtester`](https://github.com/atrinik/playtester) repository; do not add bot implementation or mutable bot state here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1c96a4..ac602b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,19 +5,12 @@ for each changed utility. Keep component-specific build tooling in its owning repository, consume the released protocol package for packet-aware Python tools, and keep autonomous gameplay testing in the independent [`atrinik/playtester`](https://github.com/atrinik/playtester) repository. -Preserve the GNU General Public License terms in `COPYING`. A new, independently -authored utility may use the MIT License when feasible, but it must carry a -clearly scoped license. Existing source remains GPL as distributed; that is not -a blanket outbound reuse ban. Exact, independently separable material may be -inspected as source reference, copied, migrated or ported, translated or -adapted, or relicensed in an MIT destination only after the -[canonical provenance audit](https://github.com/atrinik/atrinik/blob/main/docs/PROVENANCE.md) -proves each selected contribution is the applicable named grantor's original -work. Each contribution must be solely authored by that grantor and fall within -the row's temporal scope. Distinct contributions may cite different rows only -when each independently satisfies one row. Rows cannot be combined to cover -jointly authored contributions, generated output, or inseparable mixed work. -Later material needs contemporaneous compatible permission. The grants -authorize only proven destination use. They neither change this repository's -source license nor by themselves approve a GPL dependency, linked or combined -binary, bundle, or surrounding material. + +Contributions are MIT by default under root `LICENSE`. The complete +`map-checker-qt/` subtree is the sole GPL-2.0-or-later exception under +`map-checker-qt/LICENSE`; preserve that scope for its source, generated PyQt +files, tests, configuration, launchers, and documentation. Do not move material +across that boundary without a complete provenance and license review. Keep +PyQt5 and downloaded catalog dependencies under their own terms, and update +`PROVENANCE.md` whenever the current-tree rights basis or dependency boundary +changes. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4dc20d0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Zoey Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROVENANCE.md b/PROVENANCE.md new file mode 100644 index 0000000..c2bbd50 --- /dev/null +++ b/PROVENANCE.md @@ -0,0 +1,152 @@ +# Current-tree provenance + +This record covers the live tree introduced by the MIT-default transition. It +does not change the license of any tag, release, historical revision, or Git +object published before the transition. + +## Audit coordinates and method + +- Audited source repository: `atrinik/tools`. +- Complete, non-shallow baseline commit: + `7777cf9f9ab6deb58de8a481dfccd6b05d86e3e1` (tree + `daeb2eb5771d3f90ecf70ccfa2d9e1e4d768f6e4`). +- Historical grant-policy revision: + `atrinik/atrinik@f0d1225791da7484e9456b39104cc30b0c77fe52`. +- Privacy-preserving identity-policy revision: + `atrinik/atrinik@6f6040212f0fa0cb6b8e4e695d1488a403d966be`. +- Current coordinator default-branch review revision: + `atrinik/atrinik@3c21ce575076be9effee102b5e95b52399bfceed`. + +The audit used the complete local object graph with replace objects disabled, +followed path history across renames and moves, compared every retained path to +the baseline tree, and reviewed contributor, generated-file, dependency, and +embedded-notice boundaries. No confidential identity mapping or restricted +evidence was copied into this repository. + +The current maintainer and issue author, Zoey Rose (`github:zoeyrose`), has +explicitly confirmed and records through root `LICENSE` and pull request #23 +that she personally authored, or commissioned and directed as agent-assisted +work, every admitted human-authored contribution listed below; that she owns +the rights needed to license those contributions; and that she licenses them +under MIT. This is the explicit contemporaneous rights grant for both +pre-transition and transition work. It is not inferred from Git names, email +addresses, or tool use. No former identity is inferred or published. Pull +request #23 is the public implementation, attestation, and review record; its +final head and tree are the destination coordinates because a commit cannot +contain its own hash. + +The historical diagnostic implementations were not relicensed. Their baseline +blobs remain GPL in history. The current implementations were authored under +the contemporaneous grant above. Blank lines, standard language scaffolding, +and the GNU tool invocation sequence dictated by the retained functional +contract are separately classified as non-copyrightable; their exact historical +commits are recorded below rather than hidden by a re-authorship claim. The +mechanical Dependabot version-coordinate substitution is separately classified +below and contributes no retained expressive implementation requiring a grant. Consequently no +historical identity attestation is used to place a file in the MIT-default +scope; any unresolved historical identity remains fail-closed and historical +only. + +## Contribution ledger + +The commit lists are the complete current-tree line-provenance sets from +`git blame --line-porcelain HEAD -- `, after following each path's complete +history, with the final manifest-only commit represented by pull request #23's +destination head/tree. All listed expressive contributions except the +separately identified Dependabot and diagnostic-contract fragments were +authored under the explicit contemporaneous grant above. + +| Current path | Exact admitted source contributions | Classification, transformation, and review | +| --- | --- | --- | +| `.gitignore` | `a6904f87ec9cc02294abf2293e4d1e462653dece`, `56fa28993ef2bbf38815074a14c5f4b6fe9a137e`, `68a9d457f19c608b2e848c7de1496b7154af7633` | Current-public-identity work; retained as MIT metadata and reviewed by `github:zoeyrose` | +| `.github/dependabot.yml` | `843a3dff3a36135a9a60aff7c6a3fcc762f704d3` | Current-public-identity work; retained as MIT dependency policy and reviewed by `github:zoeyrose` | +| `.github/workflows/check.yml` | `a6904f87ec9cc02294abf2293e4d1e462653dece`, `25dbbbc00d8bbeb98c7d0f73cdf56145b517c398`, `55d2ce6f8d09a52d3ce4b25447d187900213179c`, `56fa28993ef2bbf38815074a14c5f4b6fe9a137e`, transition commit `124c2793a27bd9746d3ecdcb85c81664e9abe58a` | Current-public-identity workflow expression; retired steps removed and MIT tests added; reviewed by `github:zoeyrose` | +| `.github/workflows/pr-title.yml` | `a6904f87ec9cc02294abf2293e4d1e462653dece` | Current-public-identity work; retained as MIT policy and reviewed by `github:zoeyrose` | +| `.github/workflows/release.yml` | `77b39021353a4aec36632001d232f085d5b65667`, `68bf7139c98b1d6d88ee2a3f475a20cbeaee989e`, `25dbbbc00d8bbeb98c7d0f73cdf56145b517c398`, `6dfd98d630c6194b1545d95e55a5ca69d903129a`, `a90cfb5b92c1c6cc4f55a0d92949593457be604e` | Current-public-identity workflow expression; retained under contemporaneous MIT authorization and reviewed by `github:zoeyrose` | +| `.github/workflows/check.yml`, `.github/workflows/release.yml` | Dependabot commit `80f14561b9090b1573a89feaf75053a490a891bc` | Separately classified non-copyrightable mechanical substitution of the factual immutable `actions/checkout` version/commit coordinate; no bot-authored expression or grant presumed | +| `.releaserc.json` | `77b39021353a4aec36632001d232f085d5b65667`, `25dbbbc00d8bbeb98c7d0f73cdf56145b517c398`, `49df09157f1bc701bc5a48ae2e21c5b48dbc65cc`, `7d95669c6756219779907811386d64c2492ddc46` | Current-public-identity release configuration; contemporaneously authorized under MIT and reviewed by `github:zoeyrose` | +| `AGENTS.md` | `68a9d457f19c608b2e848c7de1496b7154af7633`, `75bf3a2304cc9ac7f03efe4f2f3c9f6bbb11acad`, transition commit `124c2793a27bd9746d3ecdcb85c81664e9abe58a` | Current-public-identity documentation, rewritten for the mixed boundary and reviewed by `github:zoeyrose` | +| `CONTRIBUTING.md` | `8469be5cfa3a3ef207607fec87fa609c8d585d7c`, `75bf3a2304cc9ac7f03efe4f2f3c9f6bbb11acad`, transition commit `124c2793a27bd9746d3ecdcb85c81664e9abe58a` | Current-public-identity documentation, rewritten for default/exception contribution scopes and reviewed by `github:zoeyrose` | +| `README.md` | `a6904f87ec9cc02294abf2293e4d1e462653dece`, `b0f27045a0491fd9d3f36d8ac63159c713a6c601`, `75bf3a2304cc9ac7f03efe4f2f3c9f6bbb11acad`, `56fa28993ef2bbf38815074a14c5f4b6fe9a137e`, `55d2ce6f8d09a52d3ce4b25447d187900213179c`, `72f0bf821fb149ed4d2212cae520de6644db44b6`, transition commits `3ce48bfa62edfea491304a5424da71ff2c6b79c3`, `124c2793a27bd9746d3ecdcb85c81664e9abe58a`, `4d9184dc9b052e0dca44db273b895e89874f1b93` | Current-public-identity documentation; retired paths removed and license boundary rewritten; reviewed by `github:zoeyrose` | + +Transition-created MIT-default paths are inventoried separately because they +have no baseline contribution set: + +| Current path | Exact admitted source contributions | Classification, transformation, and review | +| --- | --- | --- | +| `LICENSE` | `124c2793a27bd9746d3ecdcb85c81664e9abe58a`, `6caf3d4b1f1baf7034269be406b5c12437f5ccda`; final wording in pull request #23 destination | Standard MIT text and Zoey Rose copyright notice; reviewed by `github:zoeyrose` | +| `PROVENANCE.md` | `124c2793a27bd9746d3ecdcb85c81664e9abe58a`, `4d9184dc9b052e0dca44db273b895e89874f1b93`, `6caf3d4b1f1baf7034269be406b5c12437f5ccda`, `cc0acb4b2d2e19d42a9f989eacff5eb3f94ad842`, `3ffce3f4848ba27025def318b4a30e53eb4de492`, `efe7c16a95970dab77c4f573b44d35232ae2a6f0`, `9f2c77f029c028ebc7b9cd1b4007db58711aefee`, `256b39a62fdac5483abdaac05a8e670b371d4ba3`, `0d6d802fcfb1879eeb6ccfcf4b369baee6b80812`, `6feef28190b8fa5f7b5a9669b03c1173c78c47f1`; final manifest-only change in pull request #23 destination | Agent-assisted record commissioned, directed, owned, granted, and reviewed by `github:zoeyrose` | +| `split_symbols.sh` | `124c2793a27bd9746d3ecdcb85c81664e9abe58a`, `a41ffca9ea5bc737039e66b9ed51cb235a2523c1`, `50911c73ee307bbddfbf68e46a88ea6c7804efe8`, `ad1de2c9b64f576c08e88536079f18d8581de1a7`, `9b86634000f86d7d0aeebf8e5f07012da5e21b68`, `d577ebc3b469ecb3b3eaa36c35a3461814d3d1ba`, `d1e00bf130bbf5a5a5087fef0cebf66d593894e7`, `9c4e2ea5813a263c3bc9128972d7e2b12329e609`; historical non-expressive fragments classified below | Agent-assisted expressive implementation commissioned, directed, owned, granted, and reviewed by `github:zoeyrose`; retained functional GNU-tool sequence separately non-copyrightable | +| `stacktrace.py` | `124c2793a27bd9746d3ecdcb85c81664e9abe58a`, `a41ffca9ea5bc737039e66b9ed51cb235a2523c1`, `50911c73ee307bbddfbf68e46a88ea6c7804efe8`; historical non-expressive fragments classified below | Agent-assisted expressive implementation commissioned, directed, owned, granted, and reviewed by `github:zoeyrose`; standard syntax/scaffolding separately non-copyrightable | +| `tests/__init__.py` | `124c2793a27bd9746d3ecdcb85c81664e9abe58a` | Empty package marker; non-expressive metadata | +| `tests/test_diagnostics.py` | `124c2793a27bd9746d3ecdcb85c81664e9abe58a`, `4d9184dc9b052e0dca44db273b895e89874f1b93`, `a41ffca9ea5bc737039e66b9ed51cb235a2523c1`, `50911c73ee307bbddfbf68e46a88ea6c7804efe8`, `ad1de2c9b64f576c08e88536079f18d8581de1a7`, `9b86634000f86d7d0aeebf8e5f07012da5e21b68`, `d577ebc3b469ecb3b3eaa36c35a3461814d3d1ba`, `d1e00bf130bbf5a5a5087fef0cebf66d593894e7`, `9c4e2ea5813a263c3bc9128972d7e2b12329e609` | Agent-assisted test expression commissioned, directed, owned, granted, and reviewed by `github:zoeyrose` | + +The historic `split_symbols.sh` baseline blob +`ce6030376eb25ddc9cd1566603d3448e6071c613` and `stacktrace.py` baseline blob +`fa78cc708dd750628f6ae530e912ee657aa8d97e` are explicitly excluded. Commit +`124c2793a27bd9746d3ecdcb85c81664e9abe58a` replaced their expressive +implementations; `a41ffca9ea5bc737039e66b9ed51cb235a2523c1` and +`50911c73ee307bbddfbf68e46a88ea6c7804efe8` and +`ad1de2c9b64f576c08e88536079f18d8581de1a7` and +`9b86634000f86d7d0aeebf8e5f07012da5e21b68` and +`d577ebc3b469ecb3b3eaa36c35a3461814d3d1ba` and +`d1e00bf130bbf5a5a5087fef0cebf66d593894e7` and +`9c4e2ea5813a263c3bc9128972d7e2b12329e609` completed compatibility, +recoverable failure behavior with explicit incomplete-rollback reporting, +exact-path, symlink, and same-inode-alias safety, and executable and debug-file +identity/metadata preservation. Current `git blame` also names historical +commits `cd12a49557cb5b89a8b708fed1f3ca2510465786`, +`400b50e0ce9e49c493abf4405fc31aebd175970f`, and +`d5ea8b9856fd8a17bdb263ef5a51759acba4e83a` solely for blank lines, braces, +standard module/entry-point scaffolding, `print`/`continue`, and the functional +GNU `objcopy` → `strip` → `objcopy --add-gnu-debuglink` → `chmod` contract. +Those minimal fragments and required operation ordering are classified as +non-copyrightable facts, methods of operation, or standard syntax—not as +relicensed historical expression. Reviewer `github:zoeyrose` confirmed that +classification and granted the new expressive implementations under MIT. + +## Per-file inventory + +| Current path | Classification and rights basis | Transformation / review | +| --- | --- | --- | +| `LICENSE` | Standard MIT license text | New default license; not project implementation | +| `.gitignore` | Contribution ledger above | Reviewed for generated and mutable paths | +| `.github/dependabot.yml` | Contribution ledger above | Dependency-update schedule; no vendored material | +| `.github/workflows/check.yml` | Contribution ledger above | Retained validation only; immutable Action pin | +| `.github/workflows/pr-title.yml` | Contribution ledger above | Conventional-title policy; immutable Action-free workflow | +| `.github/workflows/release.yml` | Contribution ledger above | Semantic-release policy; immutable Action pins | +| `.releaserc.json` | Contribution ledger above | Semantic-release configuration | +| `AGENTS.md` | Contribution ledger above | Rewritten for the mixed-license live tree | +| `CONTRIBUTING.md` | Contribution ledger above | Rewritten for default and exception scopes | +| `README.md` | Contribution ledger above | Rewritten for retained tools and license disclosure | +| `PROVENANCE.md` | Contemporaneous MIT provenance record | New complete-tree audit record | +| `split_symbols.sh` | Contemporaneous MIT reimplementation | Historical GPL bytes excluded; CLI contract preserved | +| `stacktrace.py` | Contemporaneous MIT reimplementation | Historical GPL bytes excluded; CLI contract preserved | +| `tests/__init__.py` | Contemporaneous MIT test metadata | New | +| `tests/test_diagnostics.py` | Contemporaneous MIT tests | New CLI/parser regression coverage | + +Every path under `map-checker-qt/`, including source, tests, configuration, +`.ui` files, generated `ui_*.py` files, launchers, the catalog lock, README, and +the subtree `LICENSE`, is excluded from the table because it remains +GPL-2.0-or-later as one scoped unit. The complete unmodified GPLv2 text moved +from baseline root `COPYING` to `map-checker-qt/LICENSE`; both blobs have +SHA-256 `d8c320ffc0030d1b096ae4732b50d2b811cf95e9a9b7377c1127b2563e0a0388`. + +## Third-party and generated boundaries + +- `map-checker-qt` uses PyQt5 at runtime under PyQt's own distribution terms; + PyQt5 is not vendored here. +- `map-checker-qt/catalog.lock.json` downloads the separately licensed + `atrinik/content` catalog at its exact tag, commit, URL, and SHA-256. The + downloaded package remains ignored and is not part of this repository. +- Generated `map-checker-qt/ui/ui_*.py` files remain inside the GPL subtree and + are not presented as MIT material. +- GitHub Actions and semantic-release packages are external dependencies under + their own terms and are consumed by immutable coordinates or exact versions, + not vendored. + +The removed converter, legacy checker, map maker, mapset, randomizer, and world +viewer—including their Crossfire, Daimonin-fork, Nicolas Weeger, and Edwin +Miltenburg contribution boundaries—remain available only in historical GPL +revisions and releases. No current tracked path outside `map-checker-qt/` +contains those implementations or notices. diff --git a/README.md b/README.md index d538a4c..0c3ae66 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # Atrinik tools -This repository contains the committed standalone utilities used to inspect, -author, package, and diagnose Atrinik data and binaries. Component build tools -live with the component they operate on: +This repository contains the retained standalone utilities used to inspect and +diagnose Atrinik data and binaries. Component build tools live with the +component they operate on: -- content collection and validation: [`atrinik/content`](https://github.com/atrinik/content) - (`main` for replacement, `1.x` for Classic) +- content collection and validation: shared + [`atrinik/content@main`](https://github.com/atrinik/content); retained `1.x` + revisions are historical release and migration evidence only - client dependency and packaging tools: replacement [`atrinik/client`](https://github.com/atrinik/client), maintained Classic [`atrinik/classic/client`](https://github.com/atrinik/classic/tree/main/client) @@ -15,16 +16,10 @@ live with the component they operate on: ## Included utilities -- `gridarta-types-convert/` and `gridarta_materials.pl`: editor data conversion -- `map-checker/` and `map-checker-qt/`: map inspection applications -- `map-maker/`: map-maker package assembly -- `mapset/`: operations over sets of maps -- `randomizer/`: archetype variation utility -- `worldviewer/`: browser-based world viewer +- `map-checker-qt/`: checksum-pinned map inspection application - `split_symbols.sh` and `stacktrace.py`: native binary diagnostics -Some utilities retain their original runtime and dependency requirements. Read -the documentation beside a utility before using it against a working content +Read the Qt checker documentation before using it against a working content checkout. The former `atrinik_bot/` utility is now maintained independently as @@ -45,17 +40,15 @@ generated bindings for the protocol generation they implement. ## License -The extracted source retains its existing GNU General Public License terms. -See [COPYING](COPYING). Those terms are not a blanket outbound reuse ban: -under the -[canonical provenance policy](https://github.com/atrinik/atrinik/blob/main/docs/PROVENANCE.md), -an MIT destination may inspect exact, independently separable material as -source reference, copy it, migrate or port it, translate or adapt it, or -relicense it only after the canonical audit proves each selected contribution -is the applicable named grantor's original work. Each contribution must be -solely authored by that grantor and fall within the row's temporal scope. -Distinct contributions may cite different rows only when each independently -satisfies one row. Rows cannot be combined to cover jointly authored -contributions, generated output, or inseparable mixed work. Later material needs -contemporaneous compatible permission. This does not change the source license -here or approve a GPL dependency or bundle. +This repository is MIT by default under the root [LICENSE](LICENSE), with one +directory exception: the complete `map-checker-qt/` subtree remains +GPL-2.0-or-later under [its own license](map-checker-qt/LICENSE). That exception +includes the checker's source, tests, configuration, `.ui` files, generated +`ui_*.py` files, launchers, and documentation. The current source archive is +therefore not MIT-only. + +PyQt5 and the checksum-pinned downloaded content catalog retain their separate +dependency and distribution terms and are not relicensed by either repository +license. See [PROVENANCE.md](PROVENANCE.md) for the current-tree audit and the +historical release boundary. Tags and releases `v1.0.0` through `v1.2.6` +remain described by the GPL terms that applied when they were published. diff --git a/gridarta-types-convert/gridarta-types-convert.c b/gridarta-types-convert/gridarta-types-convert.c deleted file mode 100644 index e6cd8b9..0000000 --- a/gridarta-types-convert/gridarta-types-convert.c +++ /dev/null @@ -1,1267 +0,0 @@ -/** - * @file - * This small program will extract information from Gridarta's types.xml - * file to generate documentation about types and fields. - * - * Files are placed in types documentation dir by default. - * - * To build:
gcc -O3 -Wall -W -pedantic gridarta-types-convert.c
- * -I../../server/src/include -o gridarta-types-convert
- * To run:
./gridarta-types-convert
- * ../../arch/dev/editor/conf/types.xml
- * (adjust the path according to your setup) - * - * Note that someone wishing to tweak this program should know the format of - * Gridarta's types.xml. - * - * Note that "attribute" is used for "field in a object/living structure". - * - * @author Nicolas Weeger */ - -#include -#include -#include - -#include "define.h" -#define EXTERN -#define INIT_C -#include "attack.h" - -/** Root destination dir. */ -const char *destination_dir = "../../server/doc"; -/** Where the files about the fields will be stored. */ -const char *field_dir = "fields"; -/** Where the files about types will be stored. */ -const char *type_dir = "types"; - -/** One attribute in a type. */ -typedef struct { - char *field; - char *name; - char *description; -} type_attribute; - -/** One object type. */ -typedef struct { - int number; - char *name; - char *description; - char *use; - type_attribute **attributes; - int attribute_count; - char **required; - int require_count; -} type_definition; - -/** Defined types. */ -type_definition **types = NULL; - -int type_count = 0; - -/** Definitions all types have by default. */ -type_definition *default_type = NULL; - -/** Dummy object type that non defined objects use. */ -type_definition *fallback_type = NULL; - -/** One list of fields to ignore. */ -typedef struct { - char *name; - int count; - char **fields; -} ignore_list; - -ignore_list **lists = NULL; -int list_count = 0; - -/** One type for an attribute. */ -typedef struct { - char **type; - int *number; - int count; - char *description; -} attribute_type; - -/** One attribute. */ -typedef struct { - char *field; - attribute_type **types; - int type_count; -} attribute_definition; - -attribute_definition **attributes = NULL; -int attribute_count = 0; - -/** One flag. */ -typedef struct { - const char *field; - const char *code_name; -} flag_definition; - -/** Flag mapping. */ -static const flag_definition flags[] = {{"sleep", "FLAG_SLEEP"}, - {"confused", "FLAG_CONFUSED"}, - {"scared", "FLAG_SCARED"}, - {"is_blind", "FLAG_BLIND"}, - {"is_invisible", "FLAG_IS_INVISIBLE"}, - {"is_ethereal", "FLAG_IS_ETHEREAL"}, - {"is_good", "FLAG_IS_GOOD"}, - {"no_pick", "FLAG_NO_PICK"}, - {"walk_on", "FLAG_WALK_ON"}, - {"no_pass", "FLAG_NO_PASS"}, - {"is_animated", "FLAG_ANIMATE"}, - {"slow_move", "FLAG_SLOW_MOVE"}, - {"flying", "FLAG_FLYING"}, - {"monster", "FLAG_MONSTER"}, - {"friendly", "FLAG_FRIENDLY"}, - {"been_applied", "FLAG_BEEN_APPLIED"}, - {"auto_apply", "FLAG_AUTO_APPLY"}, - {"is_neutral", "FLAG_IS_NEUTRAL"}, - {"see_invisible", "FLAG_SEE_INVISIBLE"}, - {"can_roll", "FLAG_CAN_ROLL"}, - {"connect_reset", "FLAG_CONNECT_RESET"}, - {"is_turnable", "FLAG_IS_TURNABLE"}, - {"walk_off", "FLAG_WALK_OFF"}, - {"fly_on", "FLAG_FLY_ON"}, - {"fly_off", "FLAG_FLY_OFF"}, - {"is_used_up", "FLAG_IS_USED_UP"}, - {"identified", "FLAG_IDENTIFIED"}, - {"reflecting", "FLAG_REFLECTING"}, - {"changing", "FLAG_CHANGING"}, - {"splitting", "FLAG_SPLITTING"}, - {"hitback", "FLAG_HITBACK"}, - {"startequip", "FLAG_STARTEQUIP"}, - {"blocksview", "FLAG_BLOCKSVIEW"}, - {"undead", "FLAG_UNDEAD"}, - {"can_stack", "FLAG_CAN_STACK"}, - {"unaggressive", "FLAG_UNAGGRESSIVE"}, - {"reflect_missile", "FLAG_REFL_MISSILE"}, - {"reflect_spell", "FLAG_REFL_SPELL"}, - {"no_magic", "FLAG_NO_MAGIC"}, - {"no_fix_player", "FLAG_NO_FIX_PLAYER"}, - {"is_evil", "FLAG_IS_EVIL"}, - {"run_away", "FLAG_RUN_AWAY"}, - {"pass_thru", "FLAG_PASS_THRU"}, - {"can_pass_thru", "FLAG_CAN_PASS_THRU"}, - {"outdoor", "FLAG_OUTDOOR"}, - {"unique", "FLAG_UNIQUE"}, - {"no_drop", "FLAG_NO_DROP"}, - {"is_indestructible", "FLAG_INDESTRUCTIBLE"}, - {"can_cast_spell", "FLAG_CAST_SPELL"}, - {"two_handed", "FLAG_TWO_HANDED"}, - {"can_use_bow", "FLAG_USE_BOW"}, - {"can_use_armour", "FLAG_USE_ARMOUR"}, - {"can_use_weapon", "FLAG_USE_WEAPON"}, - {"connect_no_push", "FLAG_CONNECT_NO_PUSH"}, - {"connect_no_release", "FLAG_CONNECT_NO_RELEASE"}, - {"has_ready_bow", "FLAG_READY_BOW"}, - {"xrays", "FLAG_XRAYS"}, - {"is_floor", "FLAG_IS_FLOOR"}, - {"lifesave", "FLAG_LIFESAVE"}, - {"is_magical", "FLAG_IS_MAGICAL"}, - {"stand_still", "FLAG_STAND_STILL"}, - {"random_move", "FLAG_RANDOM_MOVE"}, - {"only_attack", "FLAG_ONLY_ATTACK"}, - {"stealth", "FLAG_STEALTH"}, - {"cursed", "FLAG_CURSED"}, - {"damned", "FLAG_DAMNED"}, - {"is_buildable", "FLAG_IS_BUILDABLE"}, - {"no_pvp", "FLAG_NO_PVP"}, - {"is_thrown", "FLAG_IS_THROWN"}, - {"is_male", "FLAG_IS_MALE"}, - {"is_female", "FLAG_IS_FEMALE"}, - {"applied", "FLAG_APPLIED"}, - {"inv_locked", "FLAG_INV_LOCKED"}, - {"has_ready_weapon", "FLAG_READY_WEAPON"}, - {"no_skill_ident", "FLAG_NO_SKILL_IDENT"}, - {"can_see_in_dark", "FLAG_SEE_IN_DARK"}, - {"is_cauldron", "FLAG_IS_CAULDRON"}, - {"is_dust", "FLAG_DUST"}, - {"one_hit", "FLAG_ONE_HIT"}, - {"draw_double_always", "FLAG_DRAW_DOUBLE_ALWAYS"}, - {"berserk", "FLAG_BERSERK"}, - {"no_attack", "FLAG_NO_ATTACK"}, - {"invulnerable", "FLAG_INVULNERABLE"}, - {"quest_item", "FLAG_QUEST_ITEM"}, - {"is_trapped", "FLAG_IS_TRAPPED"}, - {"sys_object", "FLAG_SYS_OBJECT"}, - {"use_fix_pos", "FLAG_USE_FIX_POS"}, - {"unpaid", "FLAG_UNPAID"}, - {"hidden", "FLAG_HIDDEN"}, - {"make_invisible", "FLAG_MAKE_INVISIBLE"}, - {"make_ethereal", "FLAG_MAKE_ETHEREAL"}, - {"is_player", "FLAG_IS_PLAYER"}, - {"is_named", "FLAG_IS_NAMED"}, - {"no_teleport", "FLAG_NO_TELEPORT"}, - {"corpse", "FLAG_CORPSE"}, - {"corpse_forced", "FLAG_CORPSE_FORCED"}, - {"player_only", "FLAG_PLAYER_ONLY"}, - {"one_drop", "FLAG_ONE_DROP"}, - {"cursed_perm", "FLAG_PERM_CURSED"}, - {"damned_perm", "FLAG_PERM_DAMNED"}, - {"door_closed", "FLAG_DOOR_CLOSED"}, - {"is_spell", "FLAG_IS_SPELL"}, - {"is_missile", "FLAG_IS_MISSILE"}, - {"draw_direction", "FLAG_DRAW_DIRECTION"}, - {"draw_double", "FLAG_DRAW_DOUBLE"}, - {"is_assassin", "FLAG_IS_ASSASSINATION"}, - {"no_save", "FLAG_NO_SAVE"}, - {NULL, NULL}}; - -/** - * Find a flag in ::flags. - * @param name Flag name. - * @return The flag if found, NULL otherwise. */ -const flag_definition *find_flag(const char *name) { - int flag; - - for (flag = 0; flags[flag].field; flag++) { - if (!strcmp(flags[flag].field, name)) { - return &flags[flag]; - } - } - - return NULL; -} - -/** - * Names of attack types to use when saving them to file. */ -char *attack_save[NROFATTACKS] = { - "impact", "slash", "cleave", "pierce", "weaponmagic", "fire", "cold", - "electricity", "poison", "acid", "magic", "mind", "blind", "paralyze", - "force", "godpower", "chaos", "drain", "slow", "confusion", "internal"}; - -/** One type. */ -typedef struct { - const char *code_name; - int value; -} type_name; - -/** All the types we want to create documentation for. */ -static type_name type_names[] = {{"PLAYER", PLAYER}, - {"BULLET", BULLET}, - {"ROD", ROD}, - {"TREASURE", TREASURE}, - {"POTION", POTION}, - {"FOOD", FOOD}, - {"REGION_MAP", REGION_MAP}, - {"BOOK", BOOK}, - {"CLOCK", CLOCK}, - {"MATERIAL", MATERIAL}, - {"DUPLICATOR", DUPLICATOR}, - {"LIGHTNING", LIGHTNING}, - {"ARROW", ARROW}, - {"BOW", BOW}, - {"WEAPON", WEAPON}, - {"ARMOUR", ARMOUR}, - {"PEDESTAL", PEDESTAL}, - {"CONFUSION", CONFUSION}, - {"DOOR", DOOR}, - {"KEY", KEY}, - {"MAP", MAP}, - {"MAGIC_MIRROR", MAGIC_MIRROR}, - {"SPELL", SPELL}, - {"SHIELD", SHIELD}, - {"HELMET", HELMET}, - {"GREAVES", GREAVES}, - {"MONEY", MONEY}, - {"CLASS", CLASS}, - {"GRAVESTONE", GRAVESTONE}, - {"AMULET", AMULET}, - {"PLAYER_MOVER", PLAYER_MOVER}, - {"CREATOR", CREATOR}, - {"SKILL", SKILL}, - {"EXPERIENCE", EXPERIENCE}, - {"BLINDNESS", BLINDNESS}, - {"GOD", GOD}, - {"DETECTOR", DETECTOR}, - {"SKILL_ITEM", SKILL_ITEM}, - {"DEAD_OBJECT", DEAD_OBJECT}, - {"DRINK", DRINK}, - {"MARKER", MARKER}, - {"HOLY_ALTAR", HOLY_ALTAR}, - {"PEARL", PEARL}, - {"GEM", GEM}, - {"SOUND_AMBIENT", SOUND_AMBIENT}, - {"FIREWALL", FIREWALL}, - {"CHECK_INV", CHECK_INV}, - {"EXIT", EXIT}, - {"SHOP_FLOOR", SHOP_FLOOR}, - {"RING", RING}, - {"FLOOR", FLOOR}, - {"FLESH", FLESH}, - {"INORGANIC", INORGANIC}, - {"LIGHT_APPLY", LIGHT_APPLY}, - {"WALL", WALL}, - {"LIGHT_SOURCE", LIGHT_SOURCE}, - {"MISC_OBJECT", MISC_OBJECT}, - {"MONSTER", MONSTER}, - {"SPAWN_POINT", SPAWN_POINT}, - {"SPAWN_POINT_MOB", SPAWN_POINT_MOB}, - {"SPAWN_POINT_INFO", SPAWN_POINT_INFO}, - {"LIGHT_REFILL", LIGHT_REFILL}, - {"BOOK_SPELL", BOOK_SPELL}, - {"ORGANIC", ORGANIC}, - {"CLOAK", CLOAK}, - {"CONE", CONE}, - {"SPINNER", SPINNER}, - {"GATE", GATE}, - {"BUTTON", BUTTON}, - {"HANDLE", TYPE_HANDLE}, - {"WORD_OF_RECALL", WORD_OF_RECALL}, - {"SIGN", SIGN}, - {"BOOTS", BOOTS}, - {"GLOVES", GLOVES}, - {"BASE_INFO", BASE_INFO}, - {"RANDOM_DROP", RANDOM_DROP}, - {"BRACERS", BRACERS}, - {"POISONING", POISONING}, - {"SAVEBED", SAVEBED}, - {"WAND", WAND}, - {"ABILITY", ABILITY}, - {"SCROLL", SCROLL}, - {"DIRECTOR", DIRECTOR}, - {"GIRDLE", GIRDLE}, - {"FORCE", FORCE}, - {"POTION_EFFECT", POTION_EFFECT}, - {"JEWEL", JEWEL}, - {"NUGGET", NUGGET}, - {"EVENT_OBJECT", EVENT_OBJECT}, - {"WAYPOINT_OBJECT", WAYPOINT_OBJECT}, - {"QUEST_CONTAINER", QUEST_CONTAINER}, - {"CONTAINER", CONTAINER}, - {"WEALTH", WEALTH}, - {"BEACON", BEACON}, - {"MAP_EVENT_OBJ", MAP_EVENT_OBJ}, - {"COMPASS", COMPASS}, - {"MAP_INFO", MAP_INFO}, - {"SWARM_SPELL", SWARM_SPELL}, - {"RUNE", RUNE}, - {"POWER_CRYSTAL", POWER_CRYSTAL}, - {"CORPSE", CORPSE}, - {"DISEASE", DISEASE}, - {"SYMPTOM", SYMPTOM}, - {NULL, 0}}; - -/** Maximum object types. */ -#define OBJECT_TYPE_MAX 160 - -/** - * Gets the attribute for the specified type. If it doesn't exist, create - * it. - * - * If the attribute is already defined, return the existing one, after - * cleaning its fields. - * @param type Type to get attribute for. - * @param attribute The attribute. - * @return The new type attribute. */ -type_attribute *get_attribute_for_type(type_definition *type, const char *attribute) { - type_attribute *ret; - int test; - - for (test = 0; test < type->attribute_count; test++) { - if (!strcmp(type->attributes[test]->field, attribute)) { - ret = type->attributes[test]; - free(ret->name); - ret->name = NULL; - free(ret->description); - ret->description = NULL; - return ret; - } - } - - ret = calloc(1, sizeof(type_attribute)); - ret->field = strdup(attribute); - - type->attribute_count++; - type->attributes = realloc(type->attributes, type->attribute_count * sizeof(type_attribute *)); - type->attributes[type->attribute_count - 1] = ret; - - return ret; -} - -/** - * Free a type attribute. - * @param attr Attribute to free. */ -void free_attribute(type_attribute *attr) { - free(attr->field); - free(attr->name); - free(attr->description); - free(attr); -} - -/** - * Copy attributes from one type definition to another. - * @param source Where to copy attributes from. - * @param type Where to copy attributes to. */ -void copy_attributes(const type_definition *source, type_definition *type) { - int attr; - type_attribute *add; - - if (!source || source->attribute_count == 0) { - return; - } - - for (attr = 0; attr < source->attribute_count; attr++) { - add = get_attribute_for_type(type, source->attributes[attr]->field); - add->name = strdup(source->attributes[attr]->name); - - if (source->attributes[attr]->description) { - add->description = strdup(source->attributes[attr]->description); - } - } -} - -/** - * Copy default attributes to a type definition. - * @param type The type definition to copy default attribute to. */ -void copy_default_attributes(type_definition *type) { - if (!default_type) { - return; - } - - copy_attributes(default_type, type); -} - -/** - * Returns a new type_definition having the default attributes. */ -type_definition *get_type_definition() { - type_definition *ret = calloc(1, sizeof(type_definition)); - - ret->attribute_count = 0; - ret->attributes = NULL; - - if (default_type) { - copy_default_attributes(ret); - } - - return ret; -} - -/** - * Used for type import. - * @param name Name of the type to import. - * @return Type definition from ::types if found, NULL otherwise. */ -type_definition *find_type_definition(const char *name) { - int type; - - for (type = 0; type < type_count; type++) { - if (!strcmp(types[type]->name, name)) { - return types[type]; - } - } - - printf("Type not found for import: %s\n", name); - return NULL; -} - -/** To sort attributes using qsort(). */ -int sort_type_attribute(const void *a, const void *b) { - const type_attribute **la = (const type_attribute **)a; - const type_attribute **lb = (const type_attribute **)b; - - return strcmp((*la)->name, (*lb)->name); -} - -/** Used for ::fake_names. */ -typedef struct { - /** Fake name. */ - const char *fake_name; - - /** Real name. */ - char *real_name; -} fake_name_definition; - -/** - * Fake names, because things like "animation" actually is "animation_id" - * and "object_int1" is "enemy_count". */ -static fake_name_definition fake_names[] = {{"animation", "animation_id"}, - {"object_int1", "enemy_count"}, - {"object_int2", "attacked_by_count"}, - {"object_int3", "ownercount"}, - {"movement_type", "move_type"}, - {"sub_type", "sub_type"}, - {"container", "weight_limit"}, - {NULL, NULL}}; - -/** - * Find real name from fake name in the ::fake_names array. - * @param name Fake name to find. - * @return The real name, or the passed name if not found. */ -static char *find_fake_attr_name(char *name) { - int i; - - for (i = 0; fake_names[i].fake_name; i++) { - if (!strcmp(name, fake_names[i].fake_name)) { - return fake_names[i].real_name; - } - } - - return name; -} - -/** - * Find ignore list from ::lists. - * @param name The ignore list to find. - * @return The ignore list if found, NULL otherwise. */ -ignore_list *find_ignore_list(const char *name) { - int list; - - for (list = 0; list < list_count; list++) { - if (strcmp(lists[list]->name, name) == 0) { - return lists[list]; - } - } - - return NULL; -} - -/** - * Remove an attribute from type definition. - * - * Used for ignoring attributes. - * @param type Type definition to remove attribute from. - * @param attribute The attribute to remove. */ -void ignore_attribute(type_definition *type, const char *attribute) { - int find; - - for (find = 0; find < type->attribute_count; find++) { - if (!strcmp(attribute, type->attributes[find]->field)) { - free_attribute(type->attributes[find]); - - if (find < type->attribute_count - 1) { - type->attributes[find] = type->attributes[type->attribute_count - 1]; - } - - type->attribute_count--; - return; - } - } -} - -/** - * Remove all attributes in the specified list from the type. - * @param type Type definition to remove ignored attributes. - * @param list Ignore list. */ -void ignore_attributes(type_definition *type, ignore_list *list) { - int attr; - - if (!list) { - printf("%s has empty ignore list?\n", type->name); - return; - } - - for (attr = 0; attr < list->count; attr++) { - ignore_attribute(type, list->fields[attr]); - } -} - -/** - * Add a required parameter to the specified type. - * @param type Type definition to. - * @param buf Line read from the file, non processed. */ -void add_required_parameter(type_definition *type, const char *buf) { - char *sn, *en, *sv, *ev; - char value[200], name[200], temp[200]; - const flag_definition *flag; - - if (type == fallback_type) { - /* the "Misc" type has dummy requirements, don't take that into account. - * */ - return; - } - - sn = strstr(buf, "arch"); - - if (!sn) { - return; - } - - sn = strchr(sn, '"'); - en = strchr(sn + 1, '"'); - sv = strstr(buf, "value"); - sv = strchr(sv, '"'); - ev = strchr(sv + 1, '"'); - - name[en - sn - 1] = '\0'; - strncpy(name, sn + 1, en - sn - 1); - value[ev - sv - 1] = '\0'; - strncpy(value, sv + 1, ev - sv - 1); - - type->require_count++; - type->required = realloc(type->required, type->require_count * sizeof(char *)); - - flag = find_flag(name); - - if (flag) { - snprintf(temp, - sizeof(temp), - "@ref %s %s", - flag->code_name, - strcmp(value, "0") ? "set" : "unset"); - } else { - snprintf(temp, sizeof(temp), "@ref object::%s = %s", name, value); - } - - type->required[type->require_count - 1] = strdup(temp); -} - -/** - * Read all lines related to a type. - * @param type Type definition. - * @param file File to read from. - * @param block_end If encountered on a line, will stop reading. */ -void read_type(type_definition *type, FILE *file, const char *block_end) { - char buf[200], tmp[200]; - char *find, *end; - type_attribute *attr; - - while (fgets(buf, sizeof(buf), file)) { - if (strstr(buf, block_end) != NULL) { - if (type->attribute_count) { - qsort(type->attributes, - type->attribute_count, - sizeof(type_attribute *), - sort_type_attribute); - } - - return; - } - - if (strstr(buf, "") != NULL) { - while (fgets(buf, sizeof(buf), file)) { - if (strstr(buf, "") != NULL) { - break; - } else if (strstr(buf, "description) { - type->description = - realloc(type->description, strlen(type->description) + strlen(buf) + 1); - strcat(type->description, buf); - } else { - type->description = strdup(buf); - } - } - - find = strstr(type->description, "]]>"); - - if (find) { - type->description[find - type->description] = '\0'; - } - while (type->description[strlen(type->description) - 1] == '\n') { - type->description[strlen(type->description) - 1] = '\0'; - } - } - - if (strstr(buf, "") != NULL) { - while (fgets(buf, sizeof(buf), file)) { - if (strstr(buf, "") != NULL) { - break; - } - - find = strstr(buf, "arch="); - - if (!find) { - continue; - } - - find = strchr(find + 1, '"'); - - if (!find) { - continue; - } - - end = strchr(find + 1, '"'); - - if (!end) { - continue; - } - - tmp[end - find - 1] = '\0'; - strncpy(tmp, find + 1, end - find - 1); - ignore_attribute(type, tmp); - } - } - - if (strstr(buf, "") != NULL) { - while (fgets(buf, sizeof(buf), file)) { - if (strstr(buf, "") != NULL) { - break; - } - - add_required_parameter(type, buf); - } - } - - if (strstr(buf, "name, tmp); - } - } - - if (strstr(buf, "") != NULL) { - continue; - } - - find = strstr(buf, "arch"); - - if (!find) { - continue; - } - - find = strchr(find, '"'); - end = strchr(find + 1, '"'); - - if (end == find + 1) { - /* Empty arch, meaning inventory or such, ignore. */ - continue; - } - - tmp[end - find - 1] = '\0'; - strncpy(tmp, find + 1, end - find - 1); - - attr = get_attribute_for_type(type, tmp); - - find = strstr(buf, "editor"); - find = strchr(find, '"'); - end = strchr(find + 1, '"'); - tmp[end - find - 1] = '\0'; - strncpy(tmp, find + 1, end - find - 1); - attr->name = strdup(tmp); - - while (fgets(buf, sizeof(buf), file)) { - if (strstr(buf, "") != NULL) { - break; - } - - if (attr->description) { - attr->description = - realloc(attr->description, strlen(attr->description) + strlen(buf) + 1); - strcat(attr->description, buf); - } else { - attr->description = strdup(buf); - } - } - - if (attr->description) { - while (attr->description[strlen(attr->description) - 1] == '\n') { - attr->description[strlen(attr->description) - 1] = '\0'; - } - } - } - } -} - -/** - * Get an attribute, create it if it doesn't exist yet. - * @param name Name of the attribute to get/create. - * @return Found/created attribute. */ -attribute_definition *get_attribute(const char *name) { - int attr; - attribute_definition *ret; - - for (attr = 0; attr < attribute_count; attr++) { - if (!strcmp(attributes[attr]->field, name)) { - return attributes[attr]; - } - } - - ret = calloc(1, sizeof(attribute_definition)); - attribute_count++; - attributes = realloc(attributes, attribute_count * sizeof(attribute_definition *)); - attributes[attribute_count - 1] = ret; - - ret->field = strdup(name); - - return ret; -} - -/** - * Gets a type description for specified attribute, create it if doesn't - * exist. - * @param attribute Attribute to get description for. - * @param description The description. - * @return Attribute type. */ -attribute_type *get_description_for_attribute(attribute_definition *attribute, - const char *description) { - int desc; - attribute_type *add; - - for (desc = 0; desc < attribute->type_count; desc++) { - if (!description && !attribute->types[desc]->description) { - return attribute->types[desc]; - } - - if (description && attribute->types[desc]->description && - !strcmp(description, attribute->types[desc]->description)) { - return attribute->types[desc]; - } - } - - add = calloc(1, sizeof(attribute_type)); - attribute->type_count++; - attribute->types = realloc(attribute->types, attribute->type_count * sizeof(attribute_type)); - attribute->types[attribute->type_count - 1] = add; - - if (description) { - add->description = strdup(description); - } - - return add; -} - -/** - * Add type to attribute. - * @param attribute Attribute defition to add to. - * @param type Type definition. - * @param attr Attribute ID. */ -void add_type_to_attribute(attribute_definition *attribute, type_definition *type, int attr) { - attribute_type *att; - - att = get_description_for_attribute(attribute, type->attributes[attr]->description); - att->count++; - att->type = realloc(att->type, att->count * sizeof(const char *)); - att->number = realloc(att->number, att->count * sizeof(int)); - att->type[att->count - 1] = strdup(type->name); - att->number[att->count - 1] = type->number; -} - -/** - * Read the contents of a \ tag. - * @param name Name of the ignore list. - * @param file File to read from. */ -void read_ignore_list(const char *name, FILE *file) { - char buf[200], tmp[200]; - char *start, *end; - ignore_list *list; - - list = calloc(1, sizeof(ignore_list)); - list_count++; - lists = realloc(lists, list_count * sizeof(ignore_list *)); - lists[list_count - 1] = list; - list->name = strdup(name); - - while (fgets(buf, sizeof(buf), file)) { - if (strstr(buf, "") != NULL) { - return; - } - - start = strstr(buf, "arch="); - - if (!start) { - continue; - } - - start = strchr(start + 1, '"'); - - if (!start) { - continue; - } - - end = strchr(start + 1, '"'); - - if (!end) { - continue; - } - - tmp[end - start - 1] = '\0'; - strncpy(tmp, start + 1, end - start - 1); - - list->count++; - list->fields = realloc(list->fields, list->count * sizeof(char *)); - list->fields[list->count - 1] = strdup(tmp); - } -} - -/** Fields part of the living structure. */ -static const char *in_living[] = {"exp", - - "hp", - "maxhp", - "sp", - "maxsp", - - "food", - "dam", - "wc", - "ac", - "wc_range", - - "Str", - "Dex", - "Con", - "Wis", - "Cha", - "Int", - "Pow", - NULL}; - -/** Custom attributes we know about, to point to the right page. */ -static const char *custom_attributes[] = {"faction", - "faction_kill_penalty", - "faction_rep", - "notification_action", - "notification_delay", - "notification_message", - "notification_shortcut", - "spawn_time", - "match", - NULL}; - -int is_custom_attribute(const char *attribute) { - int val, i; - - for (val = 0; custom_attributes[val] != NULL; val++) { - if (!strcmp(custom_attributes[val], attribute)) { - return 1; - } - } - - if (!strncmp(attribute, "attack_", 7) || !strncmp(attribute, "protection_", 11)) { - attribute += (*attribute == 'a' ? 7 : 11); - - for (i = 0; i < NROFATTACKS; i++) { - if (!strcmp(attribute, attack_save[i])) { - return 1; - } - } - } - - return 0; -} - -/** - * Write the part to the right of a \@ref for the specified attribute. - * @param attribute Attribute. - * @param file File to write to. */ -void write_attribute_reference(char *attribute, FILE *file) { - const flag_definition *flag = find_flag(attribute); - int val; - - if (flag) { - fprintf(file, "%s", flag->code_name); - return; - } - - for (val = 0; in_living[val] != NULL; val++) { - if (!strcasecmp(in_living[val], attribute)) { - fprintf(file, "liv::%s", in_living[val]); - return; - } - } - - if (is_custom_attribute(attribute)) { - fprintf(file, "page_custom_attributes \"%s\"", attribute); - return; - } - - if (!strcmp(attribute, "connected")) { - fprintf(file, "page_connected \"connection value\""); - return; - } - - fprintf(file, "obj::%s", find_fake_attr_name(attribute)); -} - -/** - * Write a type definition file. - * @param type Type definition to write. */ -void write_type_file(type_definition *type) { - FILE *file; - char buf[200]; - int attr, req; - - snprintf(buf, sizeof(buf), "%s/%s/type_%d.dox", destination_dir, type_dir, type->number); - file = fopen(buf, "w+"); - - fprintf(file, "/**\n"); - - /* Auto-generate documentation for the type, so no need to change define.h - * */ - if (type->number > 0) { - for (req = 0; type_names[req].code_name != NULL; req++) { - if (type_names[req].value == type->number) { - fprintf(file, - "@var %s\nSee @ref page_type_%d\n*/\n\n/**\n", - type_names[req].code_name, - type->number); - break; - } - } - } - - fprintf(file, "@page page_type_%d %s\n\n", type->number, type->name); - fprintf(file, "\n@section Description\n"); - fprintf(file, "%s\n\n", type->description); - - if (type != fallback_type) { - fprintf(file, "\n\nType defined by:\n"); - - if (type->number && type->number < OBJECT_TYPE_MAX) { - fprintf(file, "- @ref object::type = %d\n", type->number); - } - - for (req = 0; req < type->require_count; req++) { - fprintf(file, "- %s\n", type->required[req]); - } - } - - fprintf(file, "\n\n@section Attributes\n\n"); - fprintf(file, - "\n\t\n\t\t\n\t\t\n\t\t\n"); - - for (attr = 0; attr < type->attribute_count; attr++) { - fprintf(file, "\t\n\t\t\n\t\t\n\t\t\n\t\n", - type->attributes[attr]->description ? type->attributes[attr]->description - : "(no description)"); - } - - fprintf(file, "
AttributeFieldDescription\n\t
%s@ref ", type->attributes[attr]->name); - write_attribute_reference(type->attributes[attr]->field, file); - fprintf(file, - "%s\n\t\t
\n*/\n"); - - fclose(file); -} - -/** Write index of all types. */ -void write_type_index() { - FILE *index; - int type; - char buf[200]; - - snprintf(buf, sizeof(buf), "%s/%s/types.dox", destination_dir, type_dir); - index = fopen(buf, "w+"); - - if (index == NULL) { - printf("Could not open %s\n", buf); - exit(0); - } - - fprintf(index, "/**\n@page type_index Type index\n"); - fprintf( - index, - "Types not listed here have the attributes defined in @ref page_type_0 \"this page\".\n\n"); - - for (type = 0; type < type_count; type++) { - if (types[type]) { - fprintf(index, "- @ref page_type_%d \"%s\"\n", types[type]->number, types[type]->name); - } - } - - fprintf(index, "*/\n"); - - fclose(index); -} - -/** - * Write the description of a field. - * @param attribute Attribute definition to write. */ -void write_attribute_file(attribute_definition *attribute) { - FILE *file; - char buf[200]; - int type, desc; - const char *end; - - snprintf(buf, sizeof(buf), "%s/%s/field_%s.dox", destination_dir, field_dir, attribute->field); - file = fopen(buf, "w+"); - - fprintf(file, "/**\n@var "); - write_attribute_reference(attribute->field, file); - fprintf(file, "\n@sa @ref page_field_%s\n*/\n\n", attribute->field); - - fprintf(file, "/**\n"); - fprintf(file, "@page page_field_%s ", attribute->field); - write_attribute_reference(attribute->field, file); - fprintf(file, " Uses\n"); - - fprintf(file, "\n\t\n\t\t\n\t\t\n\t"); - - for (desc = 0; desc < attribute->type_count; desc++) { - fprintf(file, "\t\n\t\t\n\t\n", - attribute->types[desc]->description ? attribute->types[desc]->description - : "(no description)"); - } - - fprintf(file, "\n*/\n"); - fclose(file); -} - -/** - * Main function of the program. */ -int main(int argc, char **argv) { - FILE *xml; - int number, attr, dummy; - char buf[200], tmp[200]; - char *start, *end; - type_definition *type; - - if (argc < 2) { - printf("Syntax: %s /path/to/Gridarta/types.xml\n", argv[0]); - return 1; - } - - /* Dummy type number for special types. */ - dummy = OBJECT_TYPE_MAX + 50; - xml = fopen(argv[1], "r"); - - if (!xml) { - printf("Could not open file: %s\n", argv[1]); - return 1; - } - - while (fgets(buf, sizeof(buf), xml) != NULL) { - if (buf[0] == '#') { - continue; - } - - if (strstr(buf, "")) { - default_type = get_type_definition(); - default_type->name = strdup("(default type)"); - read_type(default_type, xml, ""); - continue; - } - - if (strstr(buf, "number = number; - type->name = strdup(tmp); - - read_type(type, xml, ""); - } - } - - free(fallback_type->description); - fallback_type->description = - strdup("This type regroups all types who don't have a specific definition."); - - for (number = 0; number < type_count; number++) { - for (attr = 0; attr < types[number]->attribute_count; attr++) { - add_type_to_attribute(get_attribute(types[number]->attributes[attr]->field), - types[number], - attr); - } - } - - write_type_index(); - - for (number = 0; number < type_count; number++) { - write_type_file(types[number]); - } - - write_type_file(fallback_type); - - for (attr = 0; attr < attribute_count; attr++) { - if (!is_custom_attribute(attributes[attr]->field)) { - write_attribute_file(attributes[attr]); - } - } - - fclose(xml); - free(types); - return 0; -} diff --git a/gridarta_materials.pl b/gridarta_materials.pl deleted file mode 100755 index 4b73293..0000000 --- a/gridarta_materials.pl +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/perl -#************************************************************************* -#* Atrinik, a Multiplayer Online Role Playing Game * -#* * -#* Copyright (C) 2009-2014 Zoey Rose and Atrinik Development Team * -#* * -#* Fork from Crossfire (Multiplayer game for X-windows). * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU General Public License as published by * -#* the Free Software Foundation; either version 2 of the License, or * -#* (at your option) any later version. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU General Public License for more details. * -#* * -#* You should have received a copy of the GNU General Public License * -#* along with this program; if not, write to the Free Software * -#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * -#* * -#* The author can be reached at admin@atrinik.org * -#************************************************************************* - -# This script will collect materials information from Atrinik server -# and the arch/materials file, and will output XML for use in lists.xml -# and bitmasks.xml files. - -use strict; -use warnings; - -print "\n"; -open FILE, "../server/src/include/material.h" or die $!; - -my $bit = 0; -my $in_materials = 0; -my %materials; - -while (my $line = ) -{ - if ($line eq "/*@}*/\n") - { - $in_materials = 0; - } - elsif ($in_materials) - { - if ((substr $line, 0, 10) eq "#define M_") - { - my $material = lc(substr $line, 10, index((substr $line, 10), " ")); - my $material_id = substr($line, 10 + length($material), -1); - $material_id =~ s/^\s+//; - $material =~ s/_/ /; - $material =~ s/\b(\w)/\u$1/g; - $materials{$material_id} = $material; - print "\t\n"; - $bit++; - } - } - elsif ((substr $line, 0, 15) eq "#define M_NONE ") - { - $in_materials = 1; - } -} - -close(FILE); -print "\n\n\n"; - -open FILE, "../arch/materials" or die $!; - -my $last_material = 0; -my %materials_real; - -while (my $line = ) -{ - chomp($line); - - if ((substr $line, 0, 14) eq "material_real ") - { - $last_material = substr($line, 14); - $materials_real{$last_material} = { - "name", "" - }; - } - elsif ((substr $line, 0, 5) eq "name ") - { - $materials_real{$last_material}{"name"} = substr($line, 5); - } - elsif ((substr $line, 0, 5) eq "type ") - { - $materials_real{$last_material}{"type"} = substr($line, 5); - } -} - -close(FILE); - -print "\n"; -print "\t\n"; - -foreach my $material (sort {$a <=> $b} keys %materials_real) -{ - my $material_name = lc($materials{$materials_real{$material}{"type"}}); - print "\t\n"; -} - -print "\n"; - diff --git a/COPYING b/map-checker-qt/LICENSE similarity index 100% rename from COPYING rename to map-checker-qt/LICENSE diff --git a/map-checker-qt/README.md b/map-checker-qt/README.md index d717cbd..676334e 100644 --- a/map-checker-qt/README.md +++ b/map-checker-qt/README.md @@ -1,5 +1,11 @@ # Atrinik Qt map checker +This complete subtree remains GPL-2.0-or-later under [LICENSE](LICENSE), +including source, tests, configuration, `.ui` files, generated `ui_*.py` files, +launchers, and this documentation. PyQt5 and the downloaded Atrinik content +catalog retain their own dependency and distribution terms and are not +relicensed here. + The Qt map checker runs the authoritative Atrinik content identity catalog over the complete authored repository before it scans a selected map or directory. The catalog implementation is not copied into this repository: `catalog.lock.json` diff --git a/map-checker/Readme.txt b/map-checker/Readme.txt deleted file mode 100644 index b5e15d4..0000000 --- a/map-checker/Readme.txt +++ /dev/null @@ -1,16 +0,0 @@ -=========================== -=== Atrinik Map Checker === -=========================== - -=========== -= WINDOWS = -=========== -To run Python scripts you need the latest Python 2.7. -Download installer: http://www.python.org/ftp/python/2.7.6/python-2.7.6.msi -Install it to the default folder. - -For the map-checker specifically, you need PyGTK: -http://ftp.gnome.org/pub/GNOME/binaries/win32/pygtk/2.24/pygtk-all-in-one-2.24.2.win32-py2.7.msi -Install this to the default (Python) folder. - -Use the start_gui.bat file to start the map-checker. diff --git a/map-checker/map-checker.py b/map-checker/map-checker.py deleted file mode 100755 index 609bace..0000000 --- a/map-checker/map-checker.py +++ /dev/null @@ -1,1752 +0,0 @@ -#!/usr/bin/python -#************************************************************************* -#* Atrinik, a Multiplayer Online Role Playing Game * -#* * -#* Copyright (C) 2009-2014 Zoey Rose and Atrinik Development Team * -#* * -#* Fork from Crossfire (Multiplayer game for X-windows). * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU General Public License as published by * -#* the Free Software Foundation; either version 2 of the License, or * -#* (at your option) any later version. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU General Public License for more details. * -#* * -#* You should have received a copy of the GNU General Public License * -#* along with this program; if not, write to the Free Software * -#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * -#* * -#* The author can be reached at admin@atrinik.org * -#************************************************************************* - -# Application to check Atrinik maps for common errors. - -import sys, os, getopt, re, webbrowser, subprocess -try: - from ConfigParser import ConfigParser - from StringIO import StringIO -# Python 3.x -except: - from configparser import ConfigParser - from io import StringIO - -# We will need some recursion. -sys.setrecursionlimit(50000) - -# Defines for different types of errors. -class errors: - # A warning; often not an error, but should be checked anyway. - warning = 0 - # Low priority. - low = 1 - # Medium. - medium = 2 - # High. - high = 3 - # A critical error. - critical = 4 - - # Text representations of the above. - text = ["WARNING", "LOW", "MEDIUM", "HIGH", "CRITICAL"] - # ANSI escape sequences to make colors for above errors. - colors = ["\033[30m", "\033[35m", "\033[36m", "\033[34m", "\033[31m"] - # Pango (Hex/word) colors for above errors. - pango_colors = ["black", "magenta", "cyan", "blue", "red"] - -# Common escape sequences. -class colors: - bold = "\033[1m" - underscore = "\033[4m" - end = "\033[0m" - -# Object types. -class types: - spawn_point = 81 - scroll = 111 - potion = 5 - monster = 80 - spawn_point_mob = 83 - random_drop = 102 - quest_container = 120 - ability = 110 - waypoint = 119 - player = 1 - exit = 66 - teleporter = 41 - floor = 71 - shop_floor = 68 - event_object = 118 - beacon = 126 - sign = 98 - creator = 42 - map_event_object = 127 - wall = 77 - magic_mirror = 28 - door = 20 - gate = 91 - book = 8 - magic_ear = 29 - light_source = 74 - bow = 14 - -# Configuration related to the application and some other defines. -class checker: - # Name of the application. - name = "Atrinik Map Checker" - # Version. - version = "1.0" - # Copyright. - copyright = "Copyright \xc2\xa9 2010-2012 Zoey Rose and Atrinik Development Team" - # GNU GPL license. - license = "This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA." - # Description of the application. - description = "Application to check Atrinik maps for common errors." - # Authors. - authors = ["Zoey Rose"] - # Website URL. - website = "http://www.atrinik.org/" - # Website URL for reporting bugs in the application. - website_bug = "http://bugzilla.atrinik.org/" - - # Highest layer value any archetype can have. - max_layers = 7 - # Number of sub-layers. - num_sub_layers = 5 - # Maximum level. - max_level = 115 - ## Known plugins. - plugins = ["Python", "Arena"] - -# Print usage. -def usage(): - print("\n" + colors.bold + colors.underscore + "Use:" + colors.end + colors.end) - print("\nGUI/CLI application to check Atrinik maps for common errors.\n") - print(colors.bold + colors.underscore + "Options:" + colors.end + colors.end) - print("\n\t-h, --help:\n\t\tDisplay this help.") - print("\n\t-c, --cli:\n\t\tCommand Line Interface mode. Default is to start in GUI.") - print("\n\t-d " + colors.underscore + "directory" + colors.end + ", --directory=" + colors.underscore + "directory" + colors.end + ":\n\t\tSpecify directory where to start checking map files (recursively). Default is '../../maps'.") - print("\n\t-a " + colors.underscore + "arch" + colors.end + ", --arch=" + colors.underscore + "arch" + colors.end + ":\n\t\tSpecify where 'arch' directory is located (to get artifacts, archetypes, etc from). Default is '../../arch'.") - print("\n\t-r " + colors.underscore + "file" + colors.end + ", --regions=" + colors.underscore + "file" + colors.end + ":\n\t\tSpecify where the 'regions.reg' file is located. Default is '../../maps/regions.reg.") - print("\n\t-m " + colors.underscore + "map" + colors.end + ", --map=" + colors.underscore + "map" + colors.end + ":\n\t\tSpecify the only map to check for errors.") - print("\n\t--non-rec:\n\t\tDo not go through directories recursively.") - -# Try to parse our command line options. -try: - opts, args = getopt.getopt(sys.argv[1:], "hcd:m:a:r:", ["help", "cli", "directory=", "map=", "arch=", "regions=", "non-rec", "text-only"]) -except getopt.GetoptError as err: - # Invalid option, show the error, print usage, and exit. - print(err) - usage() - sys.exit(2) - -# The default values. -path = "../../maps" -arch_dir = "../../arch" -regions_file = "../../maps/regions.reg" -one_map = None -cli = False -rec = True -text_only = False - -# Parse options. -for o, a in opts: - if o in ("-h", "--help"): - usage() - sys.exit() - elif o in ("-c", "--cli"): - cli = True - elif o in ("-d", "--directory"): - path = a - elif o in ("-m", "--map"): - one_map = a - elif o in ("-a", "--arch"): - arch_dir = a - elif o in ("-r", "--regions"): - regions_file = a - elif o == "--non-rec": - rec = False - elif o == "--text-only": - text_only = True - -if text_only: - colors.bold = "" - colors.underscore = "" - colors.end = "" - errors.colors = [""] * len(errors.colors) - -# Errors we found in maps/objects. -errors_l = [] -errors_l_last_map = None -# Errors from artifacts file. -errors_artifacts = [] -# Errors from archetypes file. -errors_archetypes = [] -# Errors from regions file. -errors_regions = [] -# Loaded archetypes. -archetypes = {} -# Artifacts. -artifacts = {} -# Regions. -regions = {} -# List of beacons. -beacons = [] - -# Default config. -default_cfg = StringIO(""" -[Suppress] -medium = off -high = off -warning = off -critical = off -low = off -[Errors] -map_no_music = off -map_no_region = on -decor_wall_l2 = off -decor_wall_l3 = off -decor_wall_l4 = off -sys_not_on_top = on -deprecated_control_chars = off -layer_changed = off -[Ignore] -ignore_events = on -""") - -config = ConfigParser() -config.readfp(default_cfg) -config.read(['config.cfg']) - -# Add error to errors_l. -# @param map Map file. -# @param msg Description of the error. -# @param severity Severity of the error, from 'errors'. -# @param x X position of object. -# @param y Y position of object. -def add_error(map, msg, severity, x = -1, y = -1): - global errors_l_last_map - - if errors_l_last_map != map or not errors_l: - errors_l_last_map = map - errors_l.append([map, []]) - - errors_l[len(errors_l) - 1][1].append((msg, severity, x, y)) - -# Load the map. -# @param fp File pointer. -# @return Dictionary of the map data, empty dictionary if the map -# is not valid. -def load_map(fp): - d = {} - in_map = False - in_msg = False - msg_buf = "" - - for line in fp: - if line == "arch map\n": - in_map = True - continue - elif not in_map: - return {} - elif line == "end\n": - # Store the map's file name. - d["file"] = fp.name - - # Strip off 'path' if possible. - if d["file"][:len(path)] == path: - d["file"] = d["file"][len(path) + 1:] - - # Load the objects on this map. - parser = ObjectParser(fp) - d["tiles"] = parser.map(d["file"]) - - return d - - # Start of message. - if line == "msg\n": - in_msg = True - # End of message. - elif line == "endmsg\n": - in_msg = False - # Add it to the dictionary, removing the last newline. - d["msg"] = msg_buf[:-1] - # Store it in a buffer. - elif in_msg: - msg_buf += line - # Map's attributes. - else: - space_pos = line.find(" ") - # Our value. - value = line[space_pos + 1:-1] - - if isint(value): - value = int(value) - - # Add it to the dictionary. - d[line[:space_pos]] = value - - return {} - -# Check map. -# @param map Map to check. -def check_map(map): - tiles = [] - - # Go through the attributes. - for attribute in map: - if attribute[:10] == "tile_path_": - # Map being tiled into itself is a critical error. - if map["file"][len(map["file"]) - len(map[attribute]):] == map[attribute]: - add_error(map["file"], "Map is tiled into itself (tile #{0}).".format(attribute[10:]), errors.critical) - continue - - # Map having two same tiles is also a critical error. - for tile in tiles: - if tile == map[attribute]: - add_error(map["file"], "Map is tiled to '{0}' more than once.".format(tile), errors.critical) - - tiles.append(map[attribute]) - - # No difficulty? Not really an error, but the server will drop a warning and change - # the difficulty to 1. - if not "difficulty" in map: - add_error(map["file"], "Map is missing difficulty.", errors.low) - map["difficulty"] = 1 - # This is an error, if the difficulty is set, and it's lower than 1 or higher than max level. - elif map["difficulty"] < 1 or map["difficulty"] > checker.max_level: - add_error(map["file"], "Map has invalid difficulty: {0}. Valid difficulties are 1-{1}.".format(map["difficulty"], checker.max_level), errors.medium) - - if "bg_music" in map: - if not re.match(r"([a-zA-Z0-9_\-]+)\.(\w+)[ 0-9\-]?", map["bg_music"]): - add_error(map["file"], "Map's background music attribute ('{0}') is not in a valid format. Valid format is (example): ocean.ogg".format(map["bg_music"]), errors.high) - - # Map missing 'width' or 'height' is a serious error. - if not "width" in map: - add_error(map["file"], "Map is missing width.", errors.high) - - if not "height" in map: - add_error(map["file"], "Map is missing height.", errors.high) - - # Do we have a region, but it's not a valid one? - if "region" in map and not map["region"] in regions: - add_error(map["file"], "Map's region '{0}' is not defined in regions.reg file.".format(map["region"]), errors.high) - - if not "msg" in map: - add_error(map["file"], "Map is missing message.", errors.low) - else: - if not re.match(r"^Created\:\s*\d{4}-\d{2}-\d{2} [^\n]*(\nModified\:\s*\d{4}-\d{2}-\d{2} [^\n]*)?$", map["msg"]): - add_error(map["file"], "Map's message is in incorrect format.", errors.low) - - # If there is no height or width, there's no point going on. - if not "height" or not "width": - return - - # Skip object checking for empty world maps. - if map["name"] == "World": - if "region" in map: - add_error(map["file"], "Empty world map has a region.", errors.warning) - else: - if config.getboolean("Errors", "map_no_region") and not "region" in map: - add_error(map["file"], "Map is missing region.", errors.medium) - - if config.getboolean("Errors", "map_no_music") and not "bg_music" in map: - add_error(map["file"], "Map is missing background music.", errors.low) - - # Go through all the spaces on the map. - for x in xrange(0, map["width"]): - for y in xrange(0, map["height"]): - if not x in map["tiles"] or not y in map["tiles"][x]: - continue - - # Our layers. - layers = [[0] * checker.num_sub_layers for i in range(checker.max_layers + 1)] - # Number of objects. Layer 0 objects are not counted. - obj_count = 0 - # Total number of objects, with layer 0 objects. - obj_count_all = 0 - is_shop = False - sys_below_floor = False - have_sys = False - sys_not_on_top = False - - # Go through the objects on this map space. - for obj in map["tiles"][x][y]: - # Get our layer and sub-layer. - layer = "layer" in obj and obj["layer"] or 0 - sub_layer = "sub_layer" in obj and obj["sub_layer"] or 0 - # Increase number of layers. - layers[layer][sub_layer] += 1 - - # Increase number of objects, if we're not on layer 0. - if layer != 0: - obj_count += 1 - - # The total count of objects. - obj_count_all += 1 - - # Now recursively check the object. - check_obj(obj, map) - - if "type" in obj: - if obj["type"] == types.shop_floor: - is_shop = True - - if layer == 0: - have_sys = True - elif have_sys: - if layer == 1: - sys_below_floor = True - else: - sys_not_on_top = True - - # No layer 1 objects and there are other non-layer-0 objects? Missing floor. - if sum(layers[1]) == 0 and obj_count > 0: - add_error(map["file"], "Missing layer 1 object on tile with some objects -- missing floor?", errors.medium, x, y) - - # Go through the layers (ignoring layer 0), and check if we have more than one - # object of the same layer on this space. - for i in xrange(1, checker.max_layers): - for j in xrange(0, checker.num_sub_layers): - if layers[i][j] > 1: - add_error(map["file"], "More than 1 object ({0}) with layer {1}, sub-layer {2} on same tile.".format(layers[i][j], i, j), errors.warning, x, y) - - if sum(layers[5]) and sum(layers[2]) and config.getboolean("Errors", "decor_wall_l2"): - add_error(map["file"], "Layer 5 object on tile with layer 2 object(s).", errors.warning, x, y) - - if sum(layers[5]) and sum(layers[3]) and config.getboolean("Errors", "decor_wall_l3"): - add_error(map["file"], "Layer 5 object on tile with layer 3 object(s).", errors.warning, x, y) - - if sum(layers[5]) and sum(layers[4]) and config.getboolean("Errors", "decor_wall_l4"): - add_error(map["file"], "Layer 5 object on tile with layer 4 object(s).", errors.warning, x, y) - - if sys_below_floor: - add_error(map["file"], "System object is below floor.", errors.low, x, y) - - if sys_not_on_top and config.getboolean("Errors", "sys_not_on_top"): - add_error(map["file"], "System object is not on top.", errors.low, x, y) - - # Recheck all objects on this square if this is a shop... - if is_shop: - for obj in map["tiles"][x][y]: - if ("sys_object" in obj and obj["sys_object"] == 1) or ("no_pick" in obj and obj["no_pick"] == 1): - continue - - if not "unpaid" in obj or obj["unpaid"] == 0: - add_error(map["file"], "Object '{0}' is on a shop tile but is not unpaid.".format(obj["archname"]), errors.high, x, y) - -## Check for errors in object message. -## @param msg The message to check. -## @return List of the errors. -def check_obj_msg(msg): - errors = [] - has_hello = False - - test_msg = re.sub(r"<(\/?[a-z_]+)([^>]*)>", r"\1\2", msg) - - if test_msg.find("[") != -1 or test_msg.find("]") != -1: - errors.append("unescaped-markup") - - for line in msg.split("\n"): - if line.startswith("@match "): - line = line[7:] - - if line.find("^hello$") != -1 and line != "^hello$": - errors.append("invalid-hello") - - if line == "^hello$": - has_hello = True - - parts = line.split("|") - - for part in parts: - if part == "*": - continue - - if part[:1] != "^" or part[-1:] != "$": - errors.append("suspicious-regex") - else: - if line.find(" checker.max_level: - add_error(map["file"], "Monster '{0}' has invalid level ({1}).".format(obj["archname"], obj["level"]), errors.high, env["x"], env["y"]) - elif not is_friendly(obj) and obj["level"] >= 10 and map["difficulty"] == 1: - add_error(map["file"], "Monster '{0}' is level {1} but map's difficulty is 1.".format(obj["archname"], obj["level"]), errors.medium, env["x"], env["y"]) - - if not "race" in obj: - add_error(map["file"], "Monster '{0}' is missing a race.".format(obj["archname"]), errors.medium, env["x"], env["y"]) - elif obj["race"] == "undead": - if not "undead" in obj or obj["undead"] != 1: - add_error(map["file"], "Monster '{0}' is of race 'undead', but has no 'undead 1' flag set.".format(obj["archname"]), errors.medium, env["x"], env["y"]) - - if get_entry(obj, "friendly") == 1 and obj["name"] not in ("guard", "knight"): - if obj["name"] == archetype["name"] and get_entry(map, "region") != "creation": - has_say_event = False - has_generic_guard_script = False - - for tmp in obj["inv"]: - if get_entry(tmp, "type") == types.event_object and get_entry(tmp, "sub_type") == 6: - if get_entry(tmp, "race") == "/python/generic/guard.py": - has_generic_guard_script = True - - has_say_event = True - break - - if not has_generic_guard_script: - if "msg" in obj or has_say_event: - add_error(map["file"], "NPC '{0}' has no custom name, but has a dialog.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - elif obj["name"][:1].istitle() and not re.match(r"^([A-Z][a-z\']*)( [A-Z][a-z\']*)?( (XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))?$", obj["name"]): - add_error(map["file"], "NPC '{0}' has name in incorrect format.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if obj["type"] == types.spawn_point_mob: - if not "env" in obj or obj["env"]["type"] != types.spawn_point: - add_error(map["file"], "Monster '{0}' is a spawn point monster but is not inside a spawn point.".format(obj["archname"]), errors.critical, env["x"], env["y"]) - - if obj["type"] == types.monster: - add_error(map["file"], "Monster '{0}' is outside spawn point.".format(obj["archname"]), errors.medium, env["x"], env["y"]) - - if obj["type"] == types.random_drop or obj["type"] == types.quest_container: - if not "env" in obj: - add_error(map["file"], "Object '{0}' outside of inventory.".format(obj["archname"]), errors.high, env["x"], env["y"]) - - if obj["type"] == types.quest_container: - if not "name" in obj: - add_error(map["file"], "Quest container '{0}' has no quest name.".format(obj["archname"]), errors.high, env["x"], env["y"]) - - abilities = [] - wps = [] - events = [] - - for tmp in obj["inv"]: - if not "type" in tmp: - continue - - if tmp["type"] == types.ability: - abilities.append(tmp) - elif tmp["type"] == types.waypoint: - wps.append(tmp) - elif tmp["type"] == types.event_object: - events.append(tmp) - - for event in events: - num = 0 - - for event2 in events: - if get_entry(event, "sub_type") == get_entry(event2, "sub_type"): - num += 1 - - if num > 1: - add_error(map["file"], "NPC '{0}' has events with two or more events with the same event type.".format(obj["archname"]), errors.low, env["x"], env["y"]) - break - - if "can_cast_spell" in obj and obj["can_cast_spell"] == 1: - if not abilities: - add_error(map["file"], "Monster '{0}' can cast spells but has no ability objects.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if not "maxsp" in obj or obj["maxsp"] == 0: - add_error(map["file"], "Monster '{0}' can cast spells but has 0 mana.".format(obj["archname"]), errors.medium, env["x"], env["y"]) - - if not "Dex" in obj or obj["Dex"] == 0: - add_error(map["file"], "Monster '{0}' can cast spells but has unset ability usage.".format(obj["archname"]), errors.medium, env["x"], env["y"]) - else: - if abilities: - add_error(map["file"], "Monster '{0}' cannot cast spells but has ability objects.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - # Waypoints movement. - if "movement_type" in obj and obj["movement_type"] == 176: - if not wps: - add_error(map["file"], "Monster '{0}' has waypoint movement enabled but no waypoints.".format(obj["archname"]), errors.medium, env["x"], env["y"]) - else: - for wp in wps: - if not "name" in wp: - add_error(map["file"], "Monster '{0}' has waypoint with no name.".format(obj["archname"]), errors.high, env["x"], env["y"]) - - if "title" in wp: - found_one = False - - for wp_next in wps: - if "name" in wp_next and wp_next["name"] == wp["title"]: - found_one = True - break - - if not found_one: - add_error(map["file"], "Monster '{0}' has waypoint ('{1}') with nonexistent next waypoint.".format(obj["archname"], "name" in wp and wp["name"] or ""), errors.high, env["x"], env["y"]) - else: - if wps: - add_error(map["file"], "Monster '{0}' has waypoint movement disabled but has waypoints in inventory.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if obj["type"] == types.sign: - # Is it a magic mouth? - if get_entry(obj, "walk_on") == 1 or get_entry(obj, "fly_on") == 1: - if get_entry(obj, "splitting") == 1 and get_entry(obj, "direction") == None: - add_error(map["file"], "Magic mouth '{0}' has adjacent direction set but actual facing direction is not set.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if get_entry(obj, "direction") not in (None, 0) and get_entry(obj, "is_turnable") != 1 and get_entry(obj, "is_animated") != 1 and get_entry(obj, "draw_direction") != 1: - add_error(map["file"], "Object '{0}' has direction but that type of object doesn't support directions.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if obj["type"] in (types.event_object, types.map_event_object): - if not "name" in obj: - add_error(map["file"], "Event object '{0}' is missing plugin name.".format(obj["archname"]), errors.high, env["x"], env["y"]) - elif not obj["name"] in checker.plugins: - add_error(map["file"], "Event object '{0}' has unknown plugin '{1}'.".format(obj["archname"], obj["name"]), errors.critical, env["x"], env["y"]) - - if "race" in obj: - if obj["race"].startswith("..") and obj["race"].find("/python") != -1: - add_error(map["file"], "Event object '{0}' is using a relative path to point to the global /python directory.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if obj["race"].startswith("/") and not os.path.isfile(os.path.join(os.path.dirname(__file__), path) + obj["race"]): - add_error(map["file"], "Event object '{0}' has a path that doesn't exist.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if obj["type"] == types.beacon: - if not "name" in obj: - add_error(map["file"], "Beacon '{0}' is missing name.".format(obj["archname"]), errors.critical, env["x"], env["y"]) - elif obj["name"] in beacons: - add_error(map["file"], "Beacon '{0}' with the name '{0}' already exists.".format(obj["archname"], obj["name"]), errors.critical, env["x"], env["y"]) - else: - beacons.append(obj["name"]) - - if get_entry(obj, "random_movement") == 1: - if not get_entry(obj, "item_race") or not get_entry(obj, "item_level"): - add_error(map["file"], "Monster '{0}' has random movement enabled but no max movement range X/Y.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if get_entry(obj, "is_turnable") == 1 and get_entry(obj, "draw_direction") == 1: - if get_entry(obj, "direction") in (5, 6, 4, 3, 2, 8): - add_error(map["file"], "Object {0} has wrong direction {1}; must be facing either west or north.".format(obj["archname"], get_entry(obj, "direction")), errors.low, env["x"], env["y"]) - - if obj["type"] in (types.door, types.gate, types.wall): - if get_entry(obj, "damned") == 1: - add_error(map["file"], "Object {0} has 'damned 1' flag set, but this is not supported.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if get_entry(obj, "no_magic") == 1: - add_error(map["file"], "Object {0} has 'no_magic 1' flag set, which may be an error, as this flag is usually set on floor objects.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if obj["type"] in (types.spawn_point_mob, types.magic_ear, types.book, types.sign): - msg = get_entry(obj, "msg") - - if msg: - is_mob_dialogue = obj["type"] == types.spawn_point_mob - is_ear_dialogue = obj["type"] == types.magic_ear - is_dialogue = is_mob_dialogue or is_ear_dialogue - - msg_errors = check_obj_msg(msg) - - if is_mob_dialogue: - if "missing-hello" in msg_errors: - add_error(map["file"], "Object {0} has a @match dialogue that is missing '@match ^hello$'.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if is_dialogue: - if "invalid-hello" in msg_errors: - add_error(map["file"], "Object {0} has a @match dialogue that has invalid '@match ^hello$'.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if "suspicious-regex" in msg_errors: - add_error(map["file"], "Object {0} has a @match that doesn't use regex.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if "link-in-msg" in msg_errors: - add_error(map["file"], "Object {0} has a @match which uses links of some sort - this is not recommended.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if "control-chars" in msg_errors and config.getboolean("Errors", "deprecated_control_chars"): - add_error(map["file"], "Object {0} contains deprecated control characters in message.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if "unescaped-markup" in msg_errors: - add_error(map["file"], "Object {0} contains unescaped markup in message.".format(obj["archname"]), errors.low, env["x"], env["y"]) - - if config.getboolean("Errors", "layer_changed") and "layer" in obj and obj["layer"] != 0 and "layer" in archetype and archetype["layer"] != 0 and obj["layer"] != archetype["layer"]: - add_error(map["file"], "Object {0} has had layer changed to {1} from the default value of {2} - this is not recommended.".format(obj["archname"], obj["layer"], archetype["layer"]), errors.warning, env["x"], env["y"]) - - if "carrying" in obj: - add_error(map["file"], "Object {0} has carrying attribute set.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if "animation" in obj and obj["animation"] == "NONE": - add_error(map["file"], "Object {0} has animation attribute set to NONE.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if get_entry(obj, "face") != get_entry(archetype, "face"): - if get_entry(obj, "is_turnable") == 1 or get_entry(obj, "is_animated") == 1: - add_error(map["file"], "Object {0} is animated/turnable but has had face changed.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - - if obj["type"] == types.light_source: - add_error(map["file"], "Object {0} is a light source but has had face changed.".format(obj["archname"]), errors.warning, env["x"], env["y"]) - -def check_archetype(arch, errors_l): - if not "type" in arch: - return - - if get_entry(arch, "material") and not (get_entry(arch, "material_real") or get_entry(arch, "item_quality")) and not get_entry(arch, "no_pick"): - errors_l.append(["Archetype '{0}' has material set but no material_real or item_quality.".format(arch["archname"]), errors.low]) - - # Is the archetype (shop) floor with 'is_floor 1' not set? - if arch["type"] in (types.floor, types.shop_floor) and get_entry(arch, "is_floor") != 1: - errors_l.append(["Archetype '{0}' is of type floor but doesn't have 'is_floor 1' set.".format(arch["archname"]), errors.low]) - - if arch["type"] == types.magic_mirror and get_entry(arch, "sys_object") != 1: - errors_l.append(["Archetype '{0}' is a magic mirror but is not 'sys_object 1'.".format(arch["archname"]), errors.medium]) - -# Load map. If successfully loaded, we will check the map header -# and its objects with check_map(). -# @param file Map to load. -def check_file(file): - fp = open(file, "r") - map = load_map(fp) - fp.close() - - if map: - check_map(map) - -# Recursively scan directories and call check_file on found files. -# @param dir Directory to scan. -def scan_dirs(dir): - files = os.listdir(dir) - - for file in files: - if os.path.isdir(dir + "/" + file): - # Skip obvious non-map directories. It's still possible to scan those - # if you pass --directory option however. - if file in ("styles", "python") or not rec: - continue - - if file == "events" and config.getboolean("Ignore", "ignore_events"): - continue - - scan_dirs(dir + "/" + file) - else: - check_file(dir + "/" + file) - -# Do the scan. If 'one_map' argument was specified, we will use that, -# otherwise we will recursively scan 'path'. -def do_scan(): - if one_map: - check_file(one_map) - else: - scan_dirs(path) - -# Try to find an archetype by its arch name. This will also consider artifacts -# as archetypes. -# @param archname Arch name to find. -# @return The arch (or artifact) if found, None otherwise. -def get_archetype(archname): - if archname in archetypes: - return archetypes[archname] - elif archname in artifacts: - return artifacts[archname] - - return None - -# Recursively look through object's environment value. -# @param obj Object. -# @return Object on map that has this object somewhere in its inventory. -def get_env(obj): - ret = obj - - while "env" in ret: - ret = ret["env"] - - return ret - -# Check if monster object is friendly or not. -# @param obj Object. -# @return True if it's friendly, False otherwise. -def is_friendly(obj): - if get_entry(obj, "friendly") == 1: - return True - - return False - -# Get entry identified by 's' from dictionary 'd'. -# @param d The dictionary. -# @param s What to get. -# @return The entry from the dictionary, None if there is no such entry. -def get_entry(d, s): - try: - return d[s] - except KeyError: - return None - -# Check whether the passed string is an integer. -# @param s String. -# @return True if s is an integer, False otherwise. -def isint(s): - try: - int(s) - return True - except ValueError: - return False - -# Object parser. -class ObjectParser: - # Initializer. - # @param fp File pointer to read data from. - def __init__(self, fp): - self.fp = fp - self.dict = {} - self.last_obj = None - self.in_msg = False - self.msg_buf = "" - self.line_num = 0 - - # Archetypes parser. - # @return Archetypes. - def archetypes(self): - for line in self.fp: - self.line_num += 1 - - if line[:7] == "Object ": - self.last_obj = line[7:-1] - self.dict[self.last_obj] = { - "archname": self.last_obj, - } - continue - elif not self.last_obj: - continue - elif line == "end\n": - self.last_obj = None - continue - - # Parse attributes. - self.parse(line, self.dict[self.last_obj]) - - return self.dict - - # Artifacts parser. - # @return Artifacts. - def artifacts(self): - for line in self.fp: - self.line_num += 1 - - if line[:9] == "artifact ": - self.last_obj = line[9:-1] - self.dict[self.last_obj] = {} - continue - elif not self.last_obj: - continue - elif line[:9] == "def_arch ": - archetype = line[9:-1] - found = get_archetype(archetype) - - if not found and archetype in self.dict: - found = dict(self.dict[archetype]) - - if not found: - errors_artifacts.append(["Artifact '{0}': Could not find archetype '{1}' for def_arch command (line: {2}).".format(self.last_obj, archetype, self.line_num), errors.critical]) - else: - self.dict[self.last_obj] = dict(found) - self.dict[self.last_obj]["archname"] = self.last_obj - - continue - elif line == "end\n": - self.last_obj = None - continue - - # Parse attributes. - self.parse(line, self.dict[self.last_obj]) - - return self.dict - - # Map objects parser. - # @param map_file Map file name we're loading objects for. - # @return List of map tiles containing the found objects. - def map(self, map_file): - # Map tiles. - self.tiles = {} - self.map_file = map_file - - for line in self.fp: - if line.startswith("arch "): - arch = self.map_parse_rec(line[5:-1]) - self.map_add_tile(arch) - - return self.tiles - - # Add object to map's tiles. - # @param arch Object to add. - def map_add_tile(self, arch): - x = arch["x"] - y = arch["y"] - - if not x in self.tiles: - self.tiles[x] = {} - - if not y in self.tiles[x]: - self.tiles[x][y] = [] - - self.tiles[x][y].append(arch) - - # Recursively parse objects on map. - # @param archname Arch name we previously found. - # @return Archetype, complete with its inventory. - def map_parse_rec(self, archname, env = None): - # Find the archetype first. - def_archetype = get_archetype(archname) - archetype = dict(def_archetype) if def_archetype else {} - - # Store its name. - archetype["archname"] = archname - # Inventory. - archetype["inv"] = [] - - if env: - archetype["env"] = env - else: - archetype["x"] = 0 - archetype["y"] = 0 - - for line in self.fp: - # Another arch? That means it's inside the previous one. - if line.startswith("arch "): - # Add it to the object's inventory. - archetype["inv"].append(self.map_parse_rec(line[5:-1], archetype)) - elif line == "end\n": - break - # Parse attributes. - else: - parsed = self.parse(line, archetype) - - if parsed and def_archetype: - (attr, value) = parsed - - if not attr in ("x", "y", "identified", "unpaid", "no_pick", "level", "nrof", "value", "can_stack", "layer", "sub_layer", "z", "zoom", "zoom_x", "zoom_y", "alpha", "align"): - archetype["modified_artifact"] = True - - def_value = get_entry(def_archetype, attr) - - if def_value == value or (value in (0, 0.0) and def_value == None): - archetype["same_attributes"] = True - - if not def_archetype: - env = get_env(archetype) - add_error(self.map_file, "Invalid archetype '{0}' found.".format(archetype["archname"]), errors.critical, "x" in env and env["x"] or 0, "y" in env and env["y"] or 0) - - return archetype - - # Parse attributes from a line. - # @param line Line to parse from. - # @param dict Dictionary add parsed attributes to. - def parse(self, line, d): - # Message start? - if line == "msg\n": - self.in_msg = True - # End of message. - elif line == "endmsg\n": - self.in_msg = False - msg = self.msg_buf[:-1] - # Add it to the dict without the last newline. - d["msg"] = msg - self.msg_buf = "" - return ("msg", msg) - # We are in a message, store it in a buffer. - elif self.in_msg: - self.msg_buf += line - # Not a message, so attribute/value combo. - else: - # Find space. - space_pos = line.find(" ") - # Our value. - value = line[space_pos + 1:-1] - - try: - integer = int(value) - value = integer - except: - pass - - attr = line[:space_pos] - # Add it to the dictionary. - d[attr] = value - return (attr, value) - - return None - -# Parse the archetypes. -# @return Dictionary of the archetypes. -def parse_archetypes(): - fp = open(arch_dir + "/archetypes") - parser = ObjectParser(fp) - d = parser.archetypes() - fp.close() - - # Post-processing. - for arch in d: - if not "name" in d[arch]: - d[arch]["name"] = arch - - for arch in d: - check_archetype(d[arch], errors_archetypes) - - return d - -# Parse the artifacts. -# @return Dictionary of the artifacts. -def parse_artifacts(): - fp = open(arch_dir + "/artifacts") - parser = ObjectParser(fp) - d = parser.artifacts() - fp.close() - - for arch in d: - check_archetype(d[arch], errors_artifacts) - - return d - -# Parse the regions. -# @return Dictionary of the regions. -def parse_regions(): - fp = open(regions_file) - d = {} - region = None - in_msg = False - msg_buf = "" - - for line in fp: - if line[:7] == "region ": - region = line[7:-1] - d[region] = {} - continue - elif not region: - continue - elif line == "end\n": - region = None - continue - - # Start of message. - if line == "msg\n": - in_msg = True - # End of message. - elif line == "endmsg\n": - in_msg = False - # Add it to the dictionary, removing the last newline. - d[region]["msg"] = msg_buf[:-1] - # Store it in a buffer. - elif in_msg: - msg_buf += line - # Region's attributes. - else: - space_pos = line.find(" ") - # Our value. - value = line[space_pos + 1:-1] - - if isint(value): - value = int(value) - - # Add it to the dictionary. - d[region][line[:space_pos]] = value - - fp.close() - return d - -def config_save(): - with open("config.cfg", "wb") as configfile: - config.write(configfile) - -# Find files in the specified path. -# @param where Where to look for the files. -# @param ext What the file must end with. -# @param rec Whether to go on recursively. -# @param ignore_dirs Whether to ignore directories. -# @param ignore_files Whether to ignore files. -# @param ignore_paths What paths to ignore. -# @return A list containing files/directories found based on the set criteria. -def find_files(where, ext = None, rec = True, ignore_dirs = True, ignore_files = False, ignore_paths = None): - nodes = os.listdir(where) - files = [] - - for node in nodes: - path = os.path.join(where, node) - - # Do we want to ignore this path? - if ignore_paths and path in ignore_paths: - continue - - # A directory. - if os.path.isdir(path): - # Do we want to go on recursively? - if rec: - files += find_files(path, ext) - - # Are we ignoring directories? If not, add it to the list. - if not ignore_dirs: - files.append(path) - else: - # Only add the file if we're not ignoring files and ext was not set or it matches. - if not ignore_files and (not ext or path.endswith(ext)): - files.append(path) - - return files - -# Find .arc file of archetype definition. -# @param arch Archetype to find. -# @return Path to the .arc file, None if the archetype could not be found. -def find_archetype_file(arch): - for file in find_files(arch_dir, ".arc"): - fp = open(file, "r") - - for line in fp: - if line == "Object " + arch + "\n": - fp.close() - return file - - fp.close() - - return None - -# Find .art file of artifact definition. -# @param art Artifact to find. -# @return Path to the .art file, None if the artifact could not be found. -def find_artifact_file(art): - for file in find_files(arch_dir, ".art"): - fp = open(file, "r") - - for line in fp: - if line == "artifact " + art + "\n": - fp.close() - return file - - fp.close() - - return None - -archetypes = parse_archetypes() -artifacts = parse_artifacts() -regions = parse_regions() - -# GUI. -if not cli: - try: - import pygtk - pygtk.require("2.0") - except ImportError: - print("PyGTK not found. Please make sure it is installed properly and referenced in your PYTHONPATH environment variable.") - - import gtk, re, webbrowser - from datetime import datetime - - class pref_types: - checkbox = 1 - - # Preferences dialog. - class PreferencesDialog: - # Contents of the dialog window. - tabs = [ - ["Errors", "Allow you to turn on/off specific types of error messages.", [ - [pref_types.checkbox, "Map with no region", ("Errors", "map_no_region")], - [pref_types.checkbox, "Map with no music", ("Errors", "map_no_music")], - [pref_types.checkbox, "Layer 2 object on square with a wall", ("Errors", "decor_wall_l2")], - [pref_types.checkbox, "Layer 3 object on square with a wall", ("Errors", "decor_wall_l3")], - [pref_types.checkbox, "Layer 4 object on square with a wall", ("Errors", "decor_wall_l4")], - [pref_types.checkbox, "System object not on top of normal objects", ("Errors", "sys_not_on_top")], - [pref_types.checkbox, "Check for deprecated control characters", ("Errors", "deprecated_control_chars")], - [pref_types.checkbox, "Layer changed from the default value", ("Errors", "layer_changed")], - ], "\nNote: You need to do a new scan to see the results."], - ["Suppress", "These allow you to suppress an entire category of error messages.", [ - [pref_types.checkbox, "Warning", ("Suppress", "warning")], - [pref_types.checkbox, "Low", ("Suppress", "low")], - [pref_types.checkbox, "Medium", ("Suppress", "medium")], - [pref_types.checkbox, "High", ("Suppress", "high")], - [pref_types.checkbox, "Critical", ("Suppress", "critical")], - ], None], - ["Ignore", "Allows you to ignore certain maps from being checked in global-scan.", [ - [pref_types.checkbox, "Ignore event maps", ("Ignore", "ignore_events")], - ], None], - ] - - # Callback for applying settings in the dialog. - # @param widget Widget. - # @param data Our data. Includes things like the setting type, config section, - # etc. - def callback(self, widget, data = None): - (pref_type, (config_section, config_name)) = data - - if pref_type == pref_types.checkbox: - config.set(config_section, config_name, ("off", "on")[widget.get_active()]) - - self.main.draw_errors() - - # Initializer. - # @param main Class we're coming from. - def __init__(self, main): - # Make a new window. - self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) - # Set the window's parent. - self.window.set_transient_for(main.window) - # Center it on parent. - self.window.set_position(gtk.WIN_POS_CENTER_ON_PARENT) - # Set title. - self.window.set_title("Properties") - # Set default size. - self.window.resize(400, 20) - self.main = main - - # Create vertical box. - self.window.vbox = gtk.VBox() - self.window.add(self.window.vbox) - - # Create a new table, and add it to the box. - table = gtk.Table(2, 2, False) - self.window.vbox.add(table) - - # Create a new GTK Notebook and add it to the table. - notebook = gtk.Notebook() - notebook.set_tab_pos(gtk.POS_LEFT) - table.attach(notebook, 0, 1, 0, 1, xpadding = 10, ypadding = 5) - - # Now we create the contents. - for (tab_name, desc, prefs, note) in self.tabs: - # Set up alignment. - vbox_alignment = gtk.Alignment() - vbox_alignment.set_padding(5, 5, 10, 10) - # Create a vertical box, and add it to the alignment. - vbox = gtk.VBox(False, 2) - vbox_alignment.add(vbox) - - # Create another alignment, set up label, add description contents, - # and add it to the box. - alignment = gtk.Alignment() - label = gtk.Label() - label.set_markup(desc) - vbox.pack_start(alignment, False, True, 1) - alignment.add(label) - - # Now we add the actual preferences. - for (pref_type, pref_name, pref_config) in prefs: - # Alignment. - alignment = gtk.Alignment() - alignment.set_padding(0, 0, 10, 5) - (config_section, config_name) = pref_config - - # A checkbox? - if pref_type == pref_types.checkbox: - widget = gtk.CheckButton(pref_name) - widget.set_active(config.getboolean(config_section, config_name)) - widget.connect("toggled", self.callback, (pref_type, pref_config)) - - vbox.pack_start(alignment, False, True, 1) - alignment.add(widget) - - # If we have a note about the particular tab, add it like - # description above. - if note: - alignment = gtk.Alignment() - label = gtk.Label() - label.set_markup(note) - vbox.pack_start(alignment, False, True, 1) - alignment.add(label) - - # Create a label for the tab name, and actually append it to the - # notebook. - label = gtk.Label(tab_name) - notebook.append_page(vbox_alignment, label) - - # Create alignment, attach it to the table, and create a - # close button. - alignment = gtk.Alignment(1) - table.attach(alignment, 0, 1, 1, 2, xpadding = 10, ypadding = 5) - button = gtk.Button("Close", gtk.STOCK_CLOSE) - button.connect("clicked", self.quit_event) - alignment.add(button) - - self.window.show_all() - - # Quit event. Destroy the preferences window. - def quit_event(self, widget, event = None, data = None): - self.window.destroy() - return False - - # The GUI class. - class GUI: - # Our UI, with menu and toolbar. - ui = ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -''' - - # Initializer. - def __init__(self): - # Create a liststore. - self.liststore = gtk.ListStore(str, str, str) - - # The window. - self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) - # Set the title. - self.window.set_title(checker.name) - # 800x600 resolution. - self.window.set_size_request(800, 600) - self.window.connect("delete_event", self.quit_event) - - icon = self.find_icon() - - if icon: - self.window.set_icon(icon) - - # New box. - self.window.vbox = gtk.VBox() - self.window.add(self.window.vbox) - - # Create UIManager instance. - uimanager = gtk.UIManager() - - # Add the accelerator group to the toplevel window. - accelgroup = uimanager.get_accel_group() - self.window.add_accel_group(accelgroup) - - # Create an ActionGroup. - self.actiongroup = gtk.ActionGroup("UIManager") - - # Create actions. - self.actiongroup.add_actions([ - ("File", None, "_File"), - ("Scan", gtk.STOCK_EXECUTE, "_Scan", "s", "Scan maps", self.scan_button), - ("Save", gtk.STOCK_SAVE, "_Save", "s", "Save", self.save_button), - ("Open Maps", gtk.STOCK_OPEN, "_Open Maps", "o", "Open maps", self.open_maps_button), - ("Check File", gtk.STOCK_OPEN, "_Check File", "f", "Check File", self.check_file_button), - ("Check Directory", gtk.STOCK_DIRECTORY, "_Check Directory", "d", "Check Directory", self.check_directory_button), - ("Preferences", gtk.STOCK_PREFERENCES, "_Preferences", "p", "Preferences", self.preferences_button), - ("Quit", gtk.STOCK_QUIT, "_Quit", "q", "Quit the program", self.quit_button), - ("Reload", None, "_Reload"), - ("Reload Archetypes", None, "_Reload Archetypes", None, "Reload archetypes from file", self.reload_archetypes_button), - ("Reload Artifacts", None, "_Reload Artifacts", None, "Reload artifacts from file", self.reload_artifacts_button), - ("Reload Regions", None, "_Reload Regions", None, "Reload regions from file", self.reload_regions_button), - ("Help", None, "_Help"), - ("Report a Problem", None, "_Report a Problem", None, "Report a Problem", self.report_button), - ("About", gtk.STOCK_ABOUT, "_About", None, "About this application", self.about_button), - ]) - - # Add the actiongroup to the UIManager. - uimanager.insert_action_group(self.actiongroup, 0) - - # Add UI description. - uimanager.add_ui_from_string(self.ui) - - # Create a MenuBar. - menubar = uimanager.get_widget("/MenuBar") - self.window.vbox.pack_start(menubar, False) - - # Create a Toolbar. - toolbar = uimanager.get_widget("/Toolbar") - self.window.vbox.pack_start(toolbar, False) - - # Create scrolled window. - self.window.sw = gtk.ScrolledWindow() - # So the scrollbars only appear if they're needed. - self.window.sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC) - self.window.sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC) - - # Tree model. - self.window.sm = gtk.TreeModelSort(self.liststore) - # Set sort column. - self.window.sm.set_sort_column_id(0, gtk.SORT_ASCENDING) - # Tree view. - self.window.tv = gtk.TreeView(self.window.sm) - self.window.tv.connect("row-activated", self.row_click_event) - self.window.vbox.pack_start(self.window.sw) - - self.window.sw.add(self.window.tv) - - # Our columns. - columns = [ - ["Map", False], - ["Severity", True], - ["Description", False], - ] - self.window.tv.column = [None] * 3 - self.window.tv.cell = [None] * 3 - - for i in range(3): - self.window.tv.cell[i] = gtk.CellRendererText() - - # See if we want Pango markup or not. - if columns[i][1]: - self.window.tv.column[i] = gtk.TreeViewColumn(columns[i][0], self.window.tv.cell[i], markup = 1) - else: - self.window.tv.column[i] = gtk.TreeViewColumn(columns[i][0]) - - self.window.tv.append_column(self.window.tv.column[i]) - self.window.tv.column[i].set_sort_column_id(i) - - # Pango markup doesn't need this. - if not columns[i][1]: - self.window.tv.column[i].pack_start(self.window.tv.cell[i], True) - self.window.tv.column[i].set_attributes(self.window.tv.cell[i], text = i) - - # Draw any errors (there can be some from things like artifacts, archetypes, etc) - self.draw_errors() - - # Now show it all. - self.window.show_all() - - # Event that happens when we quit the application (X at top right, ctrl + q, etc). - def quit_event(self, widget, event, data = None): - gtk.main_quit() - return False - - # Event activated when row is clicked. - def row_click_event(self, treeview, i, view_column): - map_name = self.window.sm.get_model()[i][0] - msg = self.window.sm.get_model()[i][2] - - if map_name == "Archetypes": - arch = re.sub(r"[Aa]rchetype '([^']*)'(.*)", r"\1", msg) - file = find_archetype_file(arch) - - if file: - webbrowser.open(file) - elif map_name == "Regions": - webbrowser.open(regions_file) - elif map_name == "Artifacts": - arch = re.sub(r"[Aa]rtifact '([^']*)'(.*)", r"\1", msg) - file = find_artifact_file(arch) - - if file: - webbrowser.open(file) - - # Preferences dialog. - def preferences_button(self, b): - PreferencesDialog(self) - - # We pressed the quit button, so quit. - def quit_button(self, b): - gtk.main_quit() - - # The report button. Take us to the Atrinik Bugzilla. - def report_button(self, b): - webbrowser.open(checker.website_bug) - - # About button. - def about_button(self, b): - about = gtk.AboutDialog() - about.set_transient_for(self.window) - about.set_name(checker.name) - about.set_version(checker.version) - about.set_copyright(checker.copyright) - about.set_license(checker.license) - about.set_wrap_license(True) - about.set_comments(checker.description) - about.set_authors(checker.authors) - about.set_website(checker.website) - icon = self.find_icon() - - if icon: - about.set_logo(icon) - - about.connect("response", lambda d, r: d.destroy()) - about.show() - - # Open maps button. - def open_maps_button(self, b): - maps = [] - - for row in self.window.sm.get_model(): - # Ignore non-map errors. - if row[0] == "Artifacts" or row[0] == "Archetypes" or row[0] == "Regions": - continue - - m_path = os.path.realpath(os.path.join(path, row[0])) - - if not m_path in maps: - maps.append(m_path) - - # Get default environment variables. - envs = dict(os.environ) - delimiter = ";" if sys.platform.startswith("win") else ":" - # Extend the PATH environment variable with the script's dir. - envs["PATH"] += delimiter + os.path.abspath(os.path.dirname(__file__)) - # Execute Gridarta. - subprocess.Popen(["java", "-jar", os.path.realpath(arch_dir + "/../editor/AtrinikEditor.jar")] + maps, env = envs, cwd = os.path.realpath(arch_dir + "/../editor")) - - # The save button. Will save output in the tree view to file. - def save_button(self, b): - l = self.window.sm.get_model() - - # Nothing to save, display an error. - if len(l) == 0: - dialog = gtk.MessageDialog(self.window, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, "There are no errors to save!") - dialog.show_all() - dialog.run() - dialog.destroy() - return - - # Create the file chooser dialog. - fc = gtk.FileChooserDialog("Save As...", None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) - # Set current directory. - fc.set_current_folder(path) - # Make up a file name that should be relatively unique. - fc.set_current_name("atrinik_map_checker_" + datetime.now().strftime("%Y%m%d_%H-%M-%S") + ".txt") - fc.set_default_response(gtk.RESPONSE_OK) - response = fc.run() - - if response == gtk.RESPONSE_OK: - # Now open the file name user chose for writing. - fp = open(fc.get_filename(), "w") - - # Write the output to file. Note that we need to strip out Pango markup from 'severity'. - for (map, severity, description) in l: - fp.write("{0}: {1}: {2}\n".format(map, re.sub(r"<[^>]*?>", "", severity), description)) - - fp.close() - - fc.destroy() - - # Action for the scan button. - def scan_button(self, b): - # Clear out old errors. - del errors_l[:] - del beacons[:] - # Re-scan. - do_scan() - # Draw the errors. - self.draw_errors() - - # Check a directory of maps. - def check_directory_button(self, b): - # Create the file chooser dialog. - fc = gtk.FileChooserDialog("Select Directory...", None, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) - # Set current directory. - fc.set_current_folder(path) - fc.set_default_response(gtk.RESPONSE_OK) - response = fc.run() - - if response == gtk.RESPONSE_OK: - # Clear out old errors. - del errors_l[:] - del beacons[:] - # Scan the directory. - scan_dirs(fc.get_filename()) - # Draw the errors. - self.draw_errors() - - fc.destroy() - - # Check a single map. - def check_file_button(self, b): - # Create the file chooser dialog. - fc = gtk.FileChooserDialog("Select File...", None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) - # Set current directory. - fc.set_current_folder(path) - fc.set_default_response(gtk.RESPONSE_OK) - response = fc.run() - - if response == gtk.RESPONSE_OK: - # Clear out old errors. - del errors_l[:] - del beacons[:] - # Check the map. - check_file(fc.get_filename()) - # Draw errors. - self.draw_errors() - - fc.destroy() - - # Reload archetypes. - def reload_archetypes_button(self, b): - del errors_archetypes[:] - archetypes = parse_archetypes() - self.draw_errors() - - # Reload artifacts. - def reload_artifacts_button(self, b): - del errors_artifacts[:] - artifacts = parse_artifacts() - self.draw_errors() - - # Reload regions. - def reload_regions_button(self, b): - del errors_regions[:] - regions = parse_regions() - self.draw_errors() - - # Common function to draw one error. - # @param error Error to draw. - # @param file File the error is in. - def draw_one_error(self, error, file): - # Check if we have suppressed this kind of errors. - if config.getboolean("Suppress", errors.text[error[1]]): - return - - pos = "" - - # We are on map, so add X/Y coordinates after the error description. - if len(error) > 2 and error[2] != -1 and error[3] != -1: - pos = " (" + str(error[2]) + ", " + str(error[3]) + ")" - - # Add the error. - self.window.sm.get_model().append([file, "" + errors.text[error[1]] + "", error[0] + pos]) - - # Draw the errors. - def draw_errors(self): - # Clear out old drawn errors. - self.window.sm.get_model().clear() - - # Draw map errors. - for (map, map_errors) in errors_l: - for error in map_errors: - self.draw_one_error(error, map) - - # Archetype errors. - for error in errors_archetypes: - self.draw_one_error(error, "Archetypes") - - # Artifact errors. - for error in errors_artifacts: - self.draw_one_error(error, "Artifacts") - - # Region errors. - for error in errors_regions: - self.draw_one_error(error, "Regions") - - # Try to find us an icon for the application. - def find_icon(self): - # Possible paths where our icon could be. - paths = [ - "/usr/share/atrinik/bitmaps/icon.png", - "../../client/bitmaps/icon.png", - ] - - for path in paths: - if os.path.exists(path): - return gtk.gdk.pixbuf_new_from_file(path) - - try: - # Initialize the GUI. - gui = GUI() - gtk.main() - finally: - config_save() -# CLI. -else: - # Common function to print one error on the CLI. - # @param error Error to print. - # @param map Map. If not None, we are drawing an error on map, otherwise a different error (artifacts, - # archetypes, etc). - def print_one_error(error, map): - pos = "" - - # We are on map, so add X/Y coordinates before the error description. - if map and error[2] != -1 and error[3] != -1: - if text_only: - pos = str(error[2]) + " " + str(error[3]) + " " - else: - pos = "(" + str(error[2]) + ", " + str(error[3]) + "): " - - if text_only: - print(pos + "" + errors.text[error[1]] + " " + error[0]) - else: - print(" " + errors.colors[error[1]] + errors.text[error[1]] + colors.end + ": " + pos + error[0]) - - # Start the scan. - do_scan() - - if not one_map: - print(colors.bold + colors.underscore + "Scan complete. Results:\n" + colors.end + colors.end) - - # Print map errors. - for (map, map_errors) in sorted(errors_l): - if not one_map: - print(colors.bold + map + ":" + colors.end) - - for error in map_errors: - print_one_error(error, map) - - # Archetype errors. - if errors_archetypes: - print(colors.bold + "Archetypes:" + colors.end) - - for error in errors_archetypes: - print_one_error(error, None) - - # Artifact errors. - if errors_artifacts: - print(colors.bold + "Artifacts:" + colors.end) - - for error in errors_artifacts: - print_one_error(error, None) - - # Region errors. - if errors_regions: - print(colors.bold + "Regions:" + colors.end) - - for error in errors_regions: - print_one_error(error, None) diff --git a/map-checker/start_gui.bat b/map-checker/start_gui.bat deleted file mode 100644 index b58e44a..0000000 --- a/map-checker/start_gui.bat +++ /dev/null @@ -1 +0,0 @@ -%SystemDrive%\Python27\pythonw.exe map-checker.py diff --git a/map-maker/README b/map-maker/README deleted file mode 100644 index 11b4e10..0000000 --- a/map-maker/README +++ /dev/null @@ -1,53 +0,0 @@ -This script can be used to easily build a map maker package. It will automatically -package the whole map maker, but you have to build the server/client binaries -first. - -=== Running the script === - -To run the script, you need to specify your working copy directory and your Atrinik -repository copy directory. - -Repository copy directory will be used to copy default files, so you should run this -when the repository is not modified and is up-to-date. - -Working copy directory will be the place where you built the binaries for the server -and the client. - -Example of running the script: - ./build.py -r ~/atrinik -w ~/atrinik-working - -=== Instructions === - -You have to build the binaries before running the script. The list of binaries the -script needs can be found below. All the binaries should be 32-bit so they can be -ran on 64-bit systems as well. - -server/atrinik-server: - GNU/Linux binary of the server. - -server/atrinik-server.exe: - Windows binary of the server. - -server/*.dll: - DLLs needed by the Windows server. This is optional as some builds of the server - don't need any dlls. - -server/plugins/*.so: - GNU/Linux plugin binaries. There must be at least one plugin. - -server/plugins/*.dll: - Windows plugin binaries. There must be at least one plugin. - -client/atrinik: - GNU/Linux binary of the client. - -client/atrinik.exe: - Windows binary of the client. - -client/*.dll: - DLLs needed by the Windows client. Required. - -=== Merge === - -Everything located inside the 'merge' directory in the directory this file is in will -be merged into the map maker package. diff --git a/map-maker/build.py b/map-maker/build.py deleted file mode 100755 index 7405a87..0000000 --- a/map-maker/build.py +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/python -#************************************************************************* -#* Atrinik, a Multiplayer Online Role Playing Game * -#* * -#* Copyright (C) 2009-2014 Zoey Rose and Atrinik Development Team * -#* * -#* Fork from Crossfire (Multiplayer game for X-windows). * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU General Public License as published by * -#* the Free Software Foundation; either version 2 of the License, or * -#* (at your option) any later version. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU General Public License for more details. * -#* * -#* You should have received a copy of the GNU General Public License * -#* along with this program; if not, write to the Free Software * -#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * -#* * -#* The author can be reached at admin@atrinik.org * -#************************************************************************* - -# Script to build Atrinik map maker package. - -import sys, os, getopt, shutil, glob, zipfile, re -from distutils import dir_util -from datetime import datetime - -# Print usage. -def usage(): - print("\nUse:\nScript to build Atrinik map maker package.\n") - print("Options:") - print("\n\t-h, --help:\n\t\tDisplay this help.") - print("\n\t-v, --version:\n\t\tBe verbose about what is happening.") - print("\n\t-w, --working:\n\t\tWorking copy. This should have Win32 and GNU/Linux server plugins, server binaries and client binaries.") - print("\n\t-r, --repo:\n\t\tYour copy of the Atrinik main repository.") - print("\nExample:") - print("\n\t{0} --repo ~/atrinik --working ~/atrinik-working".format(sys.argv[0])) - -# Try to parse our command line options. -try: - opts, args = getopt.getopt(sys.argv[1:], "hvw:r:", ["help", "verbose", "working=", "repo="]) -except getopt.GetoptError as err: - # Invalid option, show the error, print usage, and exit. - print(err) - usage() - sys.exit(2) - -# The default options. -verbose = False -repo_dir = None -working_dir = None - -# Parse options. -for o, a in opts: - if o in ("-h", "--help"): - usage() - sys.exit() - elif o in ("-v", "--verbose"): - verbose = True - elif o in ("-w", "--working"): - working_dir = a - elif o in ("-r", "--repo"): - repo_dir = a - -# Directories must be specified. -if not repo_dir: - print("ERROR: You have no specified repository directory with --repo. Re-run with --help for help.") - sys.exit(2) -elif not working_dir: - print("ERROR: You have no specified working copy directory with --working. Re-run with --help for help.") - sys.exit(2) - -# Dirs don't exist? -if not os.path.exists(repo_dir) or not os.path.isdir(repo_dir): - print("ERROR: '{0}' does not exist or is not a directory.".format(repo_dir)) - sys.exit(2) -elif not os.path.exists(working_dir) or not os.path.isdir(working_dir): - print("ERROR: '{0}' does not exist or is not a directory.".format(working_dir)) - sys.exit(2) - -# Directories to copy from repo. -dirs_copy = ["server", "client", "editor", "maps", "arch", "tools"] -# Directories to make. -dirs_make = ["server/lib", "server/data", "server/data/players", "server/data/tmp", "server/data/log", "server/data/unique-items"] -# Binaries to copy. First parameter is an expression of what binaries -# to copy, second one is whether they are required or not. -binaries = [ - ["server/atrinik-server", True], - ["server/atrinik-server.exe", True], - ["server/*.dll", True], - ["server/*.so", True], - ["client/atrinik", True], - ["client/atrinik.exe", True], - ["client/*.dll", True], - ["editor/AtrinikEditor.jar", True], - ["*.bat", True], - ["*.sh", True], -] - -# Common debug printing function. Only prints 'txt' -# if --verbose was set. -# @param txt Text to print. -def debug(txt): - if verbose: - print(txt) - -# Some information... -print("Repo dir: {0}".format(repo_dir)) -print("Working dir: {0}".format(working_dir)) -print("\nBuild in progress...\n") - -# If 'atrinik_map_maker' directory exists (previous build failed?), remove it first of all. -if os.path.exists("atrinik_map_maker"): - debug("Recursively removing old atrinik_map_maker directory...") - shutil.rmtree("atrinik_map_maker") - -# Now create a new one. -debug("Creating atrinik_map_maker directory...") -os.mkdir("atrinik_map_maker") - -# Copy default directories. -debug("Copying default directories...") - -for d in dirs_copy: - src = repo_dir + "/" + d - dst = "atrinik_map_maker/" + d - debug(" Copying: {0}".format(src)) - - # Sanity. - if not os.path.exists(src) or not os.path.isdir(src): - print("ERROR: '{0}' does not exist or is not a directory.".format(src)) - sys.exit(2) - - # Copy recursively. - dir_util.copy_tree(src, dst) - -# Create default directories. -debug("Creating default directories...") - -for d in dirs_make: - dst = "atrinik_map_maker/" + d - debug(" Creating: {0}".format(dst)) - os.mkdir(dst) - -# Copy files from server/install_data to server/data. -debug("Copying default files from install_data directory to data directory...") - -install_dir = "atrinik_map_maker/server/install_data" - -# Sanity check. -if not os.path.exists(install_dir) or not os.path.isdir(install_dir): - print("ERROR: '{0}' does not exist or is not a directory.".format(install_dir)) - sys.exit(2) - -files = os.listdir(install_dir) - -for f in files: - src = install_dir + "/" + f - - # Don't copy directories, only files. - if os.path.isfile(src): - debug(" Copying: {0}".format(src)) - # Copy the file. - shutil.copyfile(src, "atrinik_map_maker/server/data/" + f) - -# Now copy other binaries. -debug("Copying binaries...") - -for expression in binaries: - nodes = glob.glob(working_dir + "/" + expression[0]) - - if expression[1] and len(nodes) == 0: - print("ERROR: Could not match expression '{0}', missing file(s)?".format(expression[0])) - sys.exit(2) - - for src in nodes: - debug(" Copying: {0}".format(src)) - # Copy the binary. - shutil.copyfile(src, "atrinik_map_maker/" + src[len(working_dir) + 1:]) - -# Copy files from 'merge' directory to the atrinik_map_maker dir. -debug("Merging contents of 'merge' directory...") - -merge_dir = "merge" - -# Sanity. -if not os.path.exists(merge_dir) or not os.path.isdir(merge_dir): - print("ERROR: '{0}' does not exist or is not a directory.".format(merge_dir)) - sys.exit(2) - -files = os.listdir(merge_dir) - -for f in files: - src = merge_dir + "/" + f - dst = "atrinik_map_maker/" + f - debug(" Merging: {0}".format(f)) - - # For files, just copy it, for dirs, copy the entire tree recursively. - if os.path.isfile(src): - shutil.copyfile(src, dst) - else: - dir_util.copy_tree(src, dst) - -# Recursively add files to zip archive. -# @param zipf ZipFile instance. -# @param directory Directory we're doing. -def zip_recurse(zipf, directory): - nodes = os.listdir(directory) - - for item in nodes: - src = directory + "/" + item - - # Write the file to the zip archive. - if os.path.isfile(src): - zipf.write(src, None, zipfile.ZIP_DEFLATED) - # Go on recursively. - elif os.path.isdir(src): - zipf.write(src, None, zipfile.ZIP_DEFLATED) - zip_recurse(zipf, src) - -# Find Atrinik's version. -debug("Attempting to find Atrinik's version...") - -version = "" - -# Client's build.config file. -client_build_config = repo_dir + "/client/build.config" - -# Does it exist? -if os.path.exists(client_build_config): - fp = open(client_build_config) - - for line in fp: - match = re.match(r".*PACKAGE_VERSION_\w+ (\d+).*", line) - - if match: - version += match.group(1) + "." - - fp.close() - - if version: - version = version[:-1] + "_" - debug(" Found Atrinik's version: {0}".format(version[:-1])) - -# Zip the entire dir. -debug("Zipping up the entire directory...") - -# The zip archive name. -zip_name = "atrinik_map_maker_" + version + datetime.now().strftime("%Y%m%d") + ".zip" - -# Create the zip archive. -zipf = zipfile.ZipFile(zip_name, "w", compression = zipfile.ZIP_DEFLATED) -# Now recursively add the map maker dir. -zip_recurse(zipf, "atrinik_map_maker") -# Close the archive. -zipf.close() - -# Remove the map maker dir. -debug("Recursively removing temporary directory...") -shutil.rmtree("atrinik_map_maker") - -# Finished! -print(" Done!") diff --git a/map-maker/merge/README b/map-maker/merge/README deleted file mode 100644 index ee22229..0000000 --- a/map-maker/merge/README +++ /dev/null @@ -1,70 +0,0 @@ -This is the Atrinik Map Maker package. Below you'll find instructions how to -use this package. - -##################### -# Windows # -##################### - -== Map Editor == - -To run Gridarta for Atrinik - the Atrinik map editor - you'll need Java 1.6 or -later. Download it from the Java website (http://www.java.com). - -Note that on some versions of Java (generally the newer ones), you have to -manually set the PATH environment variable. See this for instructions: -http://www.jibble.org/settingupjava.php - -When you're finished setting up Java, double-click w32_start_editor.bat to start -the editor. - -== Server == - -You need to download Python 3.4 from the Python website (http://www.python.org) -and install it to the default location. - -Simply double-click w32_start_server.bat and the server should start. - -== Client == - -Double-click w32_start_client.bat. - -== Map Checker == - -Map checker is a tool to check your maps located in tools/map-checker-qt. To run -it, you need Python 3.4 and PyQt5, which can be downloaded here: -http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.4.1/PyQt5-5.4.1-gpl-Py3.4-Qt5.4.1-x32.exe - -Then simply double-click map-checker-w32.bat in tools/map-checker-qt. - -##################### -# GNU/Linux # -##################### - -== Map Editor == - -Download Java 1.6 or later from the Java website (http://www.java.com) or -install it using your favorite package manager. Then use: - -./linux_start_editor.sh - -== Server == - -Use your favorite package manager to install Python 3.3 or later (or compile it from source). -Then use this command: - -./linux_start_server.sh - -== Client == - -Use this command to start up the client: - -./linux_start_client.sh - -== Map Checker == - -Map checker is a tool to check your maps located in tools/map-checker-qt. To run -it, you need Python 3.3 or later (which you should have by now from the above -server step) and PyQt5, which you can install using your favorite package -manager. After that, run this command: - -./tools/map-checker-qt/map-checker.py diff --git a/mapset/mapset.py b/mapset/mapset.py deleted file mode 100755 index fe4f2ec..0000000 --- a/mapset/mapset.py +++ /dev/null @@ -1,461 +0,0 @@ -#!/usr/bin/python -#************************************************************************* -#* Atrinik, a Multiplayer Online Role Playing Game * -#* * -#* Copyright (C) 2009-2014 Zoey Rose and Atrinik Development Team * -#* * -#* Fork from Crossfire (Multiplayer game for X-windows). * -#* * -#* This program is free software; you can redistribute it and/or modify * -#* it under the terms of the GNU General Public License as published by * -#* the Free Software Foundation; either version 2 of the License, or * -#* (at your option) any later version. * -#* * -#* This program is distributed in the hope that it will be useful, * -#* but WITHOUT ANY WARRANTY; without even the implied warranty of * -#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -#* GNU General Public License for more details. * -#* * -#* You should have received a copy of the GNU General Public License * -#* along with this program; if not, write to the Free Software * -#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * -#* * -#* The author can be reached at admin@atrinik.org * -#************************************************************************* - -## @file -## Script to generate tiled mapsets, or extend an already existing mapset. - -import os, sys, re, shutil, time, random, getopt - -## Similar to C/C++ getline, waits for the provided stream to finish -## a line (or anything else delimited by 'delimiter'). -## @param stream Which stream to use. -## @param delimiter What marks the end of line. -## @return The line. -def getline(stream = sys.stdin, delimiter = "\n"): - def _go(): - while True: - line = stream.readline() - - if not delimiter in line: - yield line - else: - yield line[:line.index(delimiter)] - break - - return "".join(_go()) - -## The options class; ask the user questions about the mapset. -class Options: - ## Initialize the class. - ## @param options List of the options. - def __init__(self, options): - self._options = options - - ## Ask questions and wait for answers. - def run(self): - for (i, [opt_name, opt_value, opt_msg]) in enumerate(self._options): - # Cannot ask this one. - if not opt_msg: - continue - - # Loop until we get valid input. - while True: - # Ask the question. - print(opt_msg.format(self[opt_name])) - - # Handle the input (once getline() stops blocking and has finished input). - if self._handle_input(opt_name, getline()): - break - - ## Get option. - ## @param opt_name The option name to get. - ## @return The option's value, None if no such option exists. - def __getitem__(self, opt_name): - for opt in self._options: - if opt[0] == opt_name: - return opt[1] - - return None - - ## Set an option to new value. - ## - ## If the option doesn't exist, it will be created. - ## @param opt_name The option name. - ## @param val New value. - def __setitem__(self, opt_name, val): - for opt in self._options: - if opt[0] == opt_name: - opt[1] = val - return - - self._options.append([opt_name, val, None]) - - ## Handles user's input. - ## @param opt_name Option name. - ## @param line The input. - ## @return True on success, False on error. - def _handle_input(self, opt_name, line): - # Clean it up... - value = line.strip() - - # Filename, remove invalid characters. - if opt_name == "filename": - # Replace spaces with underscores, and only allow alphanumeric characters, - # underscores and hyphens. - value = re.sub(r"[^a-zA-Z0-9\_\-]", "", value.replace(" ", "_")).lower() - # Parse size. - elif opt_name == "size": - # Should be in the format of x. - match = re.match(r"([\d]+)(?:\s+)?x(?:\s+)?([\d]+)", value) - - # Invalid input. - if not match: - value = None - else: - # Construct a tuple containing the two sizes... - groups = match.groups() - value = (int(groups[0]), int(groups[1])) - - # Do not allow either width or height to be 0. - if not value[0] or not value[1]: - value = None - - # No value yet, try to use default. - if not value: - value = self[opt_name] - - # Still no value... - if not value: - print("Error: Invalid value, please try again.") - return False - - # Set the new value. - self[opt_name] = value - - # Name, try to guess the filename. - if opt_name == "name": - self["filename"] = re.sub(r"[^a-zA-Z0-9\_\-]", "", value.replace(" ", "_")).lower() - - return True - -## Handles X/Y coordinate modifiying based on the direction of tiled map. -class Tiles: - ## The directions of tiled maps. - _tiles = [ - (0, 0), - (0, 1), - (1, 0), - (0, -1), - (-1, 0), - (1, 1), - (1, -1), - (-1, -1), - (-1, 1), - ] - - ## Dictionary that maps strings into the actual directions in ::_tiles. - _directions = { - "north": 1, - "east": 2, - "south": 3, - "west": 4, - "northeast": 5, - "southeast": 6, - "southwest": 7, - "northwest": 8, - } - - ## Initialize the class. - def __init__(self): - pass - - ## Get the X/Y coordinate modifier for the specified tiled map - ## direction. - ## @param val String of the direction, or integer index inside ::_tiles. - ## @return The X/Y coordinate modifier. - def __getitem__(self, val): - if type(val) == type(int()): - return self._tiles[val] - else: - return self._tiles[self._directions[val]] - - ## Get the number of directions for tiled maps. - def __len__(self): - return len(self._tiles) - -## Transform integer coordinate into string. -## @param val The integer coordinate. -## @return The coordinate in map naming convention string. -def coordinate_str(val): - # 0 or higher, simple number. - if val >= 0: - return "{num:02d}".format(num = val + 1) - - # Negative uses alphabet, so transform the big number - # into two letters. - val = -val - 1 - return "{0}{1}".format(chr(97 + val // 26), chr(97 + val % 26)) - -## Transform string coordinate in map naming convention into integer. -## @param s The string coordinate. -## @return Integer coordinate. -def coordinate_int(s): - # It's a digit, simple integer. - if s.isdigit(): - return int(s) - 1 - - # Transform the letters into an integer. - return -(((ord(s[:1]) - 97) * 26) + ((ord(s[1:]) - 97))) - 1 - -## Generate maps. -## @param directory What directory we're working in. -## @param start X/Y coordinate modifier. -def generate_maps(directory, start): - # Get the size. - (width, height) = options["size"] - # List of archetypes we can use. - archetypes = [] if options["archetype"] == "nothing" else options["archetype"].split(",") - - for xt in range(width): - for yt in range(height): - x = xt + start[0] - y = yt + start[1] - # Construct the new path. - path = "{0}/{1}_{2}{3}".format(directory, options["filename"], coordinate_str(x), coordinate_str(y)) - - # Exists already? - if os.path.exists(path): - print("Warning: map {0} already exists, not creating.".format(path)) - continue - - # Write out the map information. - f = open(path, "wb") - f.write("arch map\n") - f.write("name {0}\n".format(options["name"])) - f.write("msg\n") - f.write("Created: {0} {1}\n".format(time.strftime("%Y-%m-%d"), options["author"])) - f.write("endmsg\n") - f.write("width 24\n") - f.write("height 24\n") - f.write("difficulty {0}\n".format(options["difficulty"])) - - # Region... - if options["region"] != "none": - f.write("region {0}\n".format(options["region"])) - - # Outdoor? - if options["outdoor"][:1] == "y": - f.write("outdoor 1\n") - - # Darkness. - if options["darkness"] != "-1": - f.write("darkness {0}\n".format(options["darkness"])) - - # Background music. - if options["bg_music"] != "none": - f.write("bg_music {0}\n".format(options["bg_music"])) - - # Weather. - if options["weather"] != "none": - f.write("weather {0}\n".format(options["weather"])) - - f.write("end\n") - - # If list of archetypes was provided, tile the map. - if archetypes: - for arch_x in range(24): - for arch_y in range(24): - f.write("arch {0}\n".format(random.choice(archetypes).strip())) - - if arch_x: - f.write("x {0}\n".format(arch_x)) - - if arch_y: - f.write("y {0}\n".format(arch_y)) - - f.write("end\n") - - f.close() - -## Connect all maps inside directory. -## @param directory The directory to work in. -def connect_maps(directory): - for f in os.listdir(directory): - path = os.path.join(directory, f) - - # Hidden file or not a file. - if f.startswith(".") or not os.path.isfile(path): - continue - - # Must match map_name_xxyy format. - if not re.match("(.+)_(.+){4}", f): - continue - - # Store the file's base name without coordinates. - fname = f[:-4] - # Read the file and store the lines in memory. - f = open(path, "rb") - lines = f.readlines() - f.close() - - # Not a map file, go on. - if lines[0] != "arch map\n": - continue - - # Get the integer coordinates. - coords = path[-4:] - coord_x = coordinate_int(coords[:2]) - coord_y = coordinate_int(coords[2:]) - # Marks that we have written out the tile path information. - tile_written = False - - # Open the file and truncate it. - f = open(path, "wb") - - # Write out the lines. - for line in lines: - # Not written yet and this is either the end of the map header - # or we found a tile_path line. - if not tile_written and (line == "end\n" or line.startswith("tile_path_")): - # So we don't write them out twice. - tile_written = True - - # Write out the tile paths. - for tile in range(1, len(tiles)): - # Construct the base map file name. - tiled_map = "{0}{1}{2}".format(fname, coordinate_str(coord_x + tiles[tile][0]), coordinate_str(coord_y + tiles[tile][1])) - tiled_path = os.path.join(directory, tiled_map) - - # The map exists and is a file, so we have a valid tiled map. - if os.path.exists(tiled_path) and os.path.isfile(tiled_path): - f.write("tile_path_{0} {1}\n".format(tile, tiled_map)) - - # Ignore tile_path lines. - if not line.startswith("tile_path_"): - f.write(line) - - f.close() - -## Print usage. -def usage(): - print("Application to create new mapset, or extend existing one.\n\nOptions:") - print("\t-h, --help: Show this help.") - print("\t-e, --extend: Extend mode.") - print("\t-r path, --reconnect=path: Specify a directory of which to reconnect tiled paths.") - -# Try to parse our command line options. -try: - opts, args = getopt.getopt(sys.argv[1:], "her:", ["help", "extend", "reconnect="]) -except getopt.GetoptError as err: - # Invalid option, show the error, print usage, and exit. - print(err) - usage() - sys.exit(2) - -# The default options. -extend = False -reconnect = None - -# Parse options. -for o, a in opts: - if o in ("-h", "--help"): - usage() - sys.exit() - elif o in ("-e", "--extend"): - extend = True - elif o in ("-r", "--reconnect"): - reconnect = a - -## Default questions asked in both modes (extending and creating). -def_questions = [ - ["size", "", "Enter the size of the mapset (for example, '10x5' [10 = width, 5 = height]):"], - ["author", "", "Enter the author (name that appears in the map's message in /mapinfo):"], - ["archetype", "nothing", "Enter the (comma delimited) archetype(s) with which to fill the created maps (default: '{0}'):"], - ["difficulty", "1", "Enter the difficulty used across all created maps (default: '{0}'):"], - ["darkness", "-1", "Enter the darkness used across all created maps ('-1' = '7' = full light, '0' = full darkness, default: '{0}'):"], - ["outdoor", "no", "Is the mapset outdoor ('yes'/'no', default: '{0}')?:"], - ["region", "world", "Enter the region the mapset is in ('world', 'none', default: '{0}'):"], - ["bg_music", "none", "Enter the background music used across all created maps ('cave.xm', 'none', default: '{0}'):"], - ["weather", "none", "Enter the weather used across all created maps ('snow', 'none', default: '{0}'):"], -] - -# Not extending, so ask questions related to the map creating. -if not extend: - questions = [ - ["name", "", "Enter the name of the mapset (map name for the created maps, for example, 'Ancient Forest'):"], - ["filename", "", "Enter the base file name (for example, 'world'; default: '{0}'):"], - ] -# Extending, we need to know where to start extending and the direction. -else: - questions = [ - ["name", "", "Enter the name of the mapset (map name for the created maps, for example, 'Ancient Forest'):"], - ["extend_start", "", "Enter the file name where to start extending (for example, '../../maps/shattered_islands/world_0101'):"], - ["direction", "", "Direction which to extend into (for example, 'south', 'west', 'northeast', etc):"], - ] - -# Initialize Options. -options = Options(questions + def_questions) -# Initialize Tiles. -tiles = Tiles() - -## The main function. -def main(): - # Reconnecting maps, no need to ask questions. - if reconnect: - if not os.path.exists(reconnect) or not os.path.isdir(reconnect): - print("Error: Directory {0} doesn't exist or is not a directory.".format(reconnect)) - return - - connect_maps(reconnect) - return - - # Ask the user some questions... - options.run() - - # Extending. - if not extend: - directory = "generated-maps" - start = (0, 0) - - # Remove existing directory. - if os.path.exists(directory): - shutil.rmtree(directory) - - os.mkdir(directory) - else: - # Get the coordinates of the start map... - coords = options["extend_start"][-4:] - coord_x = coordinate_int(coords[:2]) - coord_y = coordinate_int(coords[2:]) - - # Store the base name of the start map (without coordinates). - options["filename"] = os.path.basename(options["extend_start"])[:-5] - - try: - # Get the tile X/Y modifier, depending on the direction the user wanted. - (tile_x, tile_y) = tiles[options["direction"]] - except KeyError: - print("Error: Invalid direction {0}.".format(options["direction"])) - return - - # West, modify the starting coordinate by the wanted width. - if tile_x < 0: - tile_x *= options["size"][0] - - # South, modify the starting coordinate by the wanted height. - if tile_y < 0: - tile_y *= options["size"][1] - - directory = os.path.dirname(options["extend_start"]) - start = (coord_x + tile_x, coord_y + tile_y) - - print("Generating maps...") - generate_maps(directory, start) - print("Maps saved to {0}.".format(directory)) -# print("Connecting maps...") -# connect_maps(directory) -# print("Done!") - -main() diff --git a/randomizer/random b/randomizer/random deleted file mode 100644 index 133822a..0000000 --- a/randomizer/random +++ /dev/null @@ -1,2 +0,0 @@ -Name: grassd_ -Randoms: 1,2,3,4,5,6,7,8 diff --git a/randomizer/randomizer.c b/randomizer/randomizer.c deleted file mode 100644 index ae28514..0000000 --- a/randomizer/randomizer.c +++ /dev/null @@ -1,319 +0,0 @@ -/************************************************************************ - * Atrinik, a Multiplayer Online Role Playing Game * - * * - * Copyright (C) 2009-2014 Zoey Rose and Atrinik Development Team * - * * - * Fork from Crossfire (Multiplayer game for X-windows). * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the Free Software * - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * - * * - * The author can be reached at admin@atrinik.org * - ************************************************************************/ - -/* Small utility to randomize map arches, with a custom list of things to - * randomize, like floor, flowers, trees, etc. - * - * Syntax for the "random" file is as follows: - * Name: Arch name to match, can be incomplete - * Randoms: Random variants to add to the arch name above, if match found. - * Comments in the file are allowed and will be ignored by the parser. - * - * Compile as: - * gcc randomizer.c -O3 -Wall -W -pedantic -Werror -o randomizer - * - * Use as: - * ./randomizer "your map" > "map to write to" - * - * Please note that the randomizer NEVER saves the randomized content to - * the original file. It will output the randomized map content to the - * terminal screen, unless told otherwise, like by ">" to write the output - * to a file. */ - -#include -#include -#include -#include -#include - -#define MAX_BUF 4096 - -#define RANDOM() random() -#define SRANDOM(xyz) srandom(xyz) - -/* Structure for the random variations of arches */ -typedef struct random_variations_struct { - /* The variation ("1", "2", 3", "small", "large", etc) */ - char random_var[MAX_BUF]; - - /* Next variation in this linked list */ - struct random_variations_struct *next; -} random_variations; - -/* Structure for the arches to randomize from file */ -typedef struct random_struct { - /* Name of arch to randomize */ - char archname[MAX_BUF]; - - /* Number of possible variations */ - int variations; - - /* Start of the random variations list */ - random_variations *randoms_start; - - /* Next arch to randomize in this linked list */ - struct random_struct *next; -} random_struct; - -/* Initialize random list to NULL */ -random_struct *random_list = NULL; - -static int join_arch_name(char *destination, - size_t destination_size, - const char *name, - const char *variation) { - int length = snprintf(destination, destination_size, "%s%s", name, variation); - - return length >= 0 && (size_t)length < destination_size; -} - -/* Signal handler for SIGSEGV -- make core with abort. */ -static void signal_sigsegv(int i) { - (void)i; - - abort(); -} - -/* Random number function, with min and max */ -int rndm(int min, int max) { - int diff; - - diff = max - min + 1; - - if (max < 1 || diff < 1) - return min; - - return (RANDOM() % diff + min); -} - -/* Parse the "random" file */ -static void parse_randoms(void) { - FILE *fh; - char line[MAX_BUF], name[MAX_BUF], randoms[MAX_BUF], *p; - random_struct *random_tmp; - random_variations *random_variations_tmp; - - /* If failed to open the random file, just return. */ - if ((fh = fopen("random", "r")) == NULL) - return; - - /* Loop through all the lines in the file */ - while (fgets(line, MAX_BUF - 1, fh)) { - /* Ignore comments */ - if (line[0] == '#') - continue; - - /* Scan the line for Name:, and store it */ - if (sscanf(line, "Name: %4095s", name) == 1) { - /* Loop through the next lines, and break out on Randoms: match. */ - while (fgets(line, MAX_BUF - 1, fh)) { - /* Ignore comments */ - if (line[0] == '#') - continue; - - /* Scan the next line for Randoms: and store it */ - if (sscanf(line, "Randoms: %4095s", randoms) == 1) { - /* Allocate a new random list structure */ - random_tmp = calloc(1, sizeof(*random_tmp)); - if (random_tmp == NULL) { - fprintf(stderr, "ERROR: Out of memory while reading random rules\n"); - fclose(fh); - exit(EXIT_FAILURE); - } - - /* Append the old list structure to it */ - random_tmp->next = random_list; - - /* Switch the old structure with this new one */ - random_list = random_tmp; - - /* Number of variations starts at 0 */ - random_tmp->variations = 0; - - /* Store the arch name to look for in the map file */ - snprintf(random_tmp->archname, sizeof(random_tmp->archname), "%s", name); - - /* Now loop through the random vriations by "," */ - p = strtok(randoms, ","); - - while (p) { - /* One more variation... */ - random_tmp->variations++; - - /* Allocate a new list of random variations */ - random_variations_tmp = calloc(1, sizeof(*random_variations_tmp)); - if (random_variations_tmp == NULL) { - fprintf(stderr, "ERROR: Out of memory while reading variations\n"); - fclose(fh); - exit(EXIT_FAILURE); - } - - /* Append the old list structure to it */ - random_variations_tmp->next = random_tmp->randoms_start; - - /* Switch the old structure with this new one */ - random_tmp->randoms_start = random_variations_tmp; - - /* Store the random variation */ - snprintf(random_variations_tmp->random_var, - sizeof(random_variations_tmp->random_var), - "%s", - p); - - p = strtok(NULL, ","); - } - - break; - } - } - } - } - - /* Close the file handle */ - fclose(fh); -} - -/* Main function of the randomizer */ -int main(int argc, char *argv[]) { - random_struct *random_tmp; - random_variations *random_variations_tmp; - char archname[MAX_BUF], filename[MAX_BUF], line[MAX_BUF]; - FILE *fh; - - (void)argc; - - /* Handle SIGSEGV (Segmentation fault) event, so we can dump core */ - signal(SIGSEGV, signal_sigsegv); - - /* If map file was not specified, show usage */ - if (argv[1] == NULL) { - fprintf(stdout, "Usage: %s \n", argv[0]); - return EXIT_FAILURE; - } - /* Otherwise store the map file */ - else if (snprintf(filename, sizeof(filename), "%s", argv[1]) >= (int)sizeof(filename)) { - fprintf(stderr, "ERROR: Map filename is too long\n"); - return EXIT_FAILURE; - } - - /* Now open the map file in read mode */ - if ((fh = fopen(filename, "r")) == NULL) { - fprintf(stderr, "ERROR: Failed to open specified file in read-only mode: '%s'\n", filename); - return EXIT_FAILURE; - } - - /* Seed the random number */ - SRANDOM(time(NULL)); - - /* Parse the "random" file */ - parse_randoms(); - - /* Loop through all the lines of this map */ - while (fgets(line, MAX_BUF, fh)) { - /* Check if this line has arch name, if so, store it */ - if (sscanf(line, "arch %4095s", archname) == 1) { - /* Loop through the linked list of arches to randomize */ - for (random_tmp = random_list; random_tmp; random_tmp = random_tmp->next) { - /* The arch name on map must match with the one from the list, - * at least partially */ - if (strncmp(random_tmp->archname, archname, strlen(random_tmp->archname)) == 0) { - int found = 0, i = 1, random_int = rndm(1, random_tmp->variations); - char tmparchname[MAX_BUF]; - - /* First loop through the random variations list. This is - * necessary so - * that we know if we should randomize this arch. For - * example, do NOT - * randomize grassd_m2 if grassd_ is on the list, and m2 is - * not on the - * random variations list. */ - for (random_variations_tmp = random_tmp->randoms_start; random_variations_tmp; - random_variations_tmp = random_variations_tmp->next) { - /* Store the arch name from the config file in a - * temporary buffer, - * with both the (partial) arch name and the random - * variation. */ - if (!join_arch_name(tmparchname, - sizeof(tmparchname), - random_tmp->archname, - random_variations_tmp->random_var)) { - continue; - } - - /* Compare it, for secure also compare the lengths */ - if (strcmp(archname, tmparchname) == 0 && - strlen(archname) == strlen(tmparchname)) { - /* Found it, break out and move on the next loop */ - found = 1; - - break; - } - } - - /* Only if the above loop returned 1 */ - if (found) { - /* Second loop through the random variations list. Here - * we just - * check if i is equal to the random number calculated - * above. */ - for (random_variations_tmp = random_tmp->randoms_start; - random_variations_tmp; - random_variations_tmp = random_variations_tmp->next) { - /* If i (variation ID) is equal to the random - * number, - * overwrite the old arch name and break out. */ - if (i == random_int) { - if (!join_arch_name(archname, - sizeof(archname), - random_tmp->archname, - random_variations_tmp->random_var)) { - fprintf(stderr, - "ERROR: Randomized archetype name is too long\n"); - fclose(fh); - return EXIT_FAILURE; - } - - break; - } - - i++; - } - } - } - } - - /* Print out the arch name, even if we did no randomization */ - fprintf(stdout, "arch %s\n", archname); - } - /* Normal line -- just print it out */ - else - fprintf(stdout, "%s", line); - } - - /* Close the file handle */ - fclose(fh); - - /* Exit cleanly */ - return 0; -} diff --git a/split_symbols.sh b/split_symbols.sh old mode 100644 new mode 100755 index ce60303..7835397 --- a/split_symbols.sh +++ b/split_symbols.sh @@ -1,39 +1,91 @@ -#!/bin/bash +#!/usr/bin/env bash -scriptdir=`dirname ${0}` -scriptdir=`(cd ${scriptdir}; pwd)` -scriptname=`basename ${0}` +set -euo pipefail -set -e - -function errorexit() -{ - errorcode=${1} - shift - echo $@ - exit ${errorcode} +usage() { + printf 'Usage: %s \n' "${0##*/}" } -function usage() -{ - echo "USAGE ${scriptname} " -} +if (( $# < 1 )); then + usage + exit 0 +fi -tostripdir=`dirname "$1"` -tostripfile=`basename "$1"` +target=$1 +target_dir=$(dirname -- "${target}") +target_name=$(basename -- "${target}") +debug_name="${target_name}.debug" +cd -- "${target_dir}" -if [ -z ${tostripfile} ] ; then - usage - errorexit 0 "tostrip must be specified" +if [[ -L "${target_name}" || ! -f "${target_name}" ]]; then + printf 'Not a regular file: %s\n' "${target}" + exit 1 +fi +if [[ -L "${debug_name}" || ( -e "${debug_name}" && ! -f "${debug_name}" ) ]]; then + printf 'Refusing non-regular debug destination: %s\n' "${target_dir}/${debug_name}" >&2 + exit 1 +fi +if [[ -e "${debug_name}" && "${debug_name}" -ef "${target_name}" ]]; then + printf 'Refusing debug destination that aliases executable: %s\n' \ + "${target_dir}/${debug_name}" >&2 + exit 1 fi -cd "${tostripdir}" +scratch=$(mktemp -d ".split-symbols.XXXXXXXX") +cleanup() { + rm -rf -- "${scratch}" +} +trap cleanup EXIT -debugfile="${tostripfile}.debug" +cp --preserve=all -- "${target_name}" "${scratch}/original" +cp --preserve=all -- "${target_name}" "${scratch}/${target_name}" +had_debug=false +if [[ -f "${debug_name}" ]]; then + cp --preserve=all -- "${debug_name}" "${scratch}/previous.debug" + had_debug=true +fi -echo "stripping ${tostripfile}, putting debug info into ${debugfile}" -objcopy --only-keep-debug "${tostripfile}" "${debugfile}" -strip --strip-debug --strip-unneeded "${tostripfile}" -objcopy --add-gnu-debuglink="${debugfile}" "${tostripfile}" -chmod -x "${debugfile}" +printf 'Stripping %s; writing debug information to %s\n' \ + "${target_name}" "${debug_name}" +objcopy --only-keep-debug -- "${target_name}" "${scratch}/${debug_name}" +strip --strip-debug --strip-unneeded -- "${scratch}/${target_name}" +( + cd -- "${scratch}" + objcopy --add-gnu-debuglink="${debug_name}" -- "${target_name}" +) +chmod a-x -- "${scratch}/${debug_name}" +if [[ ${had_debug} == true ]]; then + if ! cp --preserve=mode,ownership,timestamps,xattr -- \ + "${scratch}/${debug_name}" "${debug_name}"; then + rollback_status=0 + cp --preserve=mode,ownership,timestamps,xattr -- \ + "${scratch}/previous.debug" "${debug_name}" || rollback_status=$? + if (( rollback_status != 0 )); then + printf 'Failed to publish debug information and rollback was incomplete: %s\n' \ + "${target_dir}/${debug_name}" >&2 + exit "${rollback_status}" + fi + printf 'Failed to publish debug information: %s\n' \ + "${target_dir}/${debug_name}" >&2 + exit 1 + fi +else + mv -T -- "${scratch}/${debug_name}" "${debug_name}" +fi +if ! cp --preserve=mode,ownership,timestamps,xattr -- "${scratch}/${target_name}" "${target_name}"; then + rollback_status=0 + cp --preserve=mode,ownership,timestamps,xattr -- "${scratch}/original" "${target_name}" || rollback_status=$? + if [[ ${had_debug} == true ]]; then + cp --preserve=mode,ownership,timestamps,xattr -- \ + "${scratch}/previous.debug" "${debug_name}" || rollback_status=$? + else + rm -f -- "${debug_name}" || rollback_status=$? + fi + if (( rollback_status != 0 )); then + printf 'Failed to publish %s and rollback was incomplete\n' "${target}" >&2 + exit "${rollback_status}" + fi + printf 'Failed to publish stripped executable: %s\n' "${target}" >&2 + exit 1 +fi diff --git a/stacktrace.py b/stacktrace.py index fa78cc7..7a40947 100755 --- a/stacktrace.py +++ b/stacktrace.py @@ -1,34 +1,61 @@ -#!/usr/bin/python -import sys, re, subprocess +#!/usr/bin/env python3 +"""Resolve native stack-trace addresses with addr2line.""" -def main (): - if len(sys.argv) < 3: - print("Usage: ./stacktrace.py ") - return +from __future__ import annotations - with open(sys.argv[2], "r") as f: - for line in f: - line = line.strip() - address = None +import re +import subprocess +import sys +from collections.abc import Sequence +from pathlib import Path - if line.endswith("]"): - address = line.split()[-1][1:-1] - else: - match = re.match(r"\d+:\s*(.+)\s*", line, re.I) - if match: - address = match.group(1) +_BRACKETED_ADDRESS = re.compile(r"\[([^]]+)\]$") +_NUMBERED_ADDRESS = re.compile(r"^\d+:\s*(.+?)\s*$", re.IGNORECASE) - if address == None: + +def trace_address(line: str) -> str | None: + """Return an address from a supported stack-trace line.""" + + normalized = line.strip() + for pattern in (_BRACKETED_ADDRESS, _NUMBERED_ADDRESS): + match = pattern.search(normalized) + if match is not None: + return match.group(1) + return None + + +def resolve_trace(executable: Path, trace: Path) -> int: + """Print a symbolized trace while preserving the legacy successful status.""" + + with trace.open(encoding="utf-8", errors="replace") as stream: + for raw_line in stream: + line = raw_line.strip() + address = trace_address(line) + if address is None: print(line) continue - process = subprocess.Popen(["addr2line", "-e", sys.argv[1], "-f", - "-p", address], stdout = subprocess.PIPE) - (output, err) = process.communicate() - exit_code = process.wait() + result = subprocess.run( + ["addr2line", "-e", str(executable), "-f", "-p", address], + check=False, + capture_output=True, + text=True, + ) + print(result.stdout.strip()) + if result.returncode != 0: + if result.stderr: + print(result.stderr.rstrip("\r\n"), file=sys.stderr) + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + args = list(sys.argv[1:] if argv is None else argv) + if len(args) < 2: + print(f"Usage: {Path(sys.argv[0]).name} ") + return 0 + return resolve_trace(Path(args[0]), Path(args[1])) - print(output.strip().decode("ascii")) if __name__ == "__main__": - main() + raise SystemExit(main()) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..1c46074 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the MIT-default tools.""" diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py new file mode 100644 index 0000000..1e5b292 --- /dev/null +++ b/tests/test_diagnostics.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +import contextlib +import importlib.util +import io +import os +import stat +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("stacktrace", ROOT / "stacktrace.py") +assert SPEC is not None and SPEC.loader is not None +STACKTRACE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(STACKTRACE) + + +class StacktraceTests(unittest.TestCase): + def test_trace_address_accepts_supported_formats(self) -> None: + self.assertEqual(STACKTRACE.trace_address(" frame [0x1a2B] "), "0x1a2B") + self.assertEqual(STACKTRACE.trace_address(" 12: symbol+4 "), "symbol+4") + self.assertIsNone(STACKTRACE.trace_address("unresolved frame")) + + def test_resolve_trace_preserves_text_and_legacy_success(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + trace = root / "trace.txt" + trace.write_text("heading\nframe [0x12]\n1: 0x34\n", encoding="utf-8") + resolver = root / "addr2line" + resolver.write_text( + "#!/bin/sh\n" + "case \"$5\" in\n" + " 0x12) printf 'first at source.c:12\\n' ;;\n" + " *) printf 'unresolved\\n' >&2; exit 3 ;;\n" + "esac\n", + encoding="utf-8", + ) + resolver.chmod(resolver.stat().st_mode | stat.S_IXUSR) + old_path = os.environ.get("PATH", "") + os.environ["PATH"] = f"{root}{os.pathsep}{old_path}" + stdout = io.StringIO() + stderr = io.StringIO() + try: + with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + status = STACKTRACE.resolve_trace(root / "game", trace) + finally: + os.environ["PATH"] = old_path + + self.assertEqual(status, 0) + self.assertEqual(stdout.getvalue(), "heading\nfirst at source.c:12\n\n") + self.assertEqual(stderr.getvalue(), "unresolved\n") + + def test_usage_remains_successful(self) -> None: + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(STACKTRACE.main([]), 0) + self.assertIn(" ", output.getvalue()) + + def test_extra_arguments_remain_ignored(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + trace = root / "trace.txt" + trace.write_text("plain frame\n", encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(STACKTRACE.main([str(root / "game"), str(trace), "ignored"]), 0) + self.assertEqual(output.getvalue(), "plain frame\n") + + +class SplitSymbolsTests(unittest.TestCase): + def test_usage_remains_successful(self) -> None: + result = subprocess.run( + [str(ROOT / "split_symbols.sh")], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0) + self.assertIn("", result.stdout) + + def test_extra_arguments_are_ignored_and_nonregular_debug_target_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + executable = root / "sample" + subprocess.run(["cc", "-x", "c", "-g", "-o", str(executable), "-"], input="int main(void) { return 0; }\n", text=True, check=True) + debug = root / "sample.debug" + debug.mkdir() + before = executable.read_bytes() + result = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable), "ignored"], check=False, capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + self.assertIn("Refusing non-regular debug destination", result.stderr) + self.assertEqual(executable.read_bytes(), before) + self.assertEqual(list(debug.iterdir()), []) + + def test_symlinked_executable_is_rejected_without_mutating_referent(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + executable = root / "sample" + subprocess.run(["cc", "-x", "c", "-g", "-o", str(executable), "-"], input="int main(void) { return 0; }\n", text=True, check=True) + link = root / "linked-sample" + link.symlink_to(executable.name) + before = executable.read_bytes() + result = subprocess.run([str(ROOT / "split_symbols.sh"), str(link)], check=False, capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + self.assertTrue(link.is_symlink()) + self.assertEqual(executable.read_bytes(), before) + self.assertFalse((root / "linked-sample.debug").exists()) + + def test_debug_destination_cannot_alias_executable_inode(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + executable = root / "sample" + subprocess.run(["cc", "-x", "c", "-g", "-o", str(executable), "-"], input="int main(void) { return 0; }\n", text=True, check=True) + debug = root / "sample.debug" + os.link(executable, debug) + before = executable.read_bytes() + inode = executable.stat().st_ino + result = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable)], check=False, capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + self.assertIn("aliases executable", result.stderr) + self.assertEqual(executable.read_bytes(), before) + self.assertEqual(debug.read_bytes(), before) + self.assertEqual(executable.stat().st_ino, inode) + self.assertEqual(debug.stat().st_ino, inode) + + def test_preserves_hardlink_identity_and_rolls_back_late_failure(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + executable = root / "sample" + subprocess.run(["cc", "-x", "c", "-g", "-o", str(executable), "-"], input="int main(void) { return 0; }\n", text=True, check=True) + hardlink = root / "sample-link" + os.link(executable, hardlink) + os.setxattr(executable, b"user.tools-test", b"retained") + original_inode = executable.stat().st_ino + success = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable)], check=False, capture_output=True, text=True) + self.assertEqual(success.returncode, 0, success.stderr) + self.assertEqual(executable.stat().st_ino, original_inode) + self.assertEqual(hardlink.stat().st_ino, original_inode) + self.assertEqual(executable.read_bytes(), hardlink.read_bytes()) + self.assertEqual(os.getxattr(executable, b"user.tools-test"), b"retained") + + original = executable.read_bytes() + debug = root / "sample.debug" + previous_debug = debug.read_bytes() + debug_alias = root / "sample.debug.alias" + os.link(debug, debug_alias) + debug_inode = debug.stat().st_ino + fake_tools = root / "late-failure-tools" + fake_tools.mkdir() + for tool in ("bash", "objcopy", "mktemp", "rm", "dirname", "basename", "chmod", "mv", "strip"): + os.symlink(Path("/usr/bin") / tool, fake_tools / tool) + fake_cp = fake_tools / "cp" + fake_cp.write_text( + "#!/bin/bash\n" + "count=$(<\"$COUNT_FILE\")\n" + "count=$((count + 1))\n" + "printf '%s' \"$count\" >\"$COUNT_FILE\"\n" + "case ,$FAIL_COUNTS, in *,$count,*) exit 9 ;; esac\n" + "exec /usr/bin/cp \"$@\"\n", + encoding="utf-8", + ) + fake_cp.chmod(0o755) + count_file = root / "cp-count" + count_file.write_text("0", encoding="ascii") + env = os.environ.copy() + env["PATH"] = str(fake_tools) + env["COUNT_FILE"] = str(count_file) + env["FAIL_COUNTS"] = "5" + failed = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable)], check=False, capture_output=True, text=True, env=env) + self.assertEqual(failed.returncode, 1) + self.assertIn("Failed to publish stripped executable", failed.stderr) + self.assertEqual(executable.read_bytes(), original) + self.assertEqual(debug.read_bytes(), previous_debug) + self.assertEqual(debug.stat().st_ino, debug_inode) + self.assertEqual(debug_alias.stat().st_ino, debug_inode) + self.assertEqual(debug_alias.read_bytes(), previous_debug) + self.assertEqual(executable.stat().st_ino, original_inode) + self.assertFalse(any(root.glob(".split-symbols.*"))) + + count_file.write_text("0", encoding="ascii") + env["FAIL_COUNTS"] = "4" + early = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable)], check=False, capture_output=True, text=True, env=env) + self.assertEqual(early.returncode, 1) + self.assertIn("Failed to publish debug information", early.stderr) + self.assertNotIn("rollback was incomplete", early.stderr) + self.assertEqual(executable.read_bytes(), original) + self.assertEqual(debug.read_bytes(), previous_debug) + self.assertEqual(debug.stat().st_ino, debug_inode) + self.assertEqual(debug_alias.stat().st_ino, debug_inode) + self.assertEqual(os.getxattr(executable, b"user.tools-test"), b"retained") + + count_file.write_text("0", encoding="ascii") + env["FAIL_COUNTS"] = "4,5" + incomplete = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable)], check=False, capture_output=True, text=True, env=env) + self.assertEqual(incomplete.returncode, 9) + self.assertIn("rollback was incomplete", incomplete.stderr) + self.assertEqual(debug.stat().st_ino, debug_inode) + self.assertEqual(debug_alias.stat().st_ino, debug_inode) + + def test_splits_disposable_elf_and_leaves_source_unchanged_on_failure(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "sample.c" + executable = root / "sample" + source.write_text("int retained_symbol(void) { return 42; }\nint main(void) { return retained_symbol(); }\n", encoding="utf-8") + subprocess.run(["cc", "-g", "-o", str(executable), str(source)], check=True) + + result = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable)], check=False, capture_output=True, text=True) + debug = root / "sample.debug" + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(executable.stat().st_mode & stat.S_IXUSR) + self.assertFalse(debug.stat().st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)) + self.assertEqual(subprocess.run([str(executable)], check=False).returncode, 42) + self.assertIn("retained_symbol", subprocess.run(["nm", str(debug)], check=True, capture_output=True, text=True).stdout) + self.assertIn(".gnu_debuglink", subprocess.run(["readelf", "--sections", str(executable)], check=True, capture_output=True, text=True).stdout) + + before = executable.read_bytes() + fake_tools = root / "fake-tools" + fake_tools.mkdir() + for tool in ("bash", "objcopy", "mktemp", "cp", "rm", "dirname", "basename", "chmod", "mv"): + os.symlink(Path("/usr/bin") / tool, fake_tools / tool) + failing_strip = fake_tools / "strip" + failing_strip.write_text("#!/bin/sh\nexit 9\n", encoding="utf-8") + failing_strip.chmod(0o755) + env = os.environ.copy() + env["PATH"] = str(fake_tools) + failed = subprocess.run([str(ROOT / "split_symbols.sh"), str(executable)], check=False, capture_output=True, text=True, env=env) + self.assertEqual(failed.returncode, 9) + self.assertEqual(executable.read_bytes(), before) + self.assertFalse(any(root.glob(".split-symbols.*"))) + + +if __name__ == "__main__": + unittest.main() diff --git a/worldviewer/config.php.sample b/worldviewer/config.php.sample deleted file mode 100644 index 3a21fda..0000000 --- a/worldviewer/config.php.sample +++ /dev/null @@ -1,53 +0,0 @@ - diff --git a/worldviewer/src/arch.php b/worldviewer/src/arch.php deleted file mode 100644 index 496b3c7..0000000 --- a/worldviewer/src/arch.php +++ /dev/null @@ -1,164 +0,0 @@ - array( - 'default' => 0, - 'type' => 'map', - ), - 'y' => array( - 'default' => 0, - 'type' => 'map', - ), - 'face' => array( - 'default' => 'bug.101', - ), - 'type' => array( - 'default' => 0, - ), - 'sys_object' => array( - 'default' => 0, - ), - 'direction' => array( - 'default' => -1, - ), - 'multi_x' => array( - 'default' => 0, - 'type' => null, - ), - 'multi_y' => array( - 'default' => 0, - 'type' => null, - ), - 'parts' => array( - 'default' => 1, - 'type' => null, - ), -); - -/** - * Initialize default values for a single archetype in the @ref $archetypes array. - * @param archname Arch name to initialize. */ -function archetype_init($archname) -{ - global $archetypes, $arch_attributes; - - $archetypes[$archname] = array(); - - foreach ($arch_attributes as $attribute => $data) - { - $archetypes[$archname][$attribute] = $data['default']; - } -} - -/** - * Adjusts an object's face by the facing direction. - * - * The @ref $animations array is used to look up animations and number - * of facings. - * @param archname Name of the arch. - * @param direction Direction the object is facing. - * @param face Fallback face name. - * @return Face name relevant to the facing direction if found, the - * fallback face name otherwise. */ -function adjust_face_by_dir($archname, $direction, $face) -{ - global $animations; - - // Sanity check - if (empty($animations[$archname])) - { - return $face; - } - - // Get the correct direction number for the anims array. - $dir_num = $direction * count($animations[$archname]['anims']) / $animations[$archname]['facings']; - - // If the direction number is set, return it. - if (!empty($animations[$archname]['anims'][$dir_num])) - { - return $animations[$archname]['anims'][$dir_num]; - } - - return $face; -} - -/** - * Recursively parse an archetype from a file, loading the archetype on - * the map, and its inventory. - * @param archname Name of the arch to consider. - * @param fp File pointer to get more lines from opened by fopen(). - * @param return If true, we return the found values instead of adding - * it to the $map array initialized by caller. Used when loading - * inventories of objects. - * @return The found values if $return is true, nothing otherwise. */ -function parse_arch_recursive($archname, $fp, $return = false) -{ - global $map, $archetypes; - - // Some defaults. - $archetype = !empty($archetypes[$archname]) ? $archetypes[$archname] : array(); - $inv = array(); - - // Loop through the lines. - while ($line = fgets($fp)) - { - // Hit an end, break out. - if ($line == 'end' . "\n") - { - break; - } - - // Another arch inside another arch means it's inside an - // inventory, so load it up. - if (sscanf($line, 'arch %s' . "\n", $inv_archname) == 1) - { - $inv[] = parse_arch_recursive($inv_archname, $fp, true); - } - - parse_archetype_line($line, &$archetype, 'map'); - } - - // If direction was set, adjust the face by the direction. Note we - // check for -1, because 0 is a valid direction. - if ($archetype['direction'] != -1) - { - $archetype['face'] = adjust_face_by_dir($archname, $archetype['direction'], $archetype['face']); - } - - // We ignore system objects which are not of type SPAWN_POINT. - if ($archetype['sys_object'] && $archetype['type'] != SPAWN_POINT) - { - return; - } - - if (!isset($map[$archetype['y']])) - { - $map[$archetype['y']] = array(); - } - - if (!isset($map[$archetype['y']][$archetype['x']])) - { - $map[$archetype['y']][$archetype['x']] = array(); - } - - $archetype += array( - 'inv' => $inv, - 'is_multi' => $archetype['parts'] > 1, - ); - - // If $return was set, return the array. - if ($return) - { - return $archetype; - } - - // Otherwise add it to the $map array initialized by caller. - $map[$archetype['y']][$archetype['x']][] = $archetype; -} - -?> diff --git a/worldviewer/src/commands.php b/worldviewer/src/commands.php deleted file mode 100644 index 248f80d..0000000 --- a/worldviewer/src/commands.php +++ /dev/null @@ -1,421 +0,0 @@ - array( - 'args' => array( - 'autocrop' => array( - 'type' => 'bool', - 'default' => 0, - 'doc' => 'Do automatic cropping of empty borders on every image. Slow.', - ), - 'start-dir' => array( - 'type' => 'string', - 'default' => '', - 'doc' => 'Starting point in maps directory. Useful for testing.', - ), - ), - 'doc' => 'Regenerate the cached map images that are used for the world viewer.', - ), - 'arch-cache' => array( - 'doc' => 'Regenerate the information parsed from arch directory files like animations, archetypes and stored in cached PHP file as arrays.', - ), - 'render-map' => array( - 'args' => array( - 'autocrop' => array( - 'type' => 'bool', - 'default' => 0, - 'doc' => 'Do automatic cropping of empty borders on the output image.', - ), - 'file' => array( - 'type' => 'string', - 'required' => true, - 'doc' => 'Path to the map file.', - ), - 'output' => array( - 'type' => 'string', - 'default' => 'output.png', - 'doc' => 'Name of the output file.', - ), - ), - 'doc' => 'Render a single map.', - ), - 'overview' => array( - 'args' => array( - 'resize' => array( - 'required' => true, - 'type' => 'range', - 'min' => 2, - 'max' => 6, - 'doc' => 'How much to resize the overview from the real size of the map, ie, twice, thrice, etc.', - ), - 'filename' => array( - 'type' => 'string', - 'default' => 'overview.png', - 'doc' => 'The overview filename.', - ), - 'no-autocrop' => array( - 'type' => 'bool', - 'default' => 0, - 'doc' => 'If set, don\'t do automatic cropping of empty borders. The cropping can be slow.', - ), - 'first-map' => array( - 'required' => true, - 'type' => 'string', - 'doc' => 'Name of the first map for the overview, from which to look for other tiled maps.', - ), - ), - 'doc' => 'Generate overview image from a bunch of tiled maps. The maps must all be properly named using the Atrinik map naming scheme.', - ), - 'help' => array( - 'doc' => 'Show usage and known commands.', - ), -); - -/** - * The map-cache command. Used to (re)generate the cached images of maps - * used for the world viewer. */ -function command_maps_cache() -{ - global $maps_path, $arguments, $maps_info; - - // If the map cache dir doesn't exist, create it. - if (!file_exists(MAP_CACHE_DIR)) - { - mkdir(MAP_CACHE_DIR); - } - // If it exists but is not a directory, complain. - elseif (!is_dir(MAP_CACHE_DIR)) - { - die('ERROR: \'' . MAP_CACHE_DIR . '\' exists, but is not a directory.' . "\n"); - } - - $path = $maps_path; - - if (!empty($arguments['start-dir'])) - { - $path .= '/' . $arguments['start-dir']; - - if (!file_exists($path) || !is_dir($path)) - { - die('ERROR: Invalid start-dir argument: \'' . $path . '\' doesn\'t exist or is not a directory.' . "\n"); - } - } - - // Maps to scan. - $to_scan = array($path); - $maps = array(); - $maps_info = array(); - - // Get the length of the maps path. - $maps_path_len = strlen($maps_path); - - // Scan all the maps. - for ($i = 0; $i < count($to_scan); $i++) - { - $file = $to_scan[$i]; - - // If this is a directory... - if (is_dir($file)) - { - // Open the directory. - $dir = opendir($file); - - // Read the directory. - while (($filename = readdir($dir)) !== false) - { - // Ignore anything starting with '.', and also ignore - // some obvious non-map files and directories. - if ($filename[0] == '.' || $filename == 'styles' || $filename == 'python' || $filename == 'Doxyfile') - { - continue; - } - - // Add it to the scan array so we can parse it. - $to_scan[] = $file . '/' . $filename; - } - - // Close the directory. - closedir($dir); - } - // Otherwise a file. - else - { - $fp = fopen($file, 'r'); - - // Try to parse the file as map - if (($map_info = parse_map_header($fp)) !== false) - { - // The substr will remove maps_path, so instead of /home/xxx/atrinik/shattered_islands/world_xxxx - // we will have shattered_islands/world_xxxx. - $maps_info[substr($file, strlen($maps_path) + 1)] = $map_info; - $maps[] = $file; - } - - fclose($fp); - } - } - - // Loop through all the maps. - foreach ($maps as $map) - { - // Make image of the map. - $map_img = make_map_image(parse_map_file($map)); - - // Get the name of the file that will be used to store the cached - // image. - $cache_map_file = MAP_CACHE_DIR . substr($map, $maps_path_len) . '.png'; - // Get the path to the file. - $cache_map_path = dirname($cache_map_file); - - // If the directory doesn't exist, recursively create it. - if (!file_exists($cache_map_path)) - { - mkdir($cache_map_path, 0750, true); - } - // If it exists but is not a directory, complain. - elseif (!is_dir($cache_map_path)) - { - die('ERROR: \'' . $cache_map_path . '\' exists but is not a directory.' . "\n"); - } - - // Save the image. - imagepng($map_img, $cache_map_file); - // Free resources. - imagedestroy($map_img); - - // If autocrop argument was set, remove empty borders from the - // image. - if ($arguments['autocrop']) - { - image_remove_empty_borders($cache_map_file); - } - - echo 'Rendered and saved map: ', $cache_map_file, "\n"; - - // Sleep for a bit. - usleep(5000); - } - - make_cache(array($maps_info), MAPS_CACHE_FILE); -} - -/** - * The arch-cache command. Regenerates the arch cache used by the - * script. */ -function command_arch_cache() -{ - global $arch_path, $archetypes, $faces, $animations; - - // Parse all the needed files. - parse_archetypes($arch_path . '/archetypes'); - parse_facetree($arch_path . '/facetree'); - parse_animations($arch_path . '/animations'); - parse_artifacts($arch_path . '/artifacts'); - - // Generate the cache. - make_cache(array($archetypes, $faces, $animations), ARCH_CACHE_FILE); -} - -/** - * Render a single map. */ -function command_render_map() -{ - global $arguments; - - if (!file_exists($arguments['file']) || !is_file($arguments['file'])) - { - die('ERROR: The file \'' . $arguments['file'] . '\' does not exist or is not a file.' . "\n"); - } - - // Make image of the map. - $map_img = make_map_image(parse_map_file($arguments['file'])); - - // Save the image. - imagepng($map_img, $arguments['output']); - // Free resources. - imagedestroy($map_img); - - // If autocrop argument was set, remove empty borders from the - // image. - if ($arguments['autocrop']) - { - image_remove_empty_borders($arguments['output']); - } - - echo 'Rendered and saved map: ', $arguments['output'], "\n"; -} - -/** - * The overview command. Generates image overview of a given mapset. */ -function command_overview() -{ - global $maps, $to_scan, $maps_path, $arguments; - - $maps = array(); - $base_filename = basename($maps_path . '/' . $arguments['first-map']); - $to_scan = array($base_filename); - $dir = dirname($maps_path . '/' . $arguments['first-map']); - - for ($i = 0; $i < count($to_scan); $i++) - { - $map = $dir . '/' . $to_scan[$i]; - - // Open the map. - $fp = fopen($map, 'r'); - - $got_map = false; - $tile_paths = array(); - - // Loop through the lines. - while ($line = fgets($fp)) - { - // Check if this is map arch, which should be the first line. - if ($line == 'arch map' . "\n") - { - $got_map = true; - } - elseif ($line == 'end' . "\n") - { - break; - } - - // We haven't got "arch map" as the first line, return an error. - if (!$got_map) - { - die('ERROR: Generating overview failed, file is not a map: ' . $map . "\n"); - } - - // Otherwise scan for tile paths. - if (sscanf($line, 'tile_path_%d %s' . "\n", $tile_path_id, $tile_path_map)) - { - $tile_paths[$tile_path_id] = $tile_path_map; - - // If it's not in the $to_scan array yet, add it there. - if (!in_array($tile_path_map, $to_scan)) - { - $to_scan[] = $tile_path_map; - } - } - } - - // Add it to the maps array. - $maps[] = basename($map); - - // Close the file. - fclose($fp); - } - - $lowest_xx = $lowest_yy = $highest_xx = $highest_yy = 1; - - $base_map_filename = substr($base_filename, 0, -5); - - // Go through the maps to detect lowest XX, YY and highest XX, YY. - foreach ($maps as $map) - { - $positions_xx = substr($map, -4, 2); - $positions_yy = substr($map, -2, 2); - - if ((int) $positions_xx > $highest_xx) - { - $highest_xx = (int) $positions_xx; - } - elseif ((int) $positions_xx < $lowest_xx) - { - $lowest_xx = (int) $positions_xx; - } - - if ((int) $positions_yy > $highest_yy) - { - $highest_yy = (int) $positions_yy; - } - elseif ((int) $positions_yy < $lowest_yy) - { - $lowest_yy = (int) $positions_yy; - } - } - - // Determine sizes. - $size_x = (MAP_TILE_POS_XOFF * (24 + 1) * ($highest_xx > $highest_yy ? $highest_xx : $highest_yy)) / $arguments['resize']; - $size_xpos = $size_x / 2; - $size_y = (MAP_TILE_POS_YOFF * (24 + 3) * ($highest_yy > $highest_xx ? $highest_yy : $highest_xx)) / $arguments['resize']; - $size_ypos = MAP_TILE_POS_YOFF * 2; - - // Create a new image. - $world_img = imagecreatetruecolor($size_x, $size_y); - - // Turn on alpha blending and save alpha. - imagealphablending($world_img, true); - imagesavealpha($world_img, true); - - // Fill the image with transparency. - imagefill($world_img, 0, 0, IMG_COLOR_TRANSPARENT); - - // Go through all the maps, starting from the top left one. - for ($y = $highest_yy; $y >= $lowest_yy; $y--) - { - for ($x = $lowest_xx; $x <= $highest_xx; $x++) - { - // Get the map name. - $map = $dir . '/' . $base_map_filename . '_' . ($x < 10 ? '0' . $x : $x) . ($y < 10 ? '0' . $y : $y); - - // If it doesn't exist, continue. - if (!file_exists($map) || !is_file($map)) - { - continue; - } - - // Make an image of the map. - $map_img = make_map_image(parse_map_file($map)); - - // Get width and height. - $width = imagesx($map_img); - $height = imagesy($map_img); - - // Calculate new width and height. - $newwidth = $width / $arguments['resize'] + 2; - $newheight = $height / $arguments['resize'] + 2; - - // Calculate X and Y positions. - $xpos = $size_xpos + ((($x - 1) * MAP_TILE_XOFF * MAP_TILE_XOFF * 4 - ($highest_yy - $y + 1) * MAP_TILE_YOFF * MAP_TILE_YOFF)) / $arguments['resize']; - $ypos = ($size_ypos + (($highest_yy - $y) * MAP_TILE_YOFF * (MAP_TILE_YOFF / 2)) + (($x - 1) * MAP_TILE_XOFF * MAP_TILE_XOFF * 2)) / $arguments['resize']; - - // Copy the image. - imagecopyresampled($world_img, $map_img, $xpos, $ypos, 0, 0, $newwidth, $newheight, $width, $height); - // Free resources. - imagedestroy($map_img); - - echo 'Rendered and copied image of map: ', $base_map_filename . '_' . ($x < 10 ? '0' . $x : $x) . ($y < 10 ? '0' . $y : $y), "\n"; - - // Sleep for a bit. - usleep(5000); - } - } - - // Save the image. - imagepng($world_img, $arguments['filename']); - // Free resources. - imagedestroy($world_img); - - // If no-autocrop argument was not set, remove empty borders from the - // overview image. - if (!$arguments['no-autocrop']) - { - image_remove_empty_borders($arguments['filename']); - } -} - -/** - * The help command. Used to show the usage of the script. */ -function command_help() -{ - show_usage(); -} - -?> diff --git a/worldviewer/src/main.php b/worldviewer/src/main.php deleted file mode 100644 index 9d73abd..0000000 --- a/worldviewer/src/main.php +++ /dev/null @@ -1,171 +0,0 @@ - $commands[$command]['args'][$arg]['max']) - { - show_usage(); - die('ERROR: Argument ' . $argv[$i - 1] . ' must be between ' . $commands[$command]['args'][$arg]['min'] . ' and ' . $commands[$command]['args'][$arg]['max'] . '.' . "\n"); - } - - $arguments[$arg] = $argv[$i]; - } - - break; - - // Bool, just set the argument to the opposite of the default. - case 'bool': - $arguments[$arg] = !$commands[$command]['args'][$arg]['default']; - break; - } - } - // Otherwise show an error. - else - { - show_usage(); - die('ERROR: Unknown argument for command ' . $command . ': ' . $argv[$i] . "\n"); - } - } - } - - // No valid command? - if (empty($command)) - { - show_usage(); - die('ERROR: Unknown command.' . "\n"); - } - - // If we've got arguments for this command, loop through them so we - // can set the defaults, and check required ones. - if (!empty($commands[$command]['args'])) - { - foreach ($commands[$command]['args'] as $arg => $arg_values) - { - if (!isset($arguments[$arg])) - { - // Required command not set, show an error. - if (isset($arg_values['required']) && $arg_values['required']) - { - show_usage(); - die('ERROR: Command requires argument \'' . $arg . '\' but it is not set. ' . "\n"); - } - // Otherwise set the default. - else - { - $arguments[$arg] = $arg_values['default']; - } - } - } - } - - // And finally, call the command's function. - call_user_func('command_' . str_replace('-', '_', $command)); -} - -/** - * Show the usage of the script when running from CLI, and, for example, - * an unknown command was entered, or the help command was used. */ -function show_usage() -{ - global $commands, $argv; - - // Show the usage. - echo 'Usage: ./', $argv[0], ' [command-arguments]', "\n\n"; - - echo 'Known commands:', "\n\n"; - - // And the known commands. - foreach ($commands as $command_name => $command) - { - echo ' ', $command_name, ': ', $command['doc'], "\n"; - - // And arguments for the known commands as well. - if (!empty($command['args'])) - { - echo ' Arguments:', "\n"; - - foreach ($command['args'] as $arg_name => $arg) - { - echo ' --', $arg_name, ': ', $arg['doc'], "\n"; - } - } - - echo "\n"; - } - - echo "\n"; -} - -?> diff --git a/worldviewer/src/parser.php b/worldviewer/src/parser.php deleted file mode 100644 index cc74265..0000000 --- a/worldviewer/src/parser.php +++ /dev/null @@ -1,369 +0,0 @@ - $path) - { - if (substr($file, -4) != '.png') - { - continue; - } - - $faces[substr($file, 0, -4)] = $path; - } -} - -/** - * Parse the animations file, adding the entries to the @ref $animations - * array. - * @param file The file to parse. */ -function parse_animations($file) -{ - global $animations; - - // Open the file. - $fp = fopen($file, 'r'); - - // Loop through the lines. - while ($line = fgets($fp)) - { - // If this is a start of an animation. - if (sscanf($line, 'anim %s' . "\n", $animname) == 1) - { - // Value defaults. - $facings = 1; - - // Initialize the defaults for this animation. - $animations[$animname] = array( - 'facings' => $facings, - 'anims' => array(), - ); - - // Loop through more lines. - while ($line = fgets($fp)) - { - // If we hit the end of the animation, break out. - if ($line == 'mina' . "\n") - { - break; - } - - // Parse the lines. - if (sscanf($line, 'facings %d' . "\n", $facings) == 1) - { - $animations[$animname]['facings'] = $facings; - } - else - { - $animations[$animname]['anims'][] = substr($line, 0, -1); - } - } - } - } - - // Close the file. - fclose($fp); -} - -/** - * Parse a single archetype line, either from the archetypes file or from map. - * @param line The line to parse. - * @param[out] archetype Archetype's data array that will contain the information from - * line, if successfully parsed. - * @param type Type, marks where the line is from. */ -function parse_archetype_line($line, $archetype, $type = 'archetypes') -{ - global $arch_attributes; - - foreach ($arch_attributes as $attribute => $data) - { - if (isset($data['type']) && ($data['type'] === null || $data['type'] != $type)) - { - continue; - } - - $value_type = '%d'; - - if (is_string($data['default'])) - { - $value_type = '%s'; - } - - if (sscanf($line, $attribute . ' ' . $value_type . "\n", $archetype[$attribute]) == 1) - { - break; - } - } -} - -/** - * Parse the archetypes file into the @ref $archetypes array. - * @param file The file. */ -function parse_archetypes($file) -{ - global $archetypes; - - // Indicates whether there is more to the arch. - $is_more = false; - // Name of the last arch parsed. - $last_archname = ''; - - // Open the file. - $fp = fopen($file, 'r'); - - // Loop through the lines. - while ($line = fgets($fp)) - { - // Is there more to the last archetype? - if ($line == 'More' . "\n") - { - $is_more = true; - } - // Otherwise it's start of the archetype. - elseif (sscanf($line, 'Object %s' . "\n", $archname) == 1) - { - archetype_init($archname); - - // If there is more to the archetype, increase number of - // parts the last archname had. - if ($is_more && !empty($last_archname)) - { - $archetypes[$last_archname]['parts']++; - } - - // Loop through more lines. - while ($line = fgets($fp)) - { - // If we hit the end, break out. - if ($line == 'end' . "\n") - { - break; - } - - parse_archetype_line($line, &$archetypes[$archname]); - - // X or Y being set in the archetype indicates it's a - // part of a multi archetype. - if (sscanf($line, 'x %d' . "\n", $multi_x)) - { - $archetypes[$last_archname]['multi_x'] = $multi_x; - } - elseif (sscanf($line, 'y %d' . "\n", $multi_y)) - { - $archetypes[$last_archname]['multi_y'] = $multi_y; - } - } - - // We ignore archetypes that are just information for the - // main multi archetype. - if ($is_more) - { - $is_more = false; - continue; - } - - // Set the last archname. - $last_archname = $archname; - } - } - - // Close the file. - fclose($fp); -} - -/** - * Parse the artifacts file. - * - * Entries in the artifacts file are parsed, and artifacts are added to - * the @ref $archetypes array. - * @param file The artifacts file to parse. */ -function parse_artifacts($file) -{ - global $archetypes; - - // Open the file. - $fp = fopen($file, 'r'); - - // Loop through the lines. - while ($line = fgets($fp)) - { - // Ignore comments and empty lines. - if ($line[0] == '#' || $line[0] == "\n") - { - continue; - } - - // Every artifacts starts with 'Allowed %s'. - if (sscanf($line, 'Allowed %s' . "\n", $allowed) == 1) - { - $artifact_name = $def_arch = $face = ''; - $type = -1; - - // Loop through more lines. - while ($line = fgets($fp)) - { - // If we hit, break out. - if ($line == 'end' . "\n") - { - break; - } - - // Scan for the needed values. - sscanf($line, 'artifact %s', $artifact_name); - sscanf($line, 'def_arch %s', $def_arch); - sscanf($line, 'face %s', $face); - sscanf($line, 'type %d', $type); - } - - // Sanity check. - if (empty($artifact_name) || empty($def_arch)) - { - continue; - } - - // First load the defaults, and then replace with any custom - // values in the artifacts file. - $archetypes[$artifact_name] = $archetypes[$def_arch]; - - if ($type != -1) - { - $archetypes[$artifact_name]['type'] = $type; - } - - if (!empty($face)) - { - $archetypes[$artifact_name]['face'] = $face; - } - } - } - - // Close the file. - fclose($fp); -} - -/** - * Parse a single map file. - * @param file File where to load the map stuff from. - * @return An array containing all the objects on the map. */ -function parse_map_file($file) -{ - global $archetypes, $map; - - // Open the file. - $fp = fopen($file, 'r'); - - $got_map = $got_map_end = false; - - // Initialize our array where objects will be stored. - $map = array(); - - // Loop through the lines. - while ($line = fgets($fp)) - { - // Check if this is map arch, which should be the first line. - if ($line == 'arch map' . "\n") - { - $got_map = true; - } - // If we are inside the map, and we haven't hit the end yet until - // now, set it. - elseif ($got_map && !$got_map_end && $line == 'end' . "\n") - { - $got_map_end = true; - continue; - } - - // We haven't got "arch map" as the first line, so break out. - if (!$got_map) - { - break; - } - - // We still haven't got a map end, so continue to the next line. - if (!$got_map_end) - { - continue; - } - - // If this is a new arch, call the parsing function. - if (sscanf($line, 'arch %s' . "\n", $archname) == 1) - { - parse_arch_recursive($archname, $fp); - } - } - - // Close the file. - fclose($fp); - - // Return the objects. - return $map; -} - -/** - * Attempt to parse file as map and load its header. - * @param $fp File pointer to read from. - * @return False if we failed to load the map header, the map header as - * an array otherwise. */ -function parse_map_header($fp) -{ - $got_map = false; - $map_info = array(); - $msg_buf = ''; - $in_msg = false; - - while ($line = fgets($fp)) - { - if ($line == 'arch map' . "\n") - { - $got_map = true; - continue; - } - elseif ($line == 'end' . "\n") - { - return $map_info; - } - - if (!$got_map) - { - return false; - } - - if ($line == 'msg' . "\n") - { - $in_msg = true; - } - elseif ($line == 'endmsg' . "\n") - { - $map_info['msg'] = substr($msg_buf, 0, -1); - $in_msg = false; - } - elseif ($in_msg) - { - $msg_buf .= $line; - } - else - { - $space_pos = strpos($line, ' '); - $attribute = substr($line, 0, $space_pos); - $value = substr($line, $space_pos + 1, -1); - - $map_info[$attribute] = $value; - } - } - - return false; -} - -?> diff --git a/worldviewer/src/renderer.php b/worldviewer/src/renderer.php deleted file mode 100644 index 308a6e2..0000000 --- a/worldviewer/src/renderer.php +++ /dev/null @@ -1,212 +0,0 @@ - 1) - { - // Now it won't be treated as multi arch in the above - // if anymore. - $object['parts'] = 1; - - // For multi arch objects with one or more X, adjust - // the resulting xpos. - if ($object['multi_x'] >= 1 && $object['multi_y'] >= 0) - { - if ($img_y > MAP_TILE_POS_YOFF) - { - // Should work for about any multi arch - // object. - $object['xpos'] = MAP_TILE_POS_XOFF2 * $object['multi_x']; - } - } - - // Move it further down in the array to be processed - // later, so it can be rendered properly. - $map_array[$y + $object['multi_y']][$x + $object['multi_x']][] = $object; - } - else - { - // Calculate positions. - $xpos = $size_xpos + $x * MAP_TILE_YOFF - $y * MAP_TILE_YOFF; - $ypos = (($size_ypos + $x * MAP_TILE_XOFF + $y * MAP_TILE_XOFF) + MAP_TILE_POS_YOFF) - $img_y; - - // If this is not a multi arch, adjust $xpos - // depending on the size of the image. - if (!$object['is_multi'] && $img_x > MAP_TILE_POS_XOFF) - { - $xpos -= ($img_x - MAP_TILE_POS_XOFF) / 2; - } - - // If the object had xpos set, subtract it from the - // calculated $xpos. - if (!empty($object['xpos'])) - { - $xpos -= $object['xpos']; - } - - // Copy the temporary image on the big image. - imagecopy($img, $img_face, $xpos, $ypos, 0, 0, $img_x, $img_y); - - // If this is a wall and the setting is set, we'll - // do double drawing of it. - if ($object['type'] == WALL && DRAW_DOUBLE_WALLS) - { - imagecopy($img, $img_face, $xpos, $ypos - 22, 0, 0, $img_x, $img_y); - } - } - - // Free the temporary image. - imagedestroy($img_face); - } - } - } - - // Return the image for further processing. - return $img; -} - -/** - * Remove empty borders from a specified image file. Similar to GIMP's - * autocrop functionality. - * @note This function is fairly slow, since it goes through every single - * pixel of the image to detect if it's empty or not. - * @warning Only works on PNGs. - * @param filename Name of the image to remove empty borders from. */ -function image_remove_empty_borders($filename) -{ - $imageinfo = getimagesize($filename); - $height = $newStartX = $imageinfo[1]; - $width = $newStartY = $imageinfo[0]; - $newStopX = $newStopY = 0; - $palette = $palette_num = 0; - $img = imagecreatefrompng($filename); - - // Go through the image to get color palette. - for ($i = 0; $i < $width; $i++) - { - for ($ii = 0; $ii < $height; $ii++) - { - $color = imagecolorat($img, $i, $ii); - $palette += $color; - $palette_num++; - } - } - - // Get the peak color. - $peak_color = round($palette / $palette_num) * 0.95; - unset($palette); - - // Go through the image again, this time to detect the empty pixels. - for ($i = 0; $i < $width; $i++) - { - for ($ii = 0; $ii < $height; $ii++) - { - if (imagecolorat($img, $i, $ii) < $peak_color) - { - if ($i > $newStopX) - { - $newStopX = $i; - } - - if ($ii > $newStopY) - { - $newStopY = $ii; - } - - if ($i < $newStartX) - { - $newStartX = $i; - } - - if ($ii < $newStartY) - { - $newStartY = $ii; - } - } - } - } - - // Create a new image of size based on where to crop the original. - $cropped = imagecreatetruecolor($newStopX - $newStartX, $newStopY - $newStartY); - - // Turn on alpha blending and save alpha. - imagealphablending($cropped, true); - imagesavealpha($cropped, true); - - // Fill the image with transparency. - imagefill($cropped, 0, 0, IMG_COLOR_TRANSPARENT); - - // Now copy the original, adjusting the positions where empty - // borders are so they don't get copied. - imagecopyresampled($cropped, $img, 0, 0, $newStartX, $newStartY, $newStopX - $newStartX, $newStopY - $newStartY, $newStopX - $newStartX, $newStopY - $newStartY); - - // Save the image. - imagepng($cropped, $filename); -} - -?> diff --git a/worldviewer/src/utils.php b/worldviewer/src/utils.php deleted file mode 100644 index 2af4cd8..0000000 --- a/worldviewer/src/utils.php +++ /dev/null @@ -1,129 +0,0 @@ - $value) - { - // If the key is not empty, write it out. - if (!empty($key)) - { - fwrite($fp, '\'' . $key . '\'=>'); - } - - // Write out the values, depending on their type. - if (is_string($value)) - { - fwrite($fp, '\'' . addslashes($value) . '\','); - } - elseif (is_int($value) || is_float($value)) - { - fwrite($fp, $value . ','); - } - elseif (is_bool($value)) - { - fwrite($fp, ($value ? 'true' : 'false') . ','); - } - elseif (is_array($value)) - { - fwrite($fp, 'array('); - array_to_file($value, $fp); - fwrite($fp, '),'); - } - } -} - -/** - * Write arrays to file. - * @param cache Array of arrays to write. - * @param cache_file File name to write. */ -function make_cache($cache, $cache_file) -{ - // Open the file. - $fp = fopen($cache_file, 'w'); - - // Write out PHP start tag. - fwrite($fp, ' $glob_arr) - { - if ($glob_arr == $cache_array) - { - $array_name = $glob; - break; - } - } - - // If it's empty at this point, something is very wrong. - if (empty($array_name)) - { - echo 'ERROR: Could not find array\'s name in $GLOBALS.', "\n"; - continue; - } - - // Write out the variable name of the array, and the array - // initializer. - fwrite($fp, '$' . $array_name . ' = array('); - // Write out all the array values. - array_to_file($cache_array, $fp); - // Close the array. - fwrite($fp, ');'); - } - - // Close the PHP tag. - fwrite($fp, ' ?>'); - - // Close the file. - fclose($fp); -} - -?> diff --git a/worldviewer/src/viewer.php b/worldviewer/src/viewer.php deleted file mode 100644 index f66349b..0000000 --- a/worldviewer/src/viewer.php +++ /dev/null @@ -1,141 +0,0 @@ - - - - - Atrinik World Viewer - - - -
Type(s)Description
\n"); - - for (type = 0; type < attribute->types[desc]->count; type++) { - if (type < attribute->types[desc]->count - 1) { - end = ", "; - } else { - end = "\n"; - } - - fprintf(file, "@ref page_type_%d%s", attribute->types[desc]->number[type], end); - } - - fprintf(file, - "\t\t%s
- - - - -
- - '; - - // Ouput the compass. - for ($i = 0; $i < 9; $i++) - { - if ($i && !($i % 3)) - { - echo ' - '; - } - - switch ($i) - { - case 0: - $tile_id = 4; - $classname = 'topleft'; - break; - - case 1: - $tile_id = 8; - $classname = 'top'; - break; - - case 2: - $tile_id = 1; - $classname = 'topright'; - break; - - case 3: - $tile_id = 7; - $classname = 'left'; - break; - - case 4: - $classname = ''; - break; - - case 5: - $tile_id = 5; - $classname = 'right'; - break; - - case 6: - $tile_id = 3; - $classname = 'bottomleft'; - break; - - case 7: - $tile_id = 6; - $classname = 'bottom'; - break; - - case 8: - $tile_id = 2; - $classname = 'bottomright'; - break; - } - - if (!empty($classname)) - { - $map_exists = isset($map_info['tile_path_' . $tile_id]) && file_exists(MAP_CACHE_DIR . '/' . dirname($map) . '/' . $map_info['tile_path_' . $tile_id] . '.png'); - - echo ''; - } - else - { - echo ''; - } - } - - - echo ' - -
', $map_exists ? '' : '', '
-
- -
- -'; -} - -?> diff --git a/worldviewer/world.css b/worldviewer/world.css deleted file mode 100644 index 53d26be..0000000 --- a/worldviewer/world.css +++ /dev/null @@ -1,126 +0,0 @@ -/* The main body. */ -html, body -{ - font: 12px verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif; - margin: 0; -} - -/* No borders for linked images. */ -a img -{ - border: none; -} - -/* Every table cell in the compass should have at least 46 pixels width and height. */ -#compass td -{ - width: 46px; - height: 46px; - min-height: 46px; - min-width: 46px; -} - -/* Change background of the table cell on hover. */ -#compass td.enabled:hover -{ - background: url('compass-hover.png'); -} - -/* Common style for a link inside the compass. */ -#compass a -{ - display: block; - height: 100%; - width: 100%; - text-decoration: none; -} - -/* Styles for every direction link on the compass. */ -#compass td.left a -{ - background: url('arrows.png') 0 0 no-repeat; -} - -#compass td.bottom a -{ - background: url('arrows.png') 0 -49px no-repeat; -} - -#compass td.right a -{ - background: url('arrows.png') 0 -98px no-repeat; -} - -#compass td.top a -{ - background: url('arrows.png') 0 -147px no-repeat; -} - -#compass td.bottomleft a -{ - background: url('arrows.png') 0 -196px no-repeat; -} - -#compass td.bottomright a -{ - background: url('arrows.png') 0 -245px no-repeat; -} - -#compass td.topright a -{ - background: url('arrows.png') 0 -294px no-repeat; -} - -#compass td.topleft a -{ - background: url('arrows.png') 0 -340px no-repeat; -} - -/* Styles for every disabled direction on the compass. */ -#compass td.left.disabled -{ - background: url('arrows.png') -48px 1px no-repeat; - height: 50px; -} - -#compass td.bottom.disabled -{ - background: url('arrows.png') -48px -48px no-repeat; - height: 50px; -} - -#compass td.right.disabled -{ - background: url('arrows.png') -48px -97px no-repeat; - height: 50px; -} - -#compass td.top.disabled -{ - background: url('arrows.png') -48px -146px no-repeat; - height: 50px; -} - -#compass td.bottomleft.disabled -{ - background: url('arrows.png') -48px -195px no-repeat; - height: 50px; -} - -#compass td.bottomright.disabled -{ - background: url('arrows.png') -48px -244px no-repeat; - height: 50px; -} - -#compass td.topright.disabled -{ - background: url('arrows.png') -48px -293px no-repeat; - height: 50px; -} - -#compass td.topleft.disabled -{ - background: url('arrows.png') -48px -339px no-repeat; - height: 50px; -} diff --git a/worldviewer/world.php b/worldviewer/world.php deleted file mode 100644 index 4e7adce..0000000 --- a/worldviewer/world.php +++ /dev/null @@ -1,71 +0,0 @@ -