Skip to content

Commit 3fe86a5

Browse files
committed
Add modern REXPaint load/save functions.
1 parent 802a666 commit 3fe86a5

File tree

5 files changed

+127
-4
lines changed

5 files changed

+127
-4
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ v2.0.0
88

99
Unreleased
1010
------------------
11+
Added
12+
- Added modernized REXPaint saving/loading functions.
13+
- `tcod.console.load_xp`
14+
- `tcod.console.save_xp`
15+
16+
Changed
17+
- Using `libtcod 1.18.0`.
1118

1219
12.3.2 - 2021-05-15
1320
-------------------

tcod/console.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
To render a console you need a tileset and a window to render to.
44
See :ref:`getting-started` for info on how to set those up.
55
"""
6-
6+
import os
7+
import pathlib
78
import warnings
8-
from typing import Any, Optional, Tuple, Union # noqa: F401
9+
from typing import Any, Iterable, Optional, Tuple, Union
910

1011
import numpy as np
1112
from typing_extensions import Literal
@@ -1229,3 +1230,102 @@ def recommended_size() -> Tuple[int, int]:
12291230
w = max(1, xy[0] // lib.TCOD_ctx.tileset.tile_width)
12301231
h = max(1, xy[1] // lib.TCOD_ctx.tileset.tile_height)
12311232
return w, h
1233+
1234+
1235+
def load_xp(
1236+
path: Union[str, pathlib.Path], order: Literal["C", "F"] = "C"
1237+
) -> Tuple[Console, ...]:
1238+
"""Load a REXPaint file as a tuple of consoles.
1239+
1240+
`path` is the name of the REXPaint file to load.
1241+
Usually ending with `.xp`.
1242+
1243+
`order` is the memory order of the Console's array buffer,
1244+
see :any:`tcod.console.Console`.
1245+
1246+
.. versionadded:: 12.4
1247+
1248+
Example::
1249+
import tcod
1250+
from numpy import np
1251+
1252+
path = "example.xp" # REXPaint file with one layer.
1253+
1254+
# Load a REXPaint file with a single layer.
1255+
# The comma after console is used to unpack a single item tuple.
1256+
console, = tcod.console.load_xp(path, order="F")
1257+
1258+
# Convert from REXPaint's encoding to Unicode.
1259+
CP437_TO_UNICODE = np.asarray(tcod.tileset.CHARMAP_CP437)
1260+
console.ch[:] = CP437_TO_UNICODE[console.ch]
1261+
1262+
# Apply REXPaint's alpha key color.
1263+
KEY_COLOR = (255, 0, 255)
1264+
is_transparent = console.rgb["bg"] == KEY_COLOR
1265+
console.rgba[is_transparent] = (ord(" "), (0,), (0,))
1266+
"""
1267+
if not os.path.exists(path):
1268+
raise FileNotFoundError(f"File not found:\n\t{os.path.abspath(path)}")
1269+
layers = _check(
1270+
tcod.lib.TCOD_load_xp(str(path).encode("utf-8"), 0, ffi.NULL)
1271+
)
1272+
consoles = ffi.new("TCOD_Console*[]", layers)
1273+
_check(tcod.lib.TCOD_load_xp(str(path).encode("utf-8"), layers, consoles))
1274+
return tuple(
1275+
Console._from_cdata(console_p, order=order) for console_p in consoles
1276+
)
1277+
1278+
1279+
def save_xp(
1280+
path: Union[str, pathlib.Path],
1281+
consoles: Iterable[Console],
1282+
compress_level: int = 9,
1283+
) -> None:
1284+
"""Save tcod Consoles to a REXPaint file.
1285+
1286+
`path` is where to save the file.
1287+
1288+
`consoles` are the :any:`tcod.console.Console` objects to be saved.
1289+
1290+
`compress_level` is the zlib compression level to be used.
1291+
1292+
Color alpha will be lost during saving.
1293+
1294+
Consoles will be saved as-is as much as possible. You may need to convert
1295+
characters from Unicode to CP437 if you want to load the file in REXPaint.
1296+
1297+
.. versionadded:: 12.4
1298+
1299+
Example::
1300+
import tcod
1301+
from numpy import np
1302+
1303+
console = tcod.Console(80, 24) # Example console.
1304+
1305+
# Load a REXPaint file with a single layer.
1306+
# The comma after console is used to unpack a single item tuple.
1307+
console, = tcod.console.load_xp(path, order="F")
1308+
1309+
# Convert from Unicode to REXPaint's encoding.
1310+
# Required to load this console correctly in the REXPaint tool.
1311+
CP437_TO_UNICODE = np.asarray(tcod.tileset.CHARMAP_CP437)
1312+
UNICODE_TO_CP437 = np.full(0x1FFFF, fill_value=ord("?"))
1313+
UNICODE_TO_CP437[CP437_TO_UNICODE] = np.arange(len(CP437_TO_UNICODE))
1314+
console.ch[:] = UNICODE_TO_CP437[console.ch]
1315+
1316+
# Convert console alpha into REXPaint's alpha key color.
1317+
KEY_COLOR = (255, 0, 255)
1318+
is_transparent = console.rgba["bg"][:, :, 3] == 0
1319+
console.rgb["bg"][is_transparent] = KEY_COLOR
1320+
1321+
tcod.console.save_xp("example.xp", [console])
1322+
"""
1323+
consoles_c = ffi.new("TCOD_Console*[]", [c.console_c for c in consoles])
1324+
_check(
1325+
tcod.lib.TCOD_save_xp(
1326+
len(consoles_c),
1327+
consoles_c,
1328+
str(path).encode("utf-8"),
1329+
compress_level,
1330+
)
1331+
)

tcod/libtcodpy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2078,7 +2078,7 @@ def console_save_apf(con: tcod.console.Console, filename: str) -> bool:
20782078
)
20792079

20802080

2081-
@deprecate("Use tcod.console_from_xp to load this file.")
2081+
@deprecate("Use tcod.console.load_xp to load this file.")
20822082
def console_load_xp(con: tcod.console.Console, filename: str) -> bool:
20832083
"""Update a console from a REXPaint `.xp` file.
20842084
@@ -2091,6 +2091,7 @@ def console_load_xp(con: tcod.console.Console, filename: str) -> bool:
20912091
)
20922092

