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.
Files changed (77) hide show
  1. open_dragoman-0.2.0/.gitignore +31 -0
  2. open_dragoman-0.2.0/CHANGELOG.md +118 -0
  3. open_dragoman-0.2.0/CMakeLists.txt +170 -0
  4. open_dragoman-0.2.0/LICENSE +43 -0
  5. open_dragoman-0.2.0/PKG-INFO +261 -0
  6. open_dragoman-0.2.0/README.md +196 -0
  7. open_dragoman-0.2.0/VERSION +1 -0
  8. open_dragoman-0.2.0/bindings/c/example.c +76 -0
  9. open_dragoman-0.2.0/bindings/cpp/example.cpp +58 -0
  10. open_dragoman-0.2.0/bindings/python/dragoman/__init__.py +348 -0
  11. open_dragoman-0.2.0/bindings/python/dragoman/__main__.py +139 -0
  12. open_dragoman-0.2.0/bindings/python/dragoman/_ffi.py +129 -0
  13. open_dragoman-0.2.0/bindings/python/dragoman/_version.py +1 -0
  14. open_dragoman-0.2.0/docs/abi.md +89 -0
  15. open_dragoman-0.2.0/docs/mapping.md +289 -0
  16. open_dragoman-0.2.0/docs/model.md +105 -0
  17. open_dragoman-0.2.0/docs/roundtrip.md +79 -0
  18. open_dragoman-0.2.0/docs/scripting.md +104 -0
  19. open_dragoman-0.2.0/docs/versioning.md +61 -0
  20. open_dragoman-0.2.0/include/dragoman/dragoman.h +195 -0
  21. open_dragoman-0.2.0/include/dragoman/dragoman.hpp +159 -0
  22. open_dragoman-0.2.0/pyproject.toml +89 -0
  23. open_dragoman-0.2.0/src/Api.cpp +463 -0
  24. open_dragoman-0.2.0/src/Common.cpp +523 -0
  25. open_dragoman-0.2.0/src/Formats.h +124 -0
  26. open_dragoman-0.2.0/src/Gd5Map.cpp +1222 -0
  27. open_dragoman-0.2.0/src/IsoTable.inc +263 -0
  28. open_dragoman-0.2.0/src/Model.h +208 -0
  29. open_dragoman-0.2.0/src/ModelJson.cpp +306 -0
  30. open_dragoman-0.2.0/src/ModelJson.h +14 -0
  31. open_dragoman-0.2.0/src/OdMap.cpp +741 -0
  32. open_dragoman-0.2.0/src/Raster.cpp +397 -0
  33. open_dragoman-0.2.0/src/Raster.h +132 -0
  34. open_dragoman-0.2.0/src/Scripts.cpp +543 -0
  35. open_dragoman-0.2.0/src/Support.cpp +222 -0
  36. open_dragoman-0.2.0/src/Support.h +85 -0
  37. open_dragoman-0.2.0/src/Zip.cpp +134 -0
  38. open_dragoman-0.2.0/src/Zip.h +45 -0
  39. open_dragoman-0.2.0/tests/CMakeLists.txt +33 -0
  40. open_dragoman-0.2.0/tests/Check.h +69 -0
  41. open_dragoman-0.2.0/tests/Fixture.h +127 -0
  42. open_dragoman-0.2.0/tests/test_abi.cpp +157 -0
  43. open_dragoman-0.2.0/tests/test_python.py +127 -0
  44. open_dragoman-0.2.0/tests/test_raster.cpp +194 -0
  45. open_dragoman-0.2.0/tests/test_roundtrip.cpp +382 -0
  46. open_dragoman-0.2.0/tests/test_scripts.cpp +211 -0
  47. open_dragoman-0.2.0/tests/test_version.cpp +62 -0
  48. open_dragoman-0.2.0/third_party/json/json.hpp +24765 -0
  49. open_dragoman-0.2.0/third_party/miniz/miniz.c +646 -0
  50. open_dragoman-0.2.0/third_party/miniz/miniz.h +615 -0
  51. open_dragoman-0.2.0/third_party/miniz/miniz_common.h +89 -0
  52. open_dragoman-0.2.0/third_party/miniz/miniz_export.h +2 -0
  53. open_dragoman-0.2.0/third_party/miniz/miniz_tdef.c +1602 -0
  54. open_dragoman-0.2.0/third_party/miniz/miniz_tdef.h +199 -0
  55. open_dragoman-0.2.0/third_party/miniz/miniz_tinfl.c +778 -0
  56. open_dragoman-0.2.0/third_party/miniz/miniz_tinfl.h +150 -0
  57. open_dragoman-0.2.0/third_party/miniz/miniz_zip.c +4895 -0
  58. open_dragoman-0.2.0/third_party/miniz/miniz_zip.h +454 -0
  59. open_dragoman-0.2.0/third_party/stb/stb_image.h +7988 -0
  60. open_dragoman-0.2.0/third_party/stb/stb_image_write.h +1724 -0
  61. open_dragoman-0.2.0/third_party/stb/stb_impl.c +17 -0
  62. open_dragoman-0.2.0/tools/check_version.py +79 -0
  63. open_dragoman-0.2.0/tools/conformance.py +78 -0
  64. open_dragoman-0.2.0/tools/dragoman_cli.cpp +194 -0
  65. open_dragoman-0.2.0/tools/generate_iso_table.py +70 -0
  66. open_dragoman-0.2.0/tools/publish_wiki.py +136 -0
  67. open_dragoman-0.2.0/wiki/Diagnostics.md +91 -0
  68. open_dragoman-0.2.0/wiki/Getting-Started.md +99 -0
  69. open_dragoman-0.2.0/wiki/Home.md +71 -0
  70. open_dragoman-0.2.0/wiki/Installing.md +119 -0
  71. open_dragoman-0.2.0/wiki/Languages.md +118 -0
  72. open_dragoman-0.2.0/wiki/README.md +41 -0
  73. open_dragoman-0.2.0/wiki/Round-Trips.md +94 -0
  74. open_dragoman-0.2.0/wiki/Scripts-and-Events.md +95 -0
  75. open_dragoman-0.2.0/wiki/Troubleshooting.md +105 -0
  76. open_dragoman-0.2.0/wiki/What-Crosses.md +120 -0
  77. 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.