open-dragoman 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_dragoman-0.2.0/.gitignore +31 -0
- open_dragoman-0.2.0/CHANGELOG.md +118 -0
- open_dragoman-0.2.0/CMakeLists.txt +170 -0
- open_dragoman-0.2.0/LICENSE +43 -0
- open_dragoman-0.2.0/PKG-INFO +261 -0
- open_dragoman-0.2.0/README.md +196 -0
- open_dragoman-0.2.0/VERSION +1 -0
- open_dragoman-0.2.0/bindings/c/example.c +76 -0
- open_dragoman-0.2.0/bindings/cpp/example.cpp +58 -0
- open_dragoman-0.2.0/bindings/python/dragoman/__init__.py +348 -0
- open_dragoman-0.2.0/bindings/python/dragoman/__main__.py +139 -0
- open_dragoman-0.2.0/bindings/python/dragoman/_ffi.py +129 -0
- open_dragoman-0.2.0/bindings/python/dragoman/_version.py +1 -0
- open_dragoman-0.2.0/docs/abi.md +89 -0
- open_dragoman-0.2.0/docs/mapping.md +289 -0
- open_dragoman-0.2.0/docs/model.md +105 -0
- open_dragoman-0.2.0/docs/roundtrip.md +79 -0
- open_dragoman-0.2.0/docs/scripting.md +104 -0
- open_dragoman-0.2.0/docs/versioning.md +61 -0
- open_dragoman-0.2.0/include/dragoman/dragoman.h +195 -0
- open_dragoman-0.2.0/include/dragoman/dragoman.hpp +159 -0
- open_dragoman-0.2.0/pyproject.toml +89 -0
- open_dragoman-0.2.0/src/Api.cpp +463 -0
- open_dragoman-0.2.0/src/Common.cpp +523 -0
- open_dragoman-0.2.0/src/Formats.h +124 -0
- open_dragoman-0.2.0/src/Gd5Map.cpp +1222 -0
- open_dragoman-0.2.0/src/IsoTable.inc +263 -0
- open_dragoman-0.2.0/src/Model.h +208 -0
- open_dragoman-0.2.0/src/ModelJson.cpp +306 -0
- open_dragoman-0.2.0/src/ModelJson.h +14 -0
- open_dragoman-0.2.0/src/OdMap.cpp +741 -0
- open_dragoman-0.2.0/src/Raster.cpp +397 -0
- open_dragoman-0.2.0/src/Raster.h +132 -0
- open_dragoman-0.2.0/src/Scripts.cpp +543 -0
- open_dragoman-0.2.0/src/Support.cpp +222 -0
- open_dragoman-0.2.0/src/Support.h +85 -0
- open_dragoman-0.2.0/src/Zip.cpp +134 -0
- open_dragoman-0.2.0/src/Zip.h +45 -0
- open_dragoman-0.2.0/tests/CMakeLists.txt +33 -0
- open_dragoman-0.2.0/tests/Check.h +69 -0
- open_dragoman-0.2.0/tests/Fixture.h +127 -0
- open_dragoman-0.2.0/tests/test_abi.cpp +157 -0
- open_dragoman-0.2.0/tests/test_python.py +127 -0
- open_dragoman-0.2.0/tests/test_raster.cpp +194 -0
- open_dragoman-0.2.0/tests/test_roundtrip.cpp +382 -0
- open_dragoman-0.2.0/tests/test_scripts.cpp +211 -0
- open_dragoman-0.2.0/tests/test_version.cpp +62 -0
- open_dragoman-0.2.0/third_party/json/json.hpp +24765 -0
- open_dragoman-0.2.0/third_party/miniz/miniz.c +646 -0
- open_dragoman-0.2.0/third_party/miniz/miniz.h +615 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_common.h +89 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_export.h +2 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_tdef.c +1602 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_tdef.h +199 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_tinfl.c +778 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_tinfl.h +150 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_zip.c +4895 -0
- open_dragoman-0.2.0/third_party/miniz/miniz_zip.h +454 -0
- open_dragoman-0.2.0/third_party/stb/stb_image.h +7988 -0
- open_dragoman-0.2.0/third_party/stb/stb_image_write.h +1724 -0
- open_dragoman-0.2.0/third_party/stb/stb_impl.c +17 -0
- open_dragoman-0.2.0/tools/check_version.py +79 -0
- open_dragoman-0.2.0/tools/conformance.py +78 -0
- open_dragoman-0.2.0/tools/dragoman_cli.cpp +194 -0
- open_dragoman-0.2.0/tools/generate_iso_table.py +70 -0
- open_dragoman-0.2.0/tools/publish_wiki.py +136 -0
- open_dragoman-0.2.0/wiki/Diagnostics.md +91 -0
- open_dragoman-0.2.0/wiki/Getting-Started.md +99 -0
- open_dragoman-0.2.0/wiki/Home.md +71 -0
- open_dragoman-0.2.0/wiki/Installing.md +119 -0
- open_dragoman-0.2.0/wiki/Languages.md +118 -0
- open_dragoman-0.2.0/wiki/README.md +41 -0
- open_dragoman-0.2.0/wiki/Round-Trips.md +94 -0
- open_dragoman-0.2.0/wiki/Scripts-and-Events.md +95 -0
- open_dragoman-0.2.0/wiki/Troubleshooting.md +105 -0
- open_dragoman-0.2.0/wiki/What-Crosses.md +120 -0
- open_dragoman-0.2.0/wiki/_Sidebar.md +14 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
build/
|
|
2
|
+
build-*/
|
|
3
|
+
cmake-build-*/
|
|
4
|
+
out/
|
|
5
|
+
.cache/
|
|
6
|
+
compile_commands.json
|
|
7
|
+
|
|
8
|
+
*.o
|
|
9
|
+
*.a
|
|
10
|
+
*.so
|
|
11
|
+
*.so.*
|
|
12
|
+
*.dylib
|
|
13
|
+
*.dll
|
|
14
|
+
*.lib
|
|
15
|
+
*.exp
|
|
16
|
+
*.pdb
|
|
17
|
+
|
|
18
|
+
__pycache__/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
dist/
|
|
21
|
+
.venv/
|
|
22
|
+
|
|
23
|
+
.DS_Store
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
|
|
27
|
+
# Maps used for local conformance runs. Neither game's maps belong in this
|
|
28
|
+
# repository -- Open Doctrines' are its own and GD5's are GPL -- so they are
|
|
29
|
+
# pointed at from outside instead. See docs/roundtrip.md.
|
|
30
|
+
maps/
|
|
31
|
+
*.odmap
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
Versioning: [semver](https://semver.org/), with a separate ABI version — see
|
|
5
|
+
[docs/versioning.md](docs/versioning.md).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] — 2026-08-15
|
|
8
|
+
|
|
9
|
+
Both changes here came from opening converted maps in the games themselves and
|
|
10
|
+
looking at what was wrong.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Sea provinces are invented when the source game does not draw any.** Open
|
|
15
|
+
Doctrines leaves its oceans unpainted; GD5 can neither render nor sail across
|
|
16
|
+
what is not a province, so a converted map arrived with a black sea and no
|
|
17
|
+
fleet could move.
|
|
18
|
+
|
|
19
|
+
The water is **grown** into provinces, not cut: seeds are laid on a lattice,
|
|
20
|
+
displaced by a hash of their own coordinates so the shapes are irregular but
|
|
21
|
+
a map still converts identically twice, snapped to the nearest water, and
|
|
22
|
+
grown outwards all at once through water only. Each province is the water
|
|
23
|
+
nearest one seed, so it follows the coast — 0.56 on bounding-box fill against
|
|
24
|
+
the 0.66 of GD5's own hand-drawn ones, where a grid would be 1.00. A province
|
|
25
|
+
cannot cross land, so the Mediterranean and the Atlantic are separate however
|
|
26
|
+
close two seeds fall.
|
|
27
|
+
|
|
28
|
+
The world map gets 917: one network of 747 that is every ocean joined
|
|
29
|
+
together, and 163 lakes. The ids are recorded in the sidecar and deleted
|
|
30
|
+
again on the way back, so the round trip is unaffected. New
|
|
31
|
+
`synthesise_ocean` option, default on.
|
|
32
|
+
|
|
33
|
+
- **Research is derived from the map's date.** Open Doctrines stores none in a
|
|
34
|
+
map — its tree is compiled into the game and seeded from a hardcoded list of
|
|
35
|
+
ISO codes — so converted maps reached GD5 with every nation at level zero in
|
|
36
|
+
everything. GD5 already knows how to turn a year into research levels, so
|
|
37
|
+
that rule is applied against the tech tree read out of the GD5 installation
|
|
38
|
+
being written into, rather than a copy kept here that would go stale. Output
|
|
39
|
+
matches GD5's own `get_time_appropriate_research` exactly for 1914 and 2000.
|
|
40
|
+
Nations that arrive with research keep it.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **Flags now render in GD5.** `flag_data` is not base64 of a PNG, which is
|
|
45
|
+
what this library was writing; it is base64 of *raw pixel bytes* at exactly
|
|
46
|
+
60x40, handed to `pygame.image.fromstring`. A PNG makes that raise,
|
|
47
|
+
`decode_b64_to_surf` swallows the exception, and every nation showed a blank
|
|
48
|
+
white rectangle. Flags are now decoded, resampled to 60x40 and written as raw
|
|
49
|
+
pixels, and the original full-size image is preserved in the sidecar so a
|
|
50
|
+
crossing does not permanently shrink it to GD5's icon size.
|
|
51
|
+
- **One rule for "does this map draw its water"**, shared by both writers,
|
|
52
|
+
replacing two that disagreed. Deciding it by whether any province is marked
|
|
53
|
+
sea was wrong in both directions: Open Doctrines maps have a handful of
|
|
54
|
+
coastal provinces that sit mostly under the mask, so no ocean was ever
|
|
55
|
+
synthesised, while GD5 maps with lots of unpainted border did not get those
|
|
56
|
+
borders filled. It is now a pixel count, and the two populations are not
|
|
57
|
+
close — GD5's maps run 0.35 to 7.8 on the ratio, Open Doctrines' all sit at
|
|
58
|
+
0.0001.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **ABI version 2.** `dg_options` gained a field, which changes the struct's
|
|
63
|
+
size, so anything compiled against ABI 1 must be rebuilt. `SOVERSION` moves
|
|
64
|
+
with it.
|
|
65
|
+
|
|
66
|
+
## [0.1.0] — 2026-08-15
|
|
67
|
+
|
|
68
|
+
First release. Converts maps between Open Doctrines `.odmap` archives and
|
|
69
|
+
Greater Diplomacy 5 map directories, in both directions.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **Both readers and writers**, against a shared interchange model rather than
|
|
74
|
+
directly between the two layouts.
|
|
75
|
+
- **Lossless round trips.** Everything the destination format cannot hold is
|
|
76
|
+
written into a sidecar beside the map — a directory both games ignore — and
|
|
77
|
+
restored on the way back. `dragoman roundtrip` asserts it; `--no-sidecar`
|
|
78
|
+
turns it off for a smaller, deliberately lossy conversion.
|
|
79
|
+
- **Derived geometry.** Province adjacency, centroids, coastal flags and the
|
|
80
|
+
land/sea mask are computed from the province raster, because Open Doctrines
|
|
81
|
+
derives all of them at load and GD5 requires them stored. The centroid finder
|
|
82
|
+
handles crescent-shaped provinces, whose mean pixel falls outside themselves.
|
|
83
|
+
- **Script translation** in both directions for the shared subset — a gate and
|
|
84
|
+
the things that happen when it opens. Constructs outside it are reported by
|
|
85
|
+
name and carried verbatim. GD5 event types the library does not model pass
|
|
86
|
+
through unchanged, so GD5 → OD → GD5 is lossless for all of them.
|
|
87
|
+
- **C99 ABI** with a C++ RAII wrapper, a ctypes Python package needing no
|
|
88
|
+
compiler, and a `dragoman` command line tool.
|
|
89
|
+
- **CI** across Linux, macOS and Windows, building, testing, and importing and
|
|
90
|
+
exercising the Python binding on each — a symbol that is not exported passes
|
|
91
|
+
every C++ test and fails at the first `dlsym`.
|
|
92
|
+
- **Version consistency checks**, from inside the build and from a standalone
|
|
93
|
+
script, over `VERSION`, the C header, the Python package and CMake.
|
|
94
|
+
|
|
95
|
+
### Verified
|
|
96
|
+
|
|
97
|
+
Every map both games ship round-trips with no modelled field lost: 28 of 28
|
|
98
|
+
Greater Diplomacy 5 maps (12 base maps, 16 scenarios) and 6 of 6 Open Doctrines
|
|
99
|
+
maps, plus the unit suite. Reproduce with `tools/conformance.py`.
|
|
100
|
+
|
|
101
|
+
Facts established against the real data, each of which cost a bug first:
|
|
102
|
+
|
|
103
|
+
- Province ids survive exactly. Open Doctrines packs an id into a pixel
|
|
104
|
+
big-endian and GD5 little-endian, verified across all 1248 and 2523 provinces
|
|
105
|
+
of the two games' maps with no exception, so the rasters differ only by
|
|
106
|
+
swapping red and blue.
|
|
107
|
+
- A GD5 nation's identity is its key in `nation_data`, not its `name` field. The
|
|
108
|
+
1914 scenario has two separate nations both named "German Empire"; treating
|
|
109
|
+
the name as the identity merged them and handed 121 provinces to nobody.
|
|
110
|
+
- `UNC` / "Unclaimed" is a real country in both games, not a blank — 104
|
|
111
|
+
provinces of Open Doctrines' 1914 map belong to it.
|
|
112
|
+
- A nation's claims and a province's cores are different facts in GD5. Merging
|
|
113
|
+
them invented cores and discarded claims.
|
|
114
|
+
- Open Doctrines does not divide water into provinces at all, so a map crossing
|
|
115
|
+
to GD5 arrives with nothing to sail on. Reported as `gd5.nosea` rather than
|
|
116
|
+
passed over in silence.
|
|
117
|
+
|
|
118
|
+
[0.1.0]: https://github.com/Pr1nted/dragoman/releases/tag/v0.1.0
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.16)
|
|
2
|
+
|
|
3
|
+
# The version lives in one file, is read here, and is checked against the C
|
|
4
|
+
# header and the Python package by tests/test_version.cpp and the CI job. A
|
|
5
|
+
# release that forgets one of the three fails before it is tagged.
|
|
6
|
+
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" DRAGOMAN_VERSION_RAW)
|
|
7
|
+
string(STRIP "${DRAGOMAN_VERSION_RAW}" DRAGOMAN_VERSION_RAW)
|
|
8
|
+
|
|
9
|
+
project(dragoman VERSION ${DRAGOMAN_VERSION_RAW} LANGUAGES C CXX)
|
|
10
|
+
|
|
11
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
12
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
13
|
+
set(CMAKE_C_STANDARD 99)
|
|
14
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
15
|
+
|
|
16
|
+
option(DRAGOMAN_BUILD_TESTS "Build the test suite" ON)
|
|
17
|
+
option(DRAGOMAN_BUILD_CLI "Build the dragoman command line tool" ON)
|
|
18
|
+
option(DRAGOMAN_BUILD_SHARED "Build a shared library as well as a static one" ON)
|
|
19
|
+
option(DRAGOMAN_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF)
|
|
20
|
+
option(DRAGOMAN_PYTHON_WHEEL "Install only the shared library, into the Python package" OFF)
|
|
21
|
+
|
|
22
|
+
# --------------------------------------------------------------- third party
|
|
23
|
+
#
|
|
24
|
+
# miniz for the zip container and stb for PNG, both taken from the versions
|
|
25
|
+
# Open Doctrines itself builds against, so an archive this library writes is
|
|
26
|
+
# read back by exactly the code that will open it in the game. nlohmann/json
|
|
27
|
+
# is the same single header the game uses. All three are MIT or public domain.
|
|
28
|
+
add_library(dragoman_thirdparty STATIC
|
|
29
|
+
third_party/miniz/miniz.c
|
|
30
|
+
third_party/miniz/miniz_tdef.c
|
|
31
|
+
third_party/miniz/miniz_tinfl.c
|
|
32
|
+
third_party/miniz/miniz_zip.c
|
|
33
|
+
third_party/stb/stb_impl.c
|
|
34
|
+
)
|
|
35
|
+
target_include_directories(dragoman_thirdparty PUBLIC
|
|
36
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/miniz"
|
|
37
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/stb"
|
|
38
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/json"
|
|
39
|
+
)
|
|
40
|
+
# nlohmann/json ships as json.hpp; the library includes it by its usual path.
|
|
41
|
+
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/thirdparty_include/nlohmann")
|
|
42
|
+
configure_file(third_party/json/json.hpp
|
|
43
|
+
"${CMAKE_CURRENT_BINARY_DIR}/thirdparty_include/nlohmann/json.hpp" COPYONLY)
|
|
44
|
+
target_include_directories(dragoman_thirdparty PUBLIC
|
|
45
|
+
"${CMAKE_CURRENT_BINARY_DIR}/thirdparty_include")
|
|
46
|
+
|
|
47
|
+
if(NOT MSVC)
|
|
48
|
+
target_compile_options(dragoman_thirdparty PRIVATE -w)
|
|
49
|
+
endif()
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------- library
|
|
52
|
+
|
|
53
|
+
set(DRAGOMAN_SOURCES
|
|
54
|
+
src/Api.cpp
|
|
55
|
+
src/Common.cpp
|
|
56
|
+
src/Gd5Map.cpp
|
|
57
|
+
src/ModelJson.cpp
|
|
58
|
+
src/OdMap.cpp
|
|
59
|
+
src/Raster.cpp
|
|
60
|
+
src/Scripts.cpp
|
|
61
|
+
src/Support.cpp
|
|
62
|
+
src/Zip.cpp
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
add_library(dragoman STATIC ${DRAGOMAN_SOURCES})
|
|
66
|
+
add_library(dragoman::dragoman ALIAS dragoman)
|
|
67
|
+
target_include_directories(dragoman
|
|
68
|
+
PUBLIC
|
|
69
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
70
|
+
$<INSTALL_INTERFACE:include>
|
|
71
|
+
PRIVATE
|
|
72
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
|
73
|
+
)
|
|
74
|
+
target_link_libraries(dragoman PRIVATE dragoman_thirdparty)
|
|
75
|
+
target_compile_definitions(dragoman PRIVATE DRAGOMAN_BUILDING)
|
|
76
|
+
|
|
77
|
+
if(MSVC)
|
|
78
|
+
target_compile_options(dragoman PRIVATE /W4)
|
|
79
|
+
if(DRAGOMAN_WARNINGS_AS_ERRORS)
|
|
80
|
+
target_compile_options(dragoman PRIVATE /WX)
|
|
81
|
+
endif()
|
|
82
|
+
else()
|
|
83
|
+
target_compile_options(dragoman PRIVATE -Wall -Wextra)
|
|
84
|
+
if(DRAGOMAN_WARNINGS_AS_ERRORS)
|
|
85
|
+
target_compile_options(dragoman PRIVATE -Werror)
|
|
86
|
+
endif()
|
|
87
|
+
endif()
|
|
88
|
+
|
|
89
|
+
# The shared build is what every non-C binding loads: Python via ctypes, Go
|
|
90
|
+
# via cgo, C# via P/Invoke. It exports the C ABI and nothing else.
|
|
91
|
+
if(DRAGOMAN_BUILD_SHARED)
|
|
92
|
+
add_library(dragoman_shared SHARED ${DRAGOMAN_SOURCES})
|
|
93
|
+
set_target_properties(dragoman_shared PROPERTIES
|
|
94
|
+
OUTPUT_NAME dragoman
|
|
95
|
+
VERSION ${PROJECT_VERSION}
|
|
96
|
+
SOVERSION 2 # the ABI version, not the release
|
|
97
|
+
C_VISIBILITY_PRESET hidden
|
|
98
|
+
CXX_VISIBILITY_PRESET hidden
|
|
99
|
+
)
|
|
100
|
+
target_include_directories(dragoman_shared
|
|
101
|
+
PUBLIC
|
|
102
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
103
|
+
$<INSTALL_INTERFACE:include>
|
|
104
|
+
PRIVATE
|
|
105
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
|
106
|
+
)
|
|
107
|
+
target_link_libraries(dragoman_shared PRIVATE dragoman_thirdparty)
|
|
108
|
+
target_compile_definitions(dragoman_shared PRIVATE DRAGOMAN_BUILDING DRAGOMAN_SHARED)
|
|
109
|
+
|
|
110
|
+
# The C++ runtime is linked in statically on Linux. Nothing about this
|
|
111
|
+
# library's interface is C++ -- it is loaded by ctypes, by cgo, by P/Invoke,
|
|
112
|
+
# from processes that have no reason to have libstdc++ at the version it was
|
|
113
|
+
# built against. Carrying it removes the one dependency a caller could not
|
|
114
|
+
# have predicted from the header.
|
|
115
|
+
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
|
116
|
+
target_link_options(dragoman_shared PRIVATE -static-libstdc++ -static-libgcc)
|
|
117
|
+
endif()
|
|
118
|
+
endif()
|
|
119
|
+
|
|
120
|
+
# ----------------------------------------------------------------------- cli
|
|
121
|
+
|
|
122
|
+
if(DRAGOMAN_BUILD_CLI)
|
|
123
|
+
add_executable(dragoman_cli tools/dragoman_cli.cpp)
|
|
124
|
+
set_target_properties(dragoman_cli PROPERTIES OUTPUT_NAME dragoman)
|
|
125
|
+
target_link_libraries(dragoman_cli PRIVATE dragoman)
|
|
126
|
+
endif()
|
|
127
|
+
|
|
128
|
+
# --------------------------------------------------------------------- tests
|
|
129
|
+
|
|
130
|
+
if(DRAGOMAN_BUILD_TESTS)
|
|
131
|
+
enable_testing()
|
|
132
|
+
add_subdirectory(tests)
|
|
133
|
+
endif()
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------- install
|
|
136
|
+
|
|
137
|
+
# Building a Python wheel: the shared library goes *inside* the package, beside
|
|
138
|
+
# __init__.py, which is the first place bindings/python/dragoman/_ffi.py looks.
|
|
139
|
+
# That is what makes `pip install dragoman` a complete install with no compiler
|
|
140
|
+
# and no separate library to find -- the audience for this includes people who
|
|
141
|
+
# install Greater Diplomacy 5 by unzipping it.
|
|
142
|
+
if(DRAGOMAN_PYTHON_WHEEL)
|
|
143
|
+
if(NOT DRAGOMAN_BUILD_SHARED)
|
|
144
|
+
message(FATAL_ERROR "DRAGOMAN_PYTHON_WHEEL needs DRAGOMAN_BUILD_SHARED")
|
|
145
|
+
endif()
|
|
146
|
+
# COMPONENT is repeated per artifact on purpose. Written once at the end it
|
|
147
|
+
# binds only to the group it follows, so on Linux and macOS -- where a
|
|
148
|
+
# shared library is a LIBRARY artifact, not a RUNTIME one -- the .so landed
|
|
149
|
+
# in the default component and `--install --component python` installed
|
|
150
|
+
# nothing at all. The wheel built, imported, and had no library in it.
|
|
151
|
+
install(TARGETS dragoman_shared
|
|
152
|
+
LIBRARY DESTINATION dragoman COMPONENT python
|
|
153
|
+
RUNTIME DESTINATION dragoman COMPONENT python)
|
|
154
|
+
return() # a wheel wants the library and nothing else -- no headers, no CLI
|
|
155
|
+
endif()
|
|
156
|
+
|
|
157
|
+
include(GNUInstallDirs)
|
|
158
|
+
install(TARGETS dragoman
|
|
159
|
+
EXPORT dragomanTargets
|
|
160
|
+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
|
161
|
+
if(DRAGOMAN_BUILD_SHARED)
|
|
162
|
+
install(TARGETS dragoman_shared
|
|
163
|
+
EXPORT dragomanTargets
|
|
164
|
+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
165
|
+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
|
166
|
+
endif()
|
|
167
|
+
if(DRAGOMAN_BUILD_CLI)
|
|
168
|
+
install(TARGETS dragoman_cli RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
|
169
|
+
endif()
|
|
170
|
+
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dragoman contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Dragoman is an independent implementation, written from observing the two file
|
|
26
|
+
formats it converts between. No source code from either game is included in or
|
|
27
|
+
derived into it.
|
|
28
|
+
|
|
29
|
+
The games themselves remain under their own licences, and neither project's
|
|
30
|
+
maps are redistributed here:
|
|
31
|
+
|
|
32
|
+
Open Doctrines OpenDoctrines Non-Commercial License
|
|
33
|
+
Greater Diplomacy 5 GNU General Public License v3.0
|
|
34
|
+
https://github.com/GitGetGot415/Greater-Diplomacy-5
|
|
35
|
+
|
|
36
|
+
Vendored third-party code, all permissively licensed:
|
|
37
|
+
|
|
38
|
+
third_party/miniz miniz 11.3.1, MIT
|
|
39
|
+
https://github.com/richgel999/miniz
|
|
40
|
+
third_party/stb stb_image / stb_image_write, MIT or public domain
|
|
41
|
+
https://github.com/nothings/stb
|
|
42
|
+
third_party/json nlohmann/json 3.11.3, MIT
|
|
43
|
+
https://github.com/nlohmann/json
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: open-dragoman
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Translate maps between Open Doctrines and Greater Diplomacy 5
|
|
5
|
+
Keywords: grand-strategy,map,conversion,opendoctrines,greater-diplomacy,modding
|
|
6
|
+
Author: Dragoman contributors
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Dragoman contributors
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Dragoman is an independent implementation, written from observing the two file
|
|
32
|
+
formats it converts between. No source code from either game is included in or
|
|
33
|
+
derived into it.
|
|
34
|
+
|
|
35
|
+
The games themselves remain under their own licences, and neither project's
|
|
36
|
+
maps are redistributed here:
|
|
37
|
+
|
|
38
|
+
Open Doctrines OpenDoctrines Non-Commercial License
|
|
39
|
+
Greater Diplomacy 5 GNU General Public License v3.0
|
|
40
|
+
https://github.com/GitGetGot415/Greater-Diplomacy-5
|
|
41
|
+
|
|
42
|
+
Vendored third-party code, all permissively licensed:
|
|
43
|
+
|
|
44
|
+
third_party/miniz miniz 11.3.1, MIT
|
|
45
|
+
https://github.com/richgel999/miniz
|
|
46
|
+
third_party/stb stb_image / stb_image_write, MIT or public domain
|
|
47
|
+
https://github.com/nothings/stb
|
|
48
|
+
third_party/json nlohmann/json 3.11.3, MIT
|
|
49
|
+
https://github.com/nlohmann/json
|
|
50
|
+
|
|
51
|
+
Classifier: Development Status :: 4 - Beta
|
|
52
|
+
Classifier: Intended Audience :: Developers
|
|
53
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
54
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
55
|
+
Classifier: Programming Language :: C++
|
|
56
|
+
Classifier: Programming Language :: Python :: 3
|
|
57
|
+
Classifier: Topic :: Games/Entertainment
|
|
58
|
+
Classifier: Topic :: File Formats
|
|
59
|
+
Project-URL: Homepage, https://github.com/Pr1nted/dragoman
|
|
60
|
+
Project-URL: Documentation, https://github.com/Pr1nted/dragoman/wiki
|
|
61
|
+
Project-URL: Changelog, https://github.com/Pr1nted/dragoman/blob/main/CHANGELOG.md
|
|
62
|
+
Project-URL: Issues, https://github.com/Pr1nted/dragoman/issues
|
|
63
|
+
Requires-Python: >=3.8
|
|
64
|
+
Description-Content-Type: text/markdown
|
|
65
|
+
|
|
66
|
+
# Dragoman
|
|
67
|
+
|
|
68
|
+
Translate maps between **Open Doctrines** (`.odmap`) and **Greater Diplomacy 5**
|
|
69
|
+
(map directories), in both directions, losing as little as the two formats
|
|
70
|
+
allow — and nothing at all on a round trip.
|
|
71
|
+
|
|
72
|
+
A *dragoman* was the interpreter attached to an embassy: the person through
|
|
73
|
+
whom two powers who shared no language could nonetheless sign something. This
|
|
74
|
+
is that, for map files.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install open-dragoman
|
|
78
|
+
|
|
79
|
+
dragoman convert 1914.odmap base_maps/1914 --to gd5
|
|
80
|
+
dragoman convert base_maps/GD4 gd4.odmap --to odmap
|
|
81
|
+
dragoman roundtrip 1914.odmap # prove nothing was lost
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import dragoman
|
|
86
|
+
|
|
87
|
+
dragoman.convert("1914.odmap", "base_maps/1914", to="gd5")
|
|
88
|
+
|
|
89
|
+
world = dragoman.load("1914.odmap")
|
|
90
|
+
print(world.name, len(world.provinces), "provinces")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**[Read the wiki →](https://github.com/Pr1nted/dragoman/wiki)** — what it is,
|
|
94
|
+
how to use it, what every message means, and what it cannot do.
|
|
95
|
+
|
|
96
|
+
## Status
|
|
97
|
+
|
|
98
|
+
Every map both games ship round-trips without losing a single modelled field:
|
|
99
|
+
|
|
100
|
+
| Suite | Result |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Greater Diplomacy 5 maps (12 base maps + 16 scenarios) | **28 / 28** |
|
|
103
|
+
| Open Doctrines maps (5 scenarios + the world map) | **6 / 6** |
|
|
104
|
+
| Unit tests (raster, scripts, ABI, versioning, round trip) | **5 / 5** |
|
|
105
|
+
|
|
106
|
+
Verified against Open Doctrines and against
|
|
107
|
+
[GitGetGot415/Greater-Diplomacy-5](https://github.com/GitGetGot415/Greater-Diplomacy-5).
|
|
108
|
+
Reproduce with `python3 tools/conformance.py <directory of maps>`.
|
|
109
|
+
|
|
110
|
+
Converted maps have also been **played in both games**, not merely loaded by
|
|
111
|
+
this library. Open Doctrines' world map converted to GD5 boots through GD5's
|
|
112
|
+
own `Controller` and `Map` state with all 23 screens, its flags drawn, its
|
|
113
|
+
oceans navigable and every province centre inside its own province; a GD5 map
|
|
114
|
+
converted to Open Doctrines plays five AI turns under `OpenDoctrines
|
|
115
|
+
--simulate`. Nearly everything this library gets right, it gets right because
|
|
116
|
+
somebody opened the result in the game and looked at it — the flag encoding,
|
|
117
|
+
the ocean, the political layer's colour key and the province-border fill were
|
|
118
|
+
all found that way.
|
|
119
|
+
|
|
120
|
+
One thing crossing to Open Doctrines still needs a human: GD5 starts its
|
|
121
|
+
nations with an empty stockpile and Open Doctrines expects a starting
|
|
122
|
+
endowment, so a converted map bankrupts its world on the first turn unless
|
|
123
|
+
treasuries are set. Reported as `od.treasury` rather than guessed at.
|
|
124
|
+
|
|
125
|
+
## What "lossless" means here
|
|
126
|
+
|
|
127
|
+
The two games are not the same game, so a straight translation always loses
|
|
128
|
+
something: Open Doctrines models a province's population, its port and its
|
|
129
|
+
ethnic minorities, and GD5 has no field for any of them; GD5 models terrain,
|
|
130
|
+
province adjacency, unit rosters and research, and Open Doctrines has no field
|
|
131
|
+
for those.
|
|
132
|
+
|
|
133
|
+
Dragoman writes the difference down. Everything the destination cannot hold
|
|
134
|
+
goes into a **sidecar** beside the map — `dragoman_sidecar/` in a GD5
|
|
135
|
+
directory, `dragoman/` inside a `.odmap` archive. Both games read a fixed list
|
|
136
|
+
of filenames and ignore everything else, so the sidecar is invisible to them
|
|
137
|
+
and costs the converted map nothing.
|
|
138
|
+
|
|
139
|
+
The result:
|
|
140
|
+
|
|
141
|
+
- **Converting one way** is as faithful as the target format permits, and every
|
|
142
|
+
approximation is reported by a stable code you can act on.
|
|
143
|
+
- **Converting back** restores what the target could not hold. `A → B → A` keeps
|
|
144
|
+
every province, nation, relation, claim, core, script and carried file, and
|
|
145
|
+
the province raster hashes identical.
|
|
146
|
+
|
|
147
|
+
`dragoman roundtrip <map>` asserts exactly that. Pass `--no-sidecar` for a
|
|
148
|
+
smaller, genuinely lossy conversion — the round trip then fails, on purpose.
|
|
149
|
+
|
|
150
|
+
Not promised: byte-identical container files. Two zip archives holding
|
|
151
|
+
identical members are different files, because deflate is not reproducible
|
|
152
|
+
across implementations, and Open Doctrines packs with Python's zlib while this
|
|
153
|
+
packs with miniz. Every file *inside* is preserved exactly.
|
|
154
|
+
|
|
155
|
+
## What crosses
|
|
156
|
+
|
|
157
|
+
| | Open Doctrines | Greater Diplomacy 5 |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| Province raster | `provinces.png`, id packed big-endian | `id_map.png`, id packed little-endian |
|
|
160
|
+
| Province identity | preserved exactly — the rasters differ only by swapping red and blue |||
|
|
161
|
+
| Owner, name, claims | ✅ | ✅ |
|
|
162
|
+
| Cores | — | ✅ (carried) |
|
|
163
|
+
| Sea provinces | — (**synthesised** for GD5) | ✅ |
|
|
164
|
+
| Population, ports, fortification | ✅ | — (carried) |
|
|
165
|
+
| Minorities, political compass, policies | ✅ | — (carried) |
|
|
166
|
+
| Terrain, adjacency, province centres | — (**derived** from the raster) | ✅ |
|
|
167
|
+
| Units, buildings, research, factions | — (armies carried) | ✅ |
|
|
168
|
+
| Flags | a PNG in the archive | raw 60x40 pixels, base64 |
|
|
169
|
+
| Relations | ally, non-aggression, guarantee | war and alliance only (rest carried) |
|
|
170
|
+
| Scripts | imperative `#OD/MapEngine/1` | declarative scripted events |
|
|
171
|
+
|
|
172
|
+
Adjacency and centroids are computed from the province raster when converting
|
|
173
|
+
to GD5, since Open Doctrines derives both at load and never stores them. The
|
|
174
|
+
centroid finder deliberately handles crescent-shaped provinces, whose mean
|
|
175
|
+
pixel falls outside themselves.
|
|
176
|
+
|
|
177
|
+
Full field-by-field detail: [docs/mapping.md](docs/mapping.md).
|
|
178
|
+
|
|
179
|
+
## Scripts
|
|
180
|
+
|
|
181
|
+
Open Doctrines' scripting is imperative and line-based, with loops and
|
|
182
|
+
`waitUntil` suspension points. GD5's is a list of declarative events, each a
|
|
183
|
+
set of conditions and a set of actions. The overlap is the shape both express:
|
|
184
|
+
a gate, and things that happen when it opens.
|
|
185
|
+
|
|
186
|
+
An Open Doctrines script written as top-level `waitUntil` stages becomes GD5
|
|
187
|
+
events, one stage at a time. A GD5 event becomes an entry script with a
|
|
188
|
+
`waitUntil` and some `set` lines. Anything outside that overlap — a `foreach`
|
|
189
|
+
over a country's provinces, conditions chained with `XOR` — is reported by name
|
|
190
|
+
and carried unchanged rather than half-translated. GD5 event types this library
|
|
191
|
+
has never heard of pass through untouched, so a GD5 → OD → GD5 trip is lossless
|
|
192
|
+
even for conditions added to the game after this was written.
|
|
193
|
+
|
|
194
|
+
Details and the full vocabulary: [docs/scripting.md](docs/scripting.md).
|
|
195
|
+
|
|
196
|
+
## Installing
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pip install open-dragoman
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The wheel carries the compiled library inside the package, so there is no
|
|
203
|
+
compiler needed at install time and nothing to locate afterwards. You get both
|
|
204
|
+
the Python API and a `dragoman` command.
|
|
205
|
+
|
|
206
|
+
Prebuilt binaries for people who want nothing to do with Python are attached to
|
|
207
|
+
each [release](https://github.com/Pr1nted/dragoman/releases). The full set of
|
|
208
|
+
options — release archives, source builds, CMake `FetchContent` — is on
|
|
209
|
+
[the Installing page](https://github.com/Pr1nted/dragoman/wiki/Installing).
|
|
210
|
+
|
|
211
|
+
## Building from source
|
|
212
|
+
|
|
213
|
+
Needs CMake 3.16 and a C++17 compiler. There are no external dependencies —
|
|
214
|
+
miniz, stb and nlohmann/json are vendored, all MIT or public domain.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
|
218
|
+
cmake --build build
|
|
219
|
+
ctest --test-dir build --output-on-failure
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
This produces `libdragoman.a`, a shared `libdragoman.{so,dylib,dll}`, and the
|
|
223
|
+
`dragoman` command line tool.
|
|
224
|
+
|
|
225
|
+
## Using it from your language
|
|
226
|
+
|
|
227
|
+
The core is C++17 behind a flat **C99 ABI**, so anything with an FFI can call
|
|
228
|
+
it. The whole interchange model is available as JSON through
|
|
229
|
+
`dg_world_to_json`, which is how a binding reads or edits a map without needing
|
|
230
|
+
an accessor per field.
|
|
231
|
+
|
|
232
|
+
- **C** — `#include <dragoman/dragoman.h>`, link `dragoman`. See
|
|
233
|
+
[bindings/c/example.c](bindings/c/example.c).
|
|
234
|
+
- **C++** — `#include <dragoman/dragoman.hpp>` for an RAII wrapper over the same
|
|
235
|
+
ABI. See [bindings/cpp/example.cpp](bindings/cpp/example.cpp).
|
|
236
|
+
- **Python** — `pip install open-dragoman`. Pure ctypes, and the wheel carries the
|
|
237
|
+
library, so no compiler is needed at install time.
|
|
238
|
+
- **Anything else** — Rust, Go, C#, Java, Lua and WebAssembly all bind the same
|
|
239
|
+
header. [docs/abi.md](docs/abi.md) documents the contract.
|
|
240
|
+
|
|
241
|
+
## Versioning
|
|
242
|
+
|
|
243
|
+
Two numbers that move for different reasons:
|
|
244
|
+
|
|
245
|
+
- **Library version** (`VERSION`, semver) — the release. Currently `0.2.0`.
|
|
246
|
+
- **ABI version** (`DRAGOMAN_ABI_VERSION`) — bumped only when an existing symbol
|
|
247
|
+
changes meaning, so a binding can refuse to load a library it cannot speak to
|
|
248
|
+
without parsing semver.
|
|
249
|
+
|
|
250
|
+
The version lives in `VERSION` and is mirrored into the C header, the Python
|
|
251
|
+
package and the CMake project. `tools/check_version.py` and
|
|
252
|
+
`tests/test_version.cpp` both fail if any copy drifts, and CI runs them on every
|
|
253
|
+
push. Details: [docs/versioning.md](docs/versioning.md).
|
|
254
|
+
|
|
255
|
+
## Licence
|
|
256
|
+
|
|
257
|
+
Dragoman is MIT. It is an independent implementation written from observing
|
|
258
|
+
both file formats; no code from either game is copied into it. Open Doctrines
|
|
259
|
+
and Greater Diplomacy 5 remain under their own licences (Open Doctrines
|
|
260
|
+
Non-Commercial, and GPL-3.0 respectively), and neither project's maps are
|
|
261
|
+
redistributed here.
|