20932093

2094+
@deprecate("Use tcod.console.save_xp to save this console.")
20942095
def console_save_xp(
20952096
con: tcod.console.Console, filename: str, compress_level: int = 9
20962097
) -> bool:
@@ -2102,6 +2103,7 @@ def console_save_xp(
21022103
)
21032104

21042105

2106+
@deprecate("Use tcod.console.load_xp to load this file.")
21052107
def console_from_xp(filename: str) -> tcod.console.Console:
21062108
"""Return a single console from a REXPaint `.xp` file."""
21072109
if not os.path.exists(filename):
@@ -2113,6 +2115,7 @@ def console_from_xp(filename: str) -> tcod.console.Console:
21132115
)
21142116

21152117

2118+
@deprecate("Use tcod.console.load_xp to load this file.")
21162119
def console_list_load_xp(
21172120
filename: str,
21182121
) -> Optional[List[tcod.console.Console]]:
@@ -2136,6 +2139,7 @@ def console_list_load_xp(
21362139
lib.TCOD_list_delete(tcod_list)
21372140

21382141

2142+
@deprecate("Use tcod.console.save_xp to save these consoles.")
21392143
def console_list_save_xp(
21402144
console_list: Sequence[tcod.console.Console],
21412145
filename: str,

tests/test_console.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,15 @@ def test_console_semigraphics():
129129
console.draw_semigraphics(
130130
[[[255, 255, 255], [255, 255, 255]], [[255, 255, 255], [0, 0, 0]]],
131131
)
132+
133+
def test_rexpaint(tmp_path) -> None:
134+
xp_path = tmp_path / "test.xp"
135+
consoles = tcod.Console(80, 24, order="F"), tcod.Console(8, 8, order="F")
136+
tcod.console.save_xp(xp_path, consoles, compress_level=0)
137+
loaded = tcod.console.load_xp(xp_path, order="F")
138+
assert len(consoles) == len(loaded)
139+
assert loaded[0].rgba.flags["F_CONTIGUOUS"]
140+
assert consoles[0].rgb.shape == loaded[0].rgb.shape
141+
assert consoles[1].rgb.shape == loaded[1].rgb.shape
142+
with pytest.raises(FileNotFoundError):
143+
tcod.console.load_xp(tmp_path / "non_existant")

0 commit comments

Comments
 (0)