Skip to content

Commit b6dca42

Browse files
committed
feat(memory): terrain and height from image POC
1 parent ea04886 commit b6dca42

9 files changed

Lines changed: 217 additions & 0 deletions

File tree

sourcehold/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from sourcehold.tool.argparsers.common import main_parser
1212
from sourcehold.tool.argparsers.services import services_parser, convert_parser
13+
from sourcehold.tool.memory.map import memory_map
1314
from sourcehold.tool.modify.map import modify_map
1415

1516
file_input_output = argparse.ArgumentParser(add_help=False)
@@ -82,6 +83,8 @@ def main():
8283
return
8384
if modify_map(args):
8485
return
86+
if memory_map(args):
87+
return
8588

8689
if args.service == "aiv":
8790
if args.method == "file":

sourcehold/tool/argparsers/services.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import argparse
12
from .common import main_parser, file_input_file_output
23

34
services_parser = main_parser.add_subparsers(title="service", dest="service", required=True)
@@ -11,6 +12,24 @@
1112
convert_aiv_parser.add_argument('--to-format', required=False, default='')
1213

1314
memory_parser = services_parser.add_parser('memory')
15+
memory_parser.add_argument('--game', choices=['SHC1.41-latin', "SHCE1.41-latin"], default="SHC1.41-latin")
16+
memory_subparsers = memory_parser.add_subparsers(dest='type', required=True, title='type')
17+
18+
memory_map_parser = memory_subparsers.add_parser('map')
19+
memory_map_subparsers = memory_map_parser.add_subparsers(dest='action', required=True)
20+
21+
memory_common = argparse.ArgumentParser(add_help=False)
22+
memory_common.add_argument('what', choices=['terrain', 'height'])
23+
24+
memory_map_get_parser = memory_map_subparsers.add_parser('get', parents=[memory_common])
25+
memory_map_get_parser.add_argument('--output', default='')
26+
memory_map_get_parser.add_argument('--output-format', default='', choices=['png'])
27+
28+
memory_map_set_parser = memory_map_subparsers.add_parser('set', parents=[memory_common])
29+
memory_map_set_parser.add_argument('--input', default='-')
30+
memory_map_set_parser.add_argument('--input-format', default='', choices=['png'])
31+
32+
1433

1534
modify_parser = services_parser.add_parser('modify')
1635
modify_subparser = modify_parser.add_subparsers(dest='type', required=True, title='type')

sourcehold/tool/memory/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import pathlib, sys
2+
3+
from sourcehold.tool.convert.aiv.exports import to_json
4+
from sourcehold.tool.convert.aiv.imports import from_json
5+
from sourcehold.tool.memory.map.height import set_height
6+
from sourcehold.tool.memory.map.terrain import set_terrain
7+
8+
9+
def memory_map(args):
10+
#' returns None in case of non applicable
11+
if args.service != "memory":
12+
return None
13+
14+
if args.type != "map":
15+
return None
16+
17+
if set_height(args):
18+
return True
19+
20+
if set_terrain(args):
21+
return True
22+
23+
return True
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pathlib
2+
from sourcehold.debugtools.memory.access import SHC, SHCE
3+
4+
def get_process_handle(version):
5+
if version == "SHC1.41-latin":
6+
return SHC()
7+
# if version == "SHCE1.41-latin":
8+
# return SHCE()
9+
raise NotImplementedError(f"process not implemented: {version}")
10+
11+
12+
def validate_path(img_path):
13+
if not img_path:
14+
raise Exception(f"no input file specified")
15+
if img_path == "-":
16+
raise NotImplementedError(f"stdin input not yet implemented for this action. Specify a file path using --input")
17+
18+
if not pathlib.Path(img_path).exists():
19+
raise Exception(f"file does not exist: {img_path}")
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
3+
4+
# python -m pip install Pillow
5+
import pathlib
6+
import struct
7+
from sourcehold.tool.memory.map.common import get_process_handle, validate_path
8+
from sourcehold.world import create_selection_matrix
9+
import cv2 as cv # type: ignore
10+
11+
def get_image_data_grayscale(img_path):
12+
img = cv.imread(img_path)
13+
img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
14+
return img
15+
16+
selection = create_selection_matrix()
17+
18+
# (Little endian) unsigned bytes
19+
def get_raw_height(process):
20+
return struct.unpack("<80400B", process.read_section('1045'))
21+
22+
def set_raw_height(process, data):
23+
bytes_data = struct.pack("<80400B", *data)
24+
# ChangedLayer
25+
process.write_bytes(0x01c5ad88, b'\x02' * 80400) # TODO: fix
26+
# Logical terrain height layer: DefaultHeightLayer
27+
process.write_section('1045', bytes_data)
28+
# Visual height layer, I think also includes walls and towers: HeightLayer
29+
process.write_section('1005', bytes_data)
30+
# LogicLayer
31+
process.write_section('1003', struct.pack("<80400I", *((v & 0xffffff7f) for v in struct.unpack("<80400I", process.read_section('1003')))))
32+
# Logic2Layer
33+
process.write_section('1037', b'\x04' * 80400)
34+
35+
# def post_process_raw_height():
36+
# # MiscDisplayLayer
37+
# process.write_section('1007', struct.pack("<80400H", *(((v & 0xffdf) & 0xf83f) for v in struct.unpack("<80400H", process.read_section('1007')))))
38+
# # LogicLayer, what a hot mess, probably not all required
39+
# process.write_section('1003', struct.pack("<80400I", *(((((v & 0x5f81c436) & 0xffffff7f) & 0xbfffbfff) | 32768) for v in struct.unpack("<80400I", process.read_section('1003')))))
40+
# # Logic2Layer
41+
# process.write_section('1037', b'\x04' * 80400)
42+
# # TODO: wall owner layer, and special logic2layer set to 4 or 8 depending on plateau
43+
# # ChangedLayer
44+
# process.write_bytes(0x01c5ad88, b'\x02' * 80400)
45+
46+
47+
# post_process_raw_height()
48+
49+
50+
def set_height(args):
51+
#' returns None in case of non applicable
52+
if args.what != 'height':
53+
return None
54+
55+
if args.action != "set":
56+
return None
57+
58+
img_path = args.input
59+
validate_path(img_path)
60+
61+
img = get_image_data_grayscale(img_path)
62+
63+
process = get_process_handle(args.game)
64+
65+
set_raw_height(process, img[selection].flat)
66+
67+
return True
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
MAP_SIZE = 400
2+
TILE_COUNT = MAP_SIZE * ((MAP_SIZE // 2) + 1 )
3+
# python -m pip install Pillow
4+
import struct, numpy
5+
from sourcehold.tool.memory.map.common import get_process_handle, validate_path
6+
from sourcehold.world import create_selection_matrix
7+
import cv2 as cv # type: ignore
8+
9+
10+
def get_image_data(img_path):
11+
img = cv.imread(img_path)
12+
#img = 255 - cv.cvtColor(img, cv.COLOR_BGR2GRAY)
13+
return img
14+
15+
selection = create_selection_matrix(size=MAP_SIZE)
16+
17+
# (Little endian) unsigned bytes
18+
def get_raw_logical(process):
19+
return struct.unpack(f"<{TILE_COUNT}I", process.read_section('1003'))
20+
21+
def set_raw_logical(process, data):
22+
serialized = struct.pack(f"<{TILE_COUNT}I", *data)
23+
# Logical terrain height layer
24+
process.write_section('1003', serialized)
25+
26+
27+
# 16 is used for the inaccessible parts of the map, including the outer border of the 800x800 space, and 32 is used for a border just within that
28+
29+
def set_terrain(args):
30+
#' returns None in case of non applicable
31+
if args.what != 'terrain':
32+
return None
33+
34+
if args.action != "set":
35+
return None
36+
37+
img_path = args.input
38+
validate_path(img_path)
39+
img = get_image_data(img_path)
40+
41+
process = get_process_handle(args.game)
42+
43+
matrix = numpy.zeros((400, 400), dtype='uint32')
44+
matrix[selection] = get_raw_logical(process)
45+
46+
# matrix[img > 255//128] |= 0x20000 # boulder flag?
47+
48+
set_raw_logical(process, matrix[selection].flat)
49+
50+
return True
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
monsterfish1 = {
2+
'swamp': "#475937",
3+
'pitch': '#314235',
4+
'stone': '#c3bdb4',
5+
'gravel': '#978f80',
6+
'rocks': '#675335',
7+
'iron': '#9e4f00',
8+
'ford': '#567c71',
9+
'river': '#427068',
10+
'ocean': '#1e4a44',
11+
'oasis': '#47540b',
12+
'thick_scrub': '#6a692b',
13+
'light_scrub': '#937e44',
14+
'earth_and_stones': '#7c7059',
15+
'earth': '#ae9467',
16+
'dunes': '#b79453',
17+
'beach': '#deb977',
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
logic1 = {
2+
'swamp': 1 << 1, # TODO
3+
'pitch': '#314235',
4+
'stone': '#c3bdb4',
5+
'gravel': '#978f80',
6+
'rocks': '#675335',
7+
'iron': '#9e4f00',
8+
'ford': '#567c71',
9+
'river': '#427068',
10+
'ocean': '#1e4a44',
11+
'oasis': '#47540b',
12+
'thick_scrub': '#6a692b',
13+
'light_scrub': '#937e44',
14+
'earth_and_stones': '#7c7059',
15+
'earth': '#ae9467',
16+
'dunes': '#b79453',
17+
'beach': '#deb977',
18+
}

0 commit comments

Comments
 (0)