mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-05-19 13:20:47 +01:00
9f124e5b14
* Initial * Remove corrupt dmis * Fixup maps in TGM format4e5a32721f: maps/_templates_and_guidance/Public Event Templates/Maze_Reward_-_Copy.dmm4e5a32721f: maps/_templates_and_guidance/Templates/shelter_Medical.dmm4e5a32721f: maps/expedition_vr/aerostat/aerostat.dmm4e5a32721f: maps/expedition_vr/aerostat/aerostat_science_outpost.dmm4e5a32721f: maps/expedition_vr/beach/submaps/deadBeacon.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-crash-alt.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-crash.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-surface.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-temple.dmm4e5a32721f: maps/gateway_vr/lucky_7.dmm4e5a32721f: maps/gateway_vr/snow_outpost.dmm4e5a32721f: maps/overmap/_map.dmm4e5a32721f: maps/overmap/bearcat/bearcat.dmm4e5a32721f: maps/overmap/example_sector1.dmm4e5a32721f: maps/overmap/example_sector2.dmm4e5a32721f: maps/redgate/falls/falls.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/CaveS.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/DeadSettlers1.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/DeadSettlers2.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/DoomP.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/Lab1.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/Rockybase.dmm4e5a32721f: maps/submaps/pois_vr/debris_field/debris14.dmm4e5a32721f: maps/submaps/pois_vr/debris_field/derelict.dmm4e5a32721f: maps/submaps/pois_vr/debris_field/new_escapepod_xeno.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/BlastMine1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/CaveTrench.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Cavelake.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Cliff1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/CrashedMedShuttle1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Geyser1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Geyser2.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Geyser3.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Mineshaft1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Scave1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/SupplyDrop1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crashed_ufo.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crashed_ufo_frigate.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crashedcontainmentshuttle.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crystal1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crystal2.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crystal3.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/deadBeacon.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/deadly_rabbit_vr.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/deadspy.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/digsite.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/excavation1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/lava_trench.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/prepper1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/ritual.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/spatial_anomaly.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/speakeasy_vr.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault2.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault3.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault4.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault5.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault6.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Boathouse.dmm4e5a32721f: maps/submaps/surface_submaps/plains/BuriedTreasure.dmm4e5a32721f: maps/submaps/surface_submaps/plains/BuriedTreasure2.dmm4e5a32721f: maps/submaps/surface_submaps/plains/BuriedTreasure3.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Oldhouse.dmm4e5a32721f: maps/submaps/surface_submaps/plains/PooledR.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Rocky5.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Shakden.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Thiefc.dmm4e5a32721f: maps/submaps/surface_submaps/plains/beacons.dmm4e5a32721f: maps/submaps/surface_submaps/plains/chemspill1.dmm4e5a32721f: maps/submaps/surface_submaps/plains/farm1.dmm4e5a32721f: maps/submaps/surface_submaps/plains/house1.dmm4e5a32721f: maps/submaps/surface_submaps/plains/lonehome.dmm4e5a32721f: maps/submaps/surface_submaps/plains/smol2.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Blackshuttledown.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Blueshuttledown.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Boombase.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/CaveS.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Chapel.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Cragzone1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost2.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost3.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost4.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DecoupledEngine.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DoomP.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Drugden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Epod3.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Epod4.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Flake.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/FrostflyNest.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/MCamp1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/MHR.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Manor1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Mudpit.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rocky1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rocky3.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rocky4.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rockybase.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Shack1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Shelter.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Smol1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Snowrock1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/borglab.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/butchershack.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/chasm.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/chemspill2.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/deathden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/derelictengine.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/frostoasis.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/kururakden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/spider1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/wolfden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/xenohive.dmm4e5a32721f: maps/tether/tether-02-surface2.dmm4e5a32721f: maps/virgo_minitest/virgo_minitest-sector-2.dmm Automatically commited by: tools\mapmerge2\fixup.py * Remove unnecessary whitespace edits from mapmerger * Cable dirs update path * Fix area var edits * Put the area over there * Ignore archive maps folder * Forgot to port multivar support too * A few changes I forgot about for hook install * restore multivar support that chomp doesn't have yet * ban those * Forgot to add code for the marker too * Couple more of these invalid cables were added in master * Update multiple_blood_effects.yml * Update multiple_blood_effects.yml * Fixup maps in TGM format612ca9cbb9: maps/tether/submaps/tether_misc.dmm Automatically commited by: tools\mapmerge2\fixup.py * Fixup now logs the map its currently checking * Final fixes? * Fixup maps in TGM format3078e5cd0a: maps/expedition_vr/beach/submaps/crashedcontainmentshuttle.dmm3078e5cd0a: maps/redgate/fantasy_dungeon.dmm3078e5cd0a: maps/submaps/pois_vr/aerostat/Rockybase.dmm3078e5cd0a: maps/submaps/surface_submaps/mountains/crashedcontainmentshuttle_vr.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/Oldhouse_vr.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/dogbase.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/greatwolfden.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/lonehome_vr.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/methlab.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/oldhotel.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/priderock.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/Rockybase.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/demonpool.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/dogbase.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/greatwolfden.dmm3078e5cd0a: maps/tether/submaps/underdark_pois/abandonded_outpost.dmm3078e5cd0a: maps/tether/submaps/underdark_pois/phoron_rat_den.dmm Automatically commited by: tools\mapmerge2\fixup.py * Fix tether_misc error * Remap reused solar farm area * Fix erroneous bearcat entries * Fix weird whitespace (most archive maps also affected but didn't bother) * misc mdb cleanup * moar * grr --------- Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> Co-authored-by: Cameron Lennox <killer65311@gmail.com>
248 lines
7.3 KiB
Python
248 lines
7.3 KiB
Python
# Tools for working with modern DreamMaker icon files (PNGs + metadata)
|
|
|
|
import math
|
|
from PIL import Image
|
|
from PIL.PngImagePlugin import PngInfo
|
|
|
|
DEFAULT_SIZE = 32, 32
|
|
LOOP_UNLIMITED = 0
|
|
LOOP_ONCE = 1
|
|
|
|
NORTH = 1
|
|
SOUTH = 2
|
|
EAST = 4
|
|
WEST = 8
|
|
SOUTHEAST = SOUTH | EAST
|
|
SOUTHWEST = SOUTH | WEST
|
|
NORTHEAST = NORTH | EAST
|
|
NORTHWEST = NORTH | WEST
|
|
|
|
CARDINALS = [NORTH, SOUTH, EAST, WEST]
|
|
DIR_ORDER = [SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST]
|
|
DIR_NAMES = {
|
|
'SOUTH': SOUTH,
|
|
'NORTH': NORTH,
|
|
'EAST': EAST,
|
|
'WEST': WEST,
|
|
'SOUTHEAST': SOUTHEAST,
|
|
'SOUTHWEST': SOUTHWEST,
|
|
'NORTHEAST': NORTHEAST,
|
|
'NORTHWEST': NORTHWEST,
|
|
**{str(x): x for x in DIR_ORDER},
|
|
**{x: x for x in DIR_ORDER},
|
|
'0': SOUTH,
|
|
None: SOUTH,
|
|
}
|
|
|
|
|
|
class Dmi:
|
|
version = "4.0"
|
|
|
|
def __init__(self, width, height):
|
|
self.width = width
|
|
self.height = height
|
|
self.states = []
|
|
|
|
@classmethod
|
|
def from_file(cls, fname):
|
|
image = Image.open(fname)
|
|
if image.mode != 'RGBA':
|
|
image = image.convert('RGBA')
|
|
|
|
# no metadata = regular image file
|
|
if 'Description' not in image.info:
|
|
dmi = Dmi(*image.size)
|
|
state = dmi.state("")
|
|
state.frame(image)
|
|
return dmi
|
|
|
|
# read metadata
|
|
metadata = image.info['Description']
|
|
line_iter = iter(metadata.splitlines())
|
|
assert next(line_iter) == "# BEGIN DMI"
|
|
assert next(line_iter) == f"version = {cls.version}"
|
|
|
|
dmi = Dmi(*DEFAULT_SIZE)
|
|
state = None
|
|
|
|
for line in line_iter:
|
|
if line == "# END DMI":
|
|
break
|
|
key, value = line.lstrip().split(" = ")
|
|
if key == 'width':
|
|
dmi.width = int(value)
|
|
elif key == 'height':
|
|
dmi.height = int(value)
|
|
elif key == 'state':
|
|
state = dmi.state(unescape(value))
|
|
elif key == 'dirs':
|
|
state.dirs = int(value)
|
|
elif key == 'frames':
|
|
state._nframes = int(value)
|
|
elif key == 'delay':
|
|
state.delays = [parse_num(x) for x in value.split(',')]
|
|
elif key == 'loop':
|
|
state.loop = int(value)
|
|
elif key == 'rewind':
|
|
state.rewind = parse_bool(value)
|
|
elif key == 'hotspot':
|
|
x, y, frm = [int(x) for x in value.split(',')]
|
|
state.hotspot(frm - 1, x, y)
|
|
elif key == 'movement':
|
|
state.movement = parse_bool(value)
|
|
else:
|
|
raise NotImplementedError(key)
|
|
|
|
# cut image into frames
|
|
width, height = image.size
|
|
gridwidth = width // dmi.width
|
|
i = 0
|
|
for state in dmi.states:
|
|
for frame in range(state._nframes):
|
|
for dir in range(state.dirs):
|
|
px = dmi.width * (i % gridwidth)
|
|
py = dmi.height * (i // gridwidth)
|
|
im = image.crop((px, py, px + dmi.width, py + dmi.height))
|
|
assert im.size == (dmi.width, dmi.height)
|
|
state.frames.append(im)
|
|
i += 1
|
|
state._nframes = None
|
|
|
|
return dmi
|
|
|
|
def state(self, *args, **kwargs):
|
|
s = State(self, *args, **kwargs)
|
|
self.states.append(s)
|
|
return s
|
|
|
|
@property
|
|
def default_state(self):
|
|
return self.states[0]
|
|
|
|
def get_state(self, name):
|
|
for state in self.states:
|
|
if state.name == name:
|
|
return state
|
|
raise KeyError(name)
|
|
|
|
def _assemble_comment(self):
|
|
comment = "# BEGIN DMI\n"
|
|
comment += f"version = {self.version}\n"
|
|
comment += f"\twidth = {self.width}\n"
|
|
comment += f"\theight = {self.height}\n"
|
|
for state in self.states:
|
|
comment += f"state = {escape(state.name)}\n"
|
|
comment += f"\tdirs = {state.dirs}\n"
|
|
comment += f"\tframes = {state.framecount}\n"
|
|
if state.framecount > 1 and len(state.delays): # any(x != 1 for x in state.delays):
|
|
comment += "\tdelay = " + ",".join(map(str, state.delays)) + "\n"
|
|
if state.loop != 0:
|
|
comment += f"\tloop = {state.loop}\n"
|
|
if state.rewind:
|
|
comment += "\trewind = 1\n"
|
|
if state.movement:
|
|
comment += "\tmovement = 1\n"
|
|
if state.hotspots and any(state.hotspots):
|
|
current = None
|
|
for i, value in enumerate(state.hotspots):
|
|
if value != current:
|
|
x, y = value
|
|
comment += f"\thotspot = {x},{y},{i + 1}\n"
|
|
current = value
|
|
comment += "# END DMI"
|
|
return comment
|
|
|
|
def to_file(self, filename, *, palette=False):
|
|
# assemble comment
|
|
comment = self._assemble_comment()
|
|
|
|
# assemble spritesheet
|
|
W, H = self.width, self.height
|
|
num_frames = sum(len(state.frames) for state in self.states)
|
|
sqrt = math.ceil(math.sqrt(num_frames))
|
|
output = Image.new('RGBA', (sqrt * W, math.ceil(num_frames / sqrt) * H))
|
|
|
|
i = 0
|
|
for state in self.states:
|
|
for frame in state.frames:
|
|
output.paste(frame, ((i % sqrt) * W, (i // sqrt) * H))
|
|
i += 1
|
|
|
|
# save
|
|
pnginfo = PngInfo()
|
|
pnginfo.add_text('Description', comment, zip=True)
|
|
if palette:
|
|
output = output.convert('P')
|
|
output.save(filename, 'png', optimize=True, pnginfo=pnginfo)
|
|
|
|
|
|
class State:
|
|
def __init__(self, dmi, name, *, loop=LOOP_UNLIMITED, rewind=False, movement=False, dirs=1):
|
|
self.dmi = dmi
|
|
self.name = name
|
|
self.loop = loop
|
|
self.rewind = rewind
|
|
self.movement = movement
|
|
self.dirs = dirs
|
|
|
|
self._nframes = None # used during loading only
|
|
self.frames = []
|
|
self.delays = []
|
|
self.hotspots = None
|
|
|
|
@property
|
|
def framecount(self):
|
|
if self._nframes is not None:
|
|
return self._nframes
|
|
else:
|
|
return len(self.frames) // self.dirs
|
|
|
|
def frame(self, image, *, delay=1):
|
|
assert image.size == (self.dmi.width, self.dmi.height)
|
|
self.delays.append(delay)
|
|
self.frames.append(image)
|
|
|
|
def hotspot(self, first_frame, x, y):
|
|
if self.hotspots is None:
|
|
self.hotspots = [None] * self.framecount
|
|
for i in range(first_frame, self.framecount):
|
|
self.hotspots[i] = x, y
|
|
|
|
def _frame_index(self, frame=0, dir=None):
|
|
ofs = DIR_ORDER.index(DIR_NAMES[dir])
|
|
if ofs >= self.dirs:
|
|
ofs = 0
|
|
return frame * self.dirs + ofs
|
|
|
|
def get_frame(self, *args, **kwargs):
|
|
return self.frames[self._frame_index(*args, **kwargs)]
|
|
|
|
|
|
def escape(text):
|
|
text = text.replace('\\', '\\\\')
|
|
text = text.replace('"', '\\"')
|
|
return f'"{text}"'
|
|
|
|
|
|
def unescape(text, quote='"'):
|
|
if text == 'null':
|
|
return None
|
|
if not (text.startswith(quote) and text.endswith(quote)):
|
|
raise ValueError(text)
|
|
text = text[1:-1]
|
|
text = text.replace('\\"', '"')
|
|
text = text.replace('\\\\', '\\')
|
|
return text
|
|
|
|
|
|
def parse_num(value):
|
|
if '.' in value:
|
|
return float(value)
|
|
return int(value)
|
|
|
|
|
|
def parse_bool(value):
|
|
if value not in ('0', '1'):
|
|
raise ValueError(value)
|
|
return value == '1'
|