Metadata-Version: 2.4
Name: dmcsb
Version: 0.1.0
Summary: Dungeon Master Tools — extract data from Dungeon Master / Chaos Strikes Back files (GRAPHICS.DAT, DUNGEON.DAT)
Author: Cycl0ne
License: Datenlizenz Deutschland - Zero - 2.0
Project-URL: Homepage, https://codeberg.org/CyCl0ne/DungeonMasterTools
Project-URL: Repository, https://codeberg.org/CyCl0ne/DungeonMasterTools
Project-URL: Format reference, http://dmweb.free.fr/?q=node/217
Keywords: dungeon-master,chaos-strikes-back,amiga,atari-st,retro,graphics.dat
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Public Domain
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pil
Requires-Dist: Pillow; extra == "pil"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Dynamic: license-file

# Dungeon Master Tools
Dungeon Master Tools is a collection of Tools in Python for the Amiga/PC/Atari ST Game,
packaged as the importable `dmcsb` package (published on PyPI as `dmcsb` — `pip install dmcsb`):

* `dmcsb.dungeon` (`DungeonFile`) - loads and extracts the data in DUNGEON.DAT (Big Endian only for now)
* `dmcsb.graphics` (`GraphicsFile`) - library for GRAPHICS.DAT handling

## Installation

```
pip install dmcsb              # optional: "dmcsb[pil]" enables GraphicsFile.to_pil()
```

or, from a source checkout, `pip install -e .` (or just run from the repo root —
the package is importable without installing).

## Command line

Installing puts a `dmcsb` command on your `PATH` (equivalently `python3 -m dmcsb`):

```
dmcsb dump GRAPHICS.DAT [out_dir]      # -> PNG/WAV/TXT/.bin + an HTML contact sheet
dmcsb dungeon DUNGEON.DAT --level 1    # print one dungeon level (map, things, champions)
dmcsb uncompress DUNGEON.DAT [out.dat] # write a decompressed copy (no parsing)
```

`dmcsb dump` also takes `--map` / `--palettes` / `--zones` to override the bundled
tables. The same three operations are available as standalone scripts in `examples/`
(thin wrappers over the library, runnable from a source checkout).

After loading a `DungeonFile` you can access all data from the object:

* `hdr` -> header dict
* `maps[]` / `mapsinfo[str(level)]` -> map descriptors (in file order / keyed by Level)
* `thinglist[]` -> 16 entries in storage order (`None` for unused types)
* `things[name]` -> decoded list by type: `"Door"`, `"Creature"`, `"Weapon"`, `"Sensor"`, ... (whole dungeon)
* `things_on_level(level)` -> things located on one level, grouped by type
* `located_things(level)` -> things on a level with their map `(x, y)` + sub-square cell (`dir` N/E/S/W)
* `item_name(category, type)` -> human item name (Weapon/Armour/Potion/Junk/Container/Scroll)
* `text(textstring)` / `texts()` -> decoded message strings (signs, ...)
* `scroll_text(scroll)` -> the text a scroll points at (via its TextStringThingIndex)
* `champions()` -> all champion sheets in the dungeon (name, title, gender, stats, skills); `champions_on_level(level)` -> only those whose sheet sits on that level (e.g. the Hall of Champions), with `x,y`; `champion(offset)` decodes one
* `tile_data` -> raw tile bytes; `tile_grid(level)` -> 2-D tile array
* `text_data` -> raw encoded dungeon text
* `chksum` -> trailing checksum word, or `None` if the file has none

for more info see: 
http://dmweb.free.fr/?q=node/217

The library is silent (no prints); load from a path, bytes, or an open file. Little-endian
(PC) files are not supported yet and raise `NotImplementedError`.

```python
from dmcsb import open_dungeon

d = open_dungeon("DUNGEON.DAT")          # or open_dungeon(data=raw_bytes)
print(d.hdr['OrnamentRandomSeed'])
print(d.mapsinfo['1']['Difficulty'])
for door in d.things['Door']:                 # every door in the dungeon
    print(door['Type'], door['OrnamentOrdinal'])

for name, items in d.things_on_level(1).items():   # only level 1, grouped by type
    print(name, len(items))

for i, visible, msg in d.texts():             # decoded scroll/sign messages
    print(i, repr(msg))
for c in d.champions():                       # initial party champions
    print(c['name'], c['title'], c['Health'], c['Strength'])
```

Text uses DM's 5-bit packing (3 codes per 16-bit word). The decoder handles
letters, spaces, `.`, line breaks and the standard DM/CSB escape tables (so e.g.
*THE* / *YOU* expand inside titles and messages); pass an `escape=` callback to
override them. It targets the normal/"new font" text used by the Amiga versions.
Champion statistics/skills are stored as hex nibbles and decoded to integers
(`Stamina` is kept as the raw value, i.e. 10x the number shown in-game).

---
When you run the demo, it prints one dungeon level:

```
python3 examples/demo_dungeon.py [level] [DUNGEON.DAT]
```

`level` defaults to `1`; the optional second argument is the path to the file
(defaults to `DUNGEON.DAT`). For example `python3 examples/demo_dungeon.py 5`
shows Level 5. With no arguments, Level 1 is shown:

```
Map at Level 1 ----------------
  RawMapDataByteOffset: 376
  OffsetMapX: 0, OffsetMapY: 14
  Width: 31, Height: 31, Level: 1
  RandomFloorOrnamentCount: 2
  FloorOrnamentCount: 3
  RandomWallOrnamentCount: 3
  WallOrnamentCount: 12
  Difficulty: 1
  CreatureTypeCount: 2
  DoorOrnamentCount: 3
  DoorSet1: 1, DoorSet0: 0
  WallSet: 0, FloorSet: 0

Creatures/Wall/Floor/Doors used ----------------
Creature: 6, 10, 
WallOrnate: 33, 4, 35, 51, 15, 1, 49, 38, 46, 45, 44, 5, 
FloorOrnate: 2, 8, 1, 
DoorDeco: 4, 8, 3, 

MapData ----------------

  1 4 1 1 1 1 1 1   1 1 4 1 1   1 1 1         1     1 1 4 1   1 1 
  1     3     4     1       1 1 1 1 1 4 4 1   1 1 1 1     1   1 5 
  1 1         1   1 1 1 1       1 1 1     1   1 1       1 1 1     
    4         1   1 1   1   1 1           1   1   1 1 1     1 5   
    1                   1   1 1 1 1 1 1 1 5   4   1   1 4 1 1     
  1 1   1 4 1 4 1 1   1 1   1   1 1 1 1     1 1   2               
  1     1             1           1       1 1     1 1           1 
  1 1 1 1   1 1 1     1   1 1 1   1 1 1 1 1 1       1   1 1 1 1 1 
  1     1   1   2     1   1 1 1               1 1 1 1   1   1   4 
    1 1 1   4   1   1 1   1         1 1 1 1 1 1         1   1   1 
    1       1       1   1 1   1 1   1         1 1 1 1 1 1   1   1 
  1 1 1 1 1 1   1   1   1 1 1 1 1   1   1   1       4       1     
  1             1 1 1   4               1   1 1 1   1 1 1   1 1   
  1 1 1 4 1 1 4 1       1         1 1 1 1 1 1   1 1     1         
    1                 1 1   1 1   1 1 1 1 1       1 1   1 1 1 1 1 
    1 1 1 1 4 1 1 1 1 1     2     1 1 1 1 1   1     1       1   1 
      1 1 1               1 5     1 1 1 1 1   1 1 1 1 1 1 1 1   1 
              1 1 1 1     1       1 1 1 1 1         1 1 1 1 1   1 
  1 1 1 1 1 1 1     1     1   1       4       1                   
  1 1 1 1 1 1 1   1 1     1   1 1 1 1 1 1 1 1 1         1 1 1 1   
  1   1 1 1 1 1   1   1 1 1           4           1 1 1 1 1 1 1 5 
  1 1 1         1 1   1 1 1   1 1 1   5 1 1 1 4 1 1 1 1 1 1     1 
    1           4     1       1   1 1 1   1                     1 
    1 1 1   1 1 1 1   1 1 1 1 1                     1   1 1 1 1 1 
        1   1 1 1 1                     1 1 1   1 1 1   1   1     
        1       1 1 1 1 1   1 1 1       1   1   1   1 1 1   1 1   
        1     3   1 1   1 1 1   1 1 4 1 1   1   1                 
        1     1     1 1   1       4         1   1 1 1 1 1 1 4 1   
        1     1     1 1   1 1 1   1       1 1 1     1         1   
        1     1 1 1       1 1 6           1 1 1 1 1 1 5 1 1 1 1   
      1 1 1 1     1       1   1 1         1 1 1                   
      1 1   1 1 1 1           1 1                   
```

The demo also prints every Thing on the level (grouped by type) after the map.

To just decompress a compressed dungeon to a raw file (no parsing):

```
python3 examples/uncompress_dungeon.py DUNGEON.DAT [output.dat]
```

The output defaults to `<input>.uncompressed`.

## dmcsb package — GRAPHICS.DAT library

The graphics handling lives in the installable `dmcsb` package. It loads the
Amiga 3.x `GRAPHICS.DAT` into memory and decodes IMG1 images, the FNT1 font, SND2
sounds, TXT2 text and the LAY1 screen layout. The data tables (image map, palettes,
zone names) are **bundled inside the package and applied automatically** — you can
override them by passing your own paths.

Use it in your own project (after `pip install dmcsb` — see [Installation](#installation)):

```python
from dmcsb import open_graphics

g = open_graphics("GRAPHICS.DAT")   # bundled map + palettes auto-applied
# overrides: open_graphics("GRAPHICS.DAT", map="my_map.txt", palettes="my_pal.txt")
# from memory: open_graphics(data=open("GRAPHICS.DAT","rb").read())

len(g)                  # number of items
list(g)                 # [(index, type_name), ...]
g.info(i)               # {type, name, palette, sizes, compressed}

g.image(i)              # IMG1/FNT1 -> (w, h, rgb_bytes)
g.image_indexed(i)      # -> (w, h, palette_indices) for your own coloring
g.to_pil(i)             # -> PIL.Image (needs Pillow)
g.font()                # finds FNT1 -> (w, h, rgb_bytes)
g.sound(i)              # SND2 -> signed 8-bit PCM
g.text(i)               # TXT2 -> [str]
g.layout()              # LAY1 -> {'signature','ranges','records'} (relative coords)
g.first_of_type("LAY1") /  g.indices_of_type("SND")
```

To dump everything into a folder of PNG/WAV/TXT plus an HTML contact sheet, see the
example app (it consumes the library):

```
python3 examples/dump_graphics.py GRAPHICS.DAT gfx/
# optional overrides: ... gfx/ graphics_map.txt palettes.txt zones.txt
```


---
Many Thanks to ChristopheF & Sphenx
