open-dragoman 0.2.2__py3-none-win_amd64.whl
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.
- dragoman/__init__.py +348 -0
- dragoman/__main__.py +139 -0
- dragoman/_ffi.py +129 -0
- dragoman/_version.py +1 -0
- dragoman/dragoman.dll +0 -0
- open_dragoman-0.2.2.dist-info/METADATA +261 -0
- open_dragoman-0.2.2.dist-info/RECORD +10 -0
- open_dragoman-0.2.2.dist-info/WHEEL +5 -0
- open_dragoman-0.2.2.dist-info/entry_points.txt +3 -0
- open_dragoman-0.2.2.dist-info/licenses/LICENSE +43 -0
dragoman/__init__.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Dragoman -- translate maps between Open Doctrines and Greater Diplomacy 5.
|
|
2
|
+
|
|
3
|
+
import dragoman
|
|
4
|
+
|
|
5
|
+
report = dragoman.convert("1914.odmap", "base_maps/1914", to="gd5")
|
|
6
|
+
for entry in report.warnings:
|
|
7
|
+
print(entry.code, entry.message)
|
|
8
|
+
|
|
9
|
+
world = dragoman.load("1914.odmap")
|
|
10
|
+
print(world.name, len(world.provinces), "provinces")
|
|
11
|
+
|
|
12
|
+
The whole model is available as plain Python data through `World.to_dict()`,
|
|
13
|
+
so a map can be inspected or edited here and written back out without this
|
|
14
|
+
package needing an accessor for every field the two games have.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ctypes
|
|
20
|
+
import json
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Dict, List, Optional, Union
|
|
23
|
+
|
|
24
|
+
from ._ffi import Options, declare, load_library
|
|
25
|
+
from ._version import __version__
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"__version__",
|
|
29
|
+
"abi_version",
|
|
30
|
+
"library_version",
|
|
31
|
+
"Format",
|
|
32
|
+
"Severity",
|
|
33
|
+
"Diagnostic",
|
|
34
|
+
"Report",
|
|
35
|
+
"World",
|
|
36
|
+
"DragomanError",
|
|
37
|
+
"detect",
|
|
38
|
+
"load",
|
|
39
|
+
"convert",
|
|
40
|
+
"roundtrip_check",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
_lib = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _library():
|
|
47
|
+
global _lib
|
|
48
|
+
if _lib is None:
|
|
49
|
+
_lib = declare(load_library())
|
|
50
|
+
return _lib
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DragomanError(RuntimeError):
|
|
54
|
+
"""A conversion that could not be completed.
|
|
55
|
+
|
|
56
|
+
Carries the report as well as the message, because the reason a map failed
|
|
57
|
+
is usually in a diagnostic rather than in the final error string.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str, report: Optional["Report"] = None):
|
|
61
|
+
super().__init__(message)
|
|
62
|
+
self.report = report
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Format:
|
|
66
|
+
UNKNOWN = 0
|
|
67
|
+
ODMAP = 1
|
|
68
|
+
GD5 = 2
|
|
69
|
+
|
|
70
|
+
_BY_NAME = {"odmap": ODMAP, "od": ODMAP, "gd5": GD5, "gd": GD5, "unknown": UNKNOWN}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def parse(cls, value: Union[int, str, None]) -> int:
|
|
74
|
+
if value is None:
|
|
75
|
+
return cls.UNKNOWN
|
|
76
|
+
if isinstance(value, int):
|
|
77
|
+
return value
|
|
78
|
+
key = value.strip().lower()
|
|
79
|
+
if key not in cls._BY_NAME:
|
|
80
|
+
raise ValueError(f"unknown format {value!r}; use 'odmap' or 'gd5'")
|
|
81
|
+
return cls._BY_NAME[key]
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def name(value: int) -> str:
|
|
85
|
+
return _library().dg_format_name(value).decode()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Severity:
|
|
89
|
+
INFO = 0
|
|
90
|
+
WARNING = 1
|
|
91
|
+
ERROR = 2
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class Diagnostic:
|
|
96
|
+
severity: int
|
|
97
|
+
code: str
|
|
98
|
+
message: str
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_warning(self) -> bool:
|
|
102
|
+
return self.severity == Severity.WARNING
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def is_error(self) -> bool:
|
|
106
|
+
return self.severity == Severity.ERROR
|
|
107
|
+
|
|
108
|
+
def __str__(self) -> str:
|
|
109
|
+
label = {0: "note", 1: "warning", 2: "error"}[self.severity]
|
|
110
|
+
return f"{label} {self.code}: {self.message}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Report:
|
|
114
|
+
"""What the translation could not do exactly, and why."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, entries: List[Diagnostic]):
|
|
117
|
+
self._entries = entries
|
|
118
|
+
|
|
119
|
+
def __iter__(self):
|
|
120
|
+
return iter(self._entries)
|
|
121
|
+
|
|
122
|
+
def __len__(self) -> int:
|
|
123
|
+
return len(self._entries)
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return f"<Report {len(self._entries)} entries, worst={self.worst}>"
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def entries(self) -> List[Diagnostic]:
|
|
130
|
+
return list(self._entries)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def warnings(self) -> List[Diagnostic]:
|
|
134
|
+
return [e for e in self._entries if e.severity == Severity.WARNING]
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def errors(self) -> List[Diagnostic]:
|
|
138
|
+
return [e for e in self._entries if e.severity == Severity.ERROR]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def worst(self) -> int:
|
|
142
|
+
return max((e.severity for e in self._entries), default=Severity.INFO)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def ok(self) -> bool:
|
|
146
|
+
return self.worst < Severity.ERROR
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _take_report(handle) -> Report:
|
|
150
|
+
"""Read a dg_report out and free it. Always called, even on failure."""
|
|
151
|
+
lib = _library()
|
|
152
|
+
entries: List[Diagnostic] = []
|
|
153
|
+
if handle:
|
|
154
|
+
for i in range(lib.dg_report_count(handle)):
|
|
155
|
+
entries.append(
|
|
156
|
+
Diagnostic(
|
|
157
|
+
severity=lib.dg_report_severity(handle, i),
|
|
158
|
+
code=lib.dg_report_code(handle, i).decode(),
|
|
159
|
+
message=lib.dg_report_message(handle, i).decode(),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
lib.dg_report_free(handle)
|
|
163
|
+
return Report(entries)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _take_string(pointer) -> str:
|
|
167
|
+
"""Read a char* the library allocated and hand it back to the library."""
|
|
168
|
+
if not pointer:
|
|
169
|
+
return ""
|
|
170
|
+
lib = _library()
|
|
171
|
+
text = ctypes.cast(pointer, ctypes.c_char_p).value or b""
|
|
172
|
+
lib.dg_string_free(pointer)
|
|
173
|
+
return text.decode()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _options(
|
|
177
|
+
carry_sidecar: bool = True,
|
|
178
|
+
derive_geometry: bool = True,
|
|
179
|
+
translate_scripts: bool = True,
|
|
180
|
+
strict: bool = False,
|
|
181
|
+
reencode_images: bool = False,
|
|
182
|
+
synthesise_ocean: bool = True,
|
|
183
|
+
) -> Options:
|
|
184
|
+
opts = Options()
|
|
185
|
+
_library().dg_options_defaults(ctypes.byref(opts))
|
|
186
|
+
opts.carry_sidecar = int(carry_sidecar)
|
|
187
|
+
opts.derive_geometry = int(derive_geometry)
|
|
188
|
+
opts.translate_scripts = int(translate_scripts)
|
|
189
|
+
opts.strict = int(strict)
|
|
190
|
+
opts.reencode_images = int(reencode_images)
|
|
191
|
+
opts.synthesise_ocean = int(synthesise_ocean)
|
|
192
|
+
return opts
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class World:
|
|
196
|
+
"""A loaded map, in neither game's layout but in the shared model."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, handle, report: Report):
|
|
199
|
+
self._handle = handle
|
|
200
|
+
self._dict: Optional[Dict[str, Any]] = None
|
|
201
|
+
self.report = report
|
|
202
|
+
|
|
203
|
+
def __del__(self):
|
|
204
|
+
self.close()
|
|
205
|
+
|
|
206
|
+
def __enter__(self) -> "World":
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def __exit__(self, *exc) -> None:
|
|
210
|
+
self.close()
|
|
211
|
+
|
|
212
|
+
def close(self) -> None:
|
|
213
|
+
if getattr(self, "_handle", None):
|
|
214
|
+
_library().dg_world_free(self._handle)
|
|
215
|
+
self._handle = None
|
|
216
|
+
|
|
217
|
+
def _require(self):
|
|
218
|
+
if not self._handle:
|
|
219
|
+
raise DragomanError("this world has already been closed")
|
|
220
|
+
return self._handle
|
|
221
|
+
|
|
222
|
+
def __repr__(self) -> str:
|
|
223
|
+
return (
|
|
224
|
+
f"<World {self.name!r} {len(self.provinces)} provinces "
|
|
225
|
+
f"{len(self.nations)} nations from {self.origin}>"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def name(self) -> str:
|
|
230
|
+
return _library().dg_world_name(self._require()).decode()
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def origin(self) -> str:
|
|
234
|
+
return Format.name(_library().dg_world_origin(self._require()))
|
|
235
|
+
|
|
236
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
237
|
+
"""The whole interchange model as plain Python data.
|
|
238
|
+
|
|
239
|
+
Parsed once and kept, because on a world map this is a few megabytes
|
|
240
|
+
of JSON and callers reach for `.provinces` in a loop.
|
|
241
|
+
"""
|
|
242
|
+
if self._dict is None:
|
|
243
|
+
self._dict = json.loads(_take_string(_library().dg_world_to_json(self._require())))
|
|
244
|
+
return self._dict
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def provinces(self) -> List[Dict[str, Any]]:
|
|
248
|
+
return self.to_dict().get("provinces", [])
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def nations(self) -> List[Dict[str, Any]]:
|
|
252
|
+
return self.to_dict().get("nations", [])
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def events(self) -> List[Dict[str, Any]]:
|
|
256
|
+
return self.to_dict().get("events", [])
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def scripts(self) -> List[Dict[str, Any]]:
|
|
260
|
+
return self.to_dict().get("scripts", [])
|
|
261
|
+
|
|
262
|
+
def save(self, path: str, to: Union[int, str], **options) -> Report:
|
|
263
|
+
lib = _library()
|
|
264
|
+
opts = _options(**options)
|
|
265
|
+
handle = ctypes.c_void_p()
|
|
266
|
+
rc = lib.dg_save(
|
|
267
|
+
self._require(),
|
|
268
|
+
str(path).encode(),
|
|
269
|
+
Format.parse(to),
|
|
270
|
+
ctypes.byref(opts),
|
|
271
|
+
ctypes.byref(handle),
|
|
272
|
+
)
|
|
273
|
+
report = _take_report(handle)
|
|
274
|
+
if rc != 0:
|
|
275
|
+
raise DragomanError(lib.dg_last_error().decode(), report)
|
|
276
|
+
return report
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def library_version() -> str:
|
|
280
|
+
"""The version of the shared library, which a wheel may outlive."""
|
|
281
|
+
return _library().dg_version_string().decode()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def abi_version() -> int:
|
|
285
|
+
return _library().dg_abi_version()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def detect(path: str) -> str:
|
|
289
|
+
"""'odmap', 'gd5' or 'unknown', decided by content rather than by name."""
|
|
290
|
+
return Format.name(_library().dg_detect(str(path).encode()))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def load(path: str, fmt: Union[int, str, None] = None, **options) -> World:
|
|
294
|
+
lib = _library()
|
|
295
|
+
opts = _options(**options)
|
|
296
|
+
handle = ctypes.c_void_p()
|
|
297
|
+
world = lib.dg_load(
|
|
298
|
+
str(path).encode(), Format.parse(fmt), ctypes.byref(opts), ctypes.byref(handle)
|
|
299
|
+
)
|
|
300
|
+
report = _take_report(handle)
|
|
301
|
+
if not world:
|
|
302
|
+
raise DragomanError(lib.dg_last_error().decode(), report)
|
|
303
|
+
return World(world, report)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def convert(src: str, dst: str, to: Union[int, str, None] = None, **options) -> Report:
|
|
307
|
+
"""Translate a map. With no `to`, converts to whichever format `src` is not."""
|
|
308
|
+
lib = _library()
|
|
309
|
+
if to is None:
|
|
310
|
+
found = _library().dg_detect(str(src).encode())
|
|
311
|
+
if found == Format.UNKNOWN:
|
|
312
|
+
raise DragomanError(f"cannot tell what {src} is; pass to='odmap' or to='gd5'")
|
|
313
|
+
to = Format.GD5 if found == Format.ODMAP else Format.ODMAP
|
|
314
|
+
|
|
315
|
+
opts = _options(**options)
|
|
316
|
+
handle = ctypes.c_void_p()
|
|
317
|
+
rc = lib.dg_convert(
|
|
318
|
+
str(src).encode(), str(dst).encode(), Format.parse(to), ctypes.byref(opts),
|
|
319
|
+
ctypes.byref(handle),
|
|
320
|
+
)
|
|
321
|
+
report = _take_report(handle)
|
|
322
|
+
if rc != 0:
|
|
323
|
+
raise DragomanError(lib.dg_last_error().decode(), report)
|
|
324
|
+
return report
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def roundtrip_check(path: str, to: Union[int, str, None] = None, **options):
|
|
328
|
+
"""Convert to `to` and back; return (identical, report).
|
|
329
|
+
|
|
330
|
+
'Identical' means every province, nation, relation, claim, script and
|
|
331
|
+
carried file returned unchanged and the province raster hashes the same --
|
|
332
|
+
not that the two container files are byte-for-byte equal, which deflate
|
|
333
|
+
does not promise across implementations.
|
|
334
|
+
"""
|
|
335
|
+
lib = _library()
|
|
336
|
+
if to is None:
|
|
337
|
+
found = lib.dg_detect(str(path).encode())
|
|
338
|
+
to = Format.GD5 if found == Format.ODMAP else Format.ODMAP
|
|
339
|
+
|
|
340
|
+
opts = _options(**options)
|
|
341
|
+
handle = ctypes.c_void_p()
|
|
342
|
+
rc = lib.dg_roundtrip_check(
|
|
343
|
+
str(path).encode(), Format.parse(to), ctypes.byref(opts), ctypes.byref(handle)
|
|
344
|
+
)
|
|
345
|
+
report = _take_report(handle)
|
|
346
|
+
if rc < 0:
|
|
347
|
+
raise DragomanError(lib.dg_last_error().decode(), report)
|
|
348
|
+
return rc == 1, report
|
dragoman/__main__.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""The `dragoman` command, for people who installed with pip.
|
|
2
|
+
|
|
3
|
+
The C++ build produces a command line tool of its own; this is the same set of
|
|
4
|
+
subcommands over the same library, so that `pip install dragoman` gives a
|
|
5
|
+
working command without anyone needing to know there is a binary underneath or
|
|
6
|
+
where it went.
|
|
7
|
+
|
|
8
|
+
dragoman convert 1914.odmap base_maps/1914
|
|
9
|
+
dragoman roundtrip 1914.odmap
|
|
10
|
+
dragoman inspect 1914.odmap --json
|
|
11
|
+
dragoman detect some-file
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from . import DragomanError, Severity, __version__, abi_version, convert
|
|
21
|
+
from . import detect as detect_format
|
|
22
|
+
from . import library_version, load, roundtrip_check
|
|
23
|
+
|
|
24
|
+
_LABEL = {Severity.INFO: "note", Severity.WARNING: "warning", Severity.ERROR: "error"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _print_report(report, quiet: bool) -> int:
|
|
28
|
+
for entry in report:
|
|
29
|
+
if quiet and entry.severity == Severity.INFO:
|
|
30
|
+
continue
|
|
31
|
+
stream = sys.stdout if entry.severity == Severity.INFO else sys.stderr
|
|
32
|
+
print(f" {_LABEL[entry.severity]:<7} {entry.code}: {entry.message}", file=stream)
|
|
33
|
+
return report.worst
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _options(args) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"carry_sidecar": not args.no_sidecar,
|
|
39
|
+
"derive_geometry": not args.no_geometry,
|
|
40
|
+
"translate_scripts": not args.no_scripts,
|
|
41
|
+
"synthesise_ocean": not args.no_ocean,
|
|
42
|
+
"strict": args.strict,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _add_common(parser) -> None:
|
|
47
|
+
parser.add_argument("--no-sidecar", action="store_true",
|
|
48
|
+
help="do not carry what the target format cannot hold")
|
|
49
|
+
parser.add_argument("--no-geometry", action="store_true",
|
|
50
|
+
help="do not derive adjacency, centres or the land/sea mask")
|
|
51
|
+
parser.add_argument("--no-scripts", action="store_true",
|
|
52
|
+
help="do not translate scripts or scripted events")
|
|
53
|
+
parser.add_argument("--no-ocean", action="store_true",
|
|
54
|
+
help="do not invent sea provinces for a game that draws none")
|
|
55
|
+
parser.add_argument("--strict", action="store_true",
|
|
56
|
+
help="treat any warning as a failure")
|
|
57
|
+
parser.add_argument("--quiet", action="store_true", help="warnings and errors only")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv=None) -> int:
|
|
61
|
+
parser = argparse.ArgumentParser(
|
|
62
|
+
prog="dragoman",
|
|
63
|
+
description="Translate maps between Open Doctrines and Greater Diplomacy 5.",
|
|
64
|
+
epilog="Documentation: https://github.com/Pr1nted/dragoman/wiki",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument("--version", action="store_true", help="print versions and exit")
|
|
67
|
+
sub = parser.add_subparsers(dest="command")
|
|
68
|
+
|
|
69
|
+
p = sub.add_parser("convert", help="convert a map to the other format")
|
|
70
|
+
p.add_argument("input")
|
|
71
|
+
p.add_argument("output")
|
|
72
|
+
p.add_argument("--to", choices=["odmap", "gd5"],
|
|
73
|
+
help="target format; inferred from the source when omitted")
|
|
74
|
+
_add_common(p)
|
|
75
|
+
|
|
76
|
+
p = sub.add_parser("roundtrip", help="convert to the other format and back, and compare")
|
|
77
|
+
p.add_argument("map")
|
|
78
|
+
p.add_argument("--to", choices=["odmap", "gd5"])
|
|
79
|
+
_add_common(p)
|
|
80
|
+
|
|
81
|
+
p = sub.add_parser("inspect", help="summarise a map, or dump the whole model")
|
|
82
|
+
p.add_argument("map")
|
|
83
|
+
p.add_argument("--json", action="store_true", help="print the interchange model")
|
|
84
|
+
|
|
85
|
+
p = sub.add_parser("detect", help="say what a file is: odmap, gd5, or unknown")
|
|
86
|
+
p.add_argument("path")
|
|
87
|
+
|
|
88
|
+
args = parser.parse_args(argv)
|
|
89
|
+
|
|
90
|
+
if args.version or args.command is None:
|
|
91
|
+
if args.version:
|
|
92
|
+
print(f"dragoman {__version__} (library {library_version()}, abi {abi_version()})")
|
|
93
|
+
return 0
|
|
94
|
+
parser.print_help()
|
|
95
|
+
return 2
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
if args.command == "detect":
|
|
99
|
+
found = detect_format(args.path)
|
|
100
|
+
print(found)
|
|
101
|
+
return 1 if found == "unknown" else 0
|
|
102
|
+
|
|
103
|
+
if args.command == "inspect":
|
|
104
|
+
with load(args.map) as world:
|
|
105
|
+
if args.json:
|
|
106
|
+
print(json.dumps(world.to_dict(), indent=1))
|
|
107
|
+
else:
|
|
108
|
+
print(world.name)
|
|
109
|
+
print(f" format {world.origin}")
|
|
110
|
+
print(f" provinces {len(world.provinces)}")
|
|
111
|
+
print(f" nations {len(world.nations)}")
|
|
112
|
+
print(f" scripts {len(world.scripts) + len(world.events)}")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
if args.command == "convert":
|
|
116
|
+
report = convert(args.input, args.output, to=args.to, **_options(args))
|
|
117
|
+
if not args.quiet:
|
|
118
|
+
print(f"converted {args.input} -> {args.output}")
|
|
119
|
+
return 1 if _print_report(report, args.quiet) >= Severity.ERROR else 0
|
|
120
|
+
|
|
121
|
+
if args.command == "roundtrip":
|
|
122
|
+
identical, report = roundtrip_check(args.map, to=args.to, **_options(args))
|
|
123
|
+
print("round trip preserved every modelled field and the province raster"
|
|
124
|
+
if identical else "round trip changed the map -- see below")
|
|
125
|
+
_print_report(report, args.quiet)
|
|
126
|
+
return 0 if identical else 1
|
|
127
|
+
|
|
128
|
+
except DragomanError as exc:
|
|
129
|
+
print(f"dragoman: {exc}", file=sys.stderr)
|
|
130
|
+
if exc.report is not None:
|
|
131
|
+
_print_report(exc.report, quiet=False)
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
parser.print_help()
|
|
135
|
+
return 2
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
sys.exit(main())
|
dragoman/_ffi.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Finding and declaring the shared library.
|
|
2
|
+
|
|
3
|
+
ctypes rather than a compiled extension on purpose. Greater Diplomacy 5 is a
|
|
4
|
+
pygame project whose players install it by unzipping it, and asking that
|
|
5
|
+
audience for a C compiler to convert a map is asking too much. ctypes needs
|
|
6
|
+
only the .so/.dylib/.dll, which CI builds for each platform and the wheel
|
|
7
|
+
carries.
|
|
8
|
+
|
|
9
|
+
The search order is: an explicit DRAGOMAN_LIBRARY, then a copy shipped inside
|
|
10
|
+
this package (how a wheel is laid out), then a CMake build tree beside the
|
|
11
|
+
checkout (how a contributor runs it), then the system loader.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import ctypes
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_LIB_NAMES = {
|
|
20
|
+
"win32": ["dragoman.dll", "libdragoman.dll"],
|
|
21
|
+
"darwin": ["libdragoman.dylib"],
|
|
22
|
+
}
|
|
23
|
+
_DEFAULT_NAMES = ["libdragoman.so"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _candidate_names():
|
|
27
|
+
return _LIB_NAMES.get(sys.platform, _DEFAULT_NAMES)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _candidate_paths():
|
|
31
|
+
override = os.environ.get("DRAGOMAN_LIBRARY")
|
|
32
|
+
if override:
|
|
33
|
+
yield Path(override)
|
|
34
|
+
|
|
35
|
+
here = Path(__file__).resolve().parent
|
|
36
|
+
for name in _candidate_names():
|
|
37
|
+
yield here / name
|
|
38
|
+
|
|
39
|
+
# A CMake build tree beside the repository, which is what a contributor
|
|
40
|
+
# who just ran `cmake --build build` has.
|
|
41
|
+
repo = here.parents[2]
|
|
42
|
+
for build in ("build", "cmake-build-debug", "cmake-build-release", "out"):
|
|
43
|
+
for name in _candidate_names():
|
|
44
|
+
yield repo / build / name
|
|
45
|
+
yield repo / build / "Release" / name
|
|
46
|
+
yield repo / build / "Debug" / name
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_library():
|
|
50
|
+
tried = []
|
|
51
|
+
for path in _candidate_paths():
|
|
52
|
+
tried.append(str(path))
|
|
53
|
+
if path.exists():
|
|
54
|
+
return ctypes.CDLL(str(path))
|
|
55
|
+
|
|
56
|
+
# Last resort: let the platform loader look on its own search path.
|
|
57
|
+
for name in _candidate_names():
|
|
58
|
+
try:
|
|
59
|
+
return ctypes.CDLL(name)
|
|
60
|
+
except OSError:
|
|
61
|
+
tried.append(name)
|
|
62
|
+
|
|
63
|
+
raise OSError(
|
|
64
|
+
"could not find the dragoman shared library. Build it with\n"
|
|
65
|
+
" cmake -S . -B build && cmake --build build\n"
|
|
66
|
+
"or set DRAGOMAN_LIBRARY to its path. Looked in:\n "
|
|
67
|
+
+ "\n ".join(tried)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Options(ctypes.Structure):
|
|
72
|
+
_fields_ = [
|
|
73
|
+
("carry_sidecar", ctypes.c_int),
|
|
74
|
+
("derive_geometry", ctypes.c_int),
|
|
75
|
+
("translate_scripts", ctypes.c_int),
|
|
76
|
+
("strict", ctypes.c_int),
|
|
77
|
+
("reencode_images", ctypes.c_int),
|
|
78
|
+
("synthesise_ocean", ctypes.c_int),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def declare(lib):
|
|
83
|
+
"""Give every symbol its signature.
|
|
84
|
+
|
|
85
|
+
Without this ctypes assumes an int return, which silently truncates a
|
|
86
|
+
64-bit pointer on the first call that returns a handle -- the failure
|
|
87
|
+
looks like a corrupt map rather than a binding mistake, so it is worth
|
|
88
|
+
being exhaustive here.
|
|
89
|
+
"""
|
|
90
|
+
c = ctypes
|
|
91
|
+
p = c.c_void_p
|
|
92
|
+
s = c.c_char_p
|
|
93
|
+
|
|
94
|
+
sigs = {
|
|
95
|
+
"dg_version_string": ([], s),
|
|
96
|
+
"dg_version_major": ([], c.c_int),
|
|
97
|
+
"dg_version_minor": ([], c.c_int),
|
|
98
|
+
"dg_version_patch": ([], c.c_int),
|
|
99
|
+
"dg_abi_version": ([], c.c_int),
|
|
100
|
+
"dg_format_name": ([c.c_int], s),
|
|
101
|
+
"dg_detect": ([s], c.c_int),
|
|
102
|
+
"dg_options_defaults": ([c.POINTER(Options)], None),
|
|
103
|
+
"dg_load": ([s, c.c_int, c.POINTER(Options), c.POINTER(p)], p),
|
|
104
|
+
"dg_save": ([p, s, c.c_int, c.POINTER(Options), c.POINTER(p)], c.c_int),
|
|
105
|
+
"dg_convert": ([s, s, c.c_int, c.POINTER(Options), c.POINTER(p)], c.c_int),
|
|
106
|
+
"dg_roundtrip_check": ([s, c.c_int, c.POINTER(Options), c.POINTER(p)], c.c_int),
|
|
107
|
+
"dg_world_free": ([p], None),
|
|
108
|
+
"dg_world_province_count": ([p], c.c_int),
|
|
109
|
+
"dg_world_nation_count": ([p], c.c_int),
|
|
110
|
+
"dg_world_script_count": ([p], c.c_int),
|
|
111
|
+
"dg_world_name": ([p], s),
|
|
112
|
+
"dg_world_origin": ([p], c.c_int),
|
|
113
|
+
"dg_world_to_json": ([p], p),
|
|
114
|
+
"dg_world_from_json": ([s, c.POINTER(p)], p),
|
|
115
|
+
"dg_report_count": ([p], c.c_int),
|
|
116
|
+
"dg_report_severity": ([p, c.c_int], c.c_int),
|
|
117
|
+
"dg_report_code": ([p, c.c_int], s),
|
|
118
|
+
"dg_report_message": ([p, c.c_int], s),
|
|
119
|
+
"dg_report_worst": ([p], c.c_int),
|
|
120
|
+
"dg_report_to_json": ([p], p),
|
|
121
|
+
"dg_report_free": ([p], None),
|
|
122
|
+
"dg_string_free": ([p], None),
|
|
123
|
+
"dg_last_error": ([], s),
|
|
124
|
+
}
|
|
125
|
+
for name, (argtypes, restype) in sigs.items():
|
|
126
|
+
fn = getattr(lib, name)
|
|
127
|
+
fn.argtypes = argtypes
|
|
128
|
+
fn.restype = restype
|
|
129
|
+
return lib
|
dragoman/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
dragoman/dragoman.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: open-dragoman
|
|
3
|
+
Version: 0.2.2
|
|
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.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
dragoman/__init__.py,sha256=jlYzSX1zdfBMNDsoqWJFlR42WCqQdE2PoKyrSWH5A4M,10497
|
|
2
|
+
dragoman/__main__.py,sha256=y6JHFD8TiG3h652J1KR9X3fjZXdClBg_0xJwWEBeQxQ,5473
|
|
3
|
+
dragoman/_ffi.py,sha256=vz9PiWxmYP0GFE6G-1QKSIAbzfsLhKuJhIuopH_TdMQ,4528
|
|
4
|
+
dragoman/_version.py,sha256=anBcPGggCP0f-o9w0IsZYV49rl6HvJbHcuDN39x3mxw,23
|
|
5
|
+
dragoman/dragoman.dll,sha256=MUcqF4m-RpBmYyiMIZIPWWdXoEfGElACZjKRS3LrNvk,703488
|
|
6
|
+
open_dragoman-0.2.2.dist-info/METADATA,sha256=lYx8oAoVnQglETSoL69a4S_v2TxY1-nEeohB5cAI9KU,12004
|
|
7
|
+
open_dragoman-0.2.2.dist-info/WHEEL,sha256=k8ugwGhXAI0--PEoWE0F6sXc-nX2zbZqyTgxRg_imsk,102
|
|
8
|
+
open_dragoman-0.2.2.dist-info/entry_points.txt,sha256=oSIpRxsoPRA5OKPlIPTg4zOt1N937ZUMEyocpf9oIY8,53
|
|
9
|
+
open_dragoman-0.2.2.dist-info/licenses/LICENSE,sha256=P7RsI-F89-a0HjEqhSpzC_oMu9Bii2KZT4r8k__7Gx0,1997
|
|
10
|
+
open_dragoman-0.2.2.dist-info/RECORD,,
|
|
@@ -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
|