pax_global_header 0000666 0000000 0000000 00000000064 14433250142 0014510 g ustar 00root root 0000000 0000000 52 comment=dace1615604f13bd1a81bf94e31ad364734c6dba
omgifol-0.5.1/ 0000775 0000000 0000000 00000000000 14433250142 0013147 5 ustar 00root root 0000000 0000000 omgifol-0.5.1/.gitignore 0000664 0000000 0000000 00000000050 14433250142 0015132 0 ustar 00root root 0000000 0000000 *.pyc
__pycache__
build
dist
*.egg-info
omgifol-0.5.1/CHANGES 0000664 0000000 0000000 00000003560 14433250142 0014146 0 ustar 00root root 0000000 0000000 0.5.1 (2023/05/23)
fix NameError regression in MapEditor.to_lumps [strategineer]
0.5.0 (2022/11/19)
remove Python 2 support
add support for UDMF maps (thanks to cyclopsian for the initial work)
add support for Hexen 64x128 flats
add 'listdir.py' demo script [Xymph]
rewrite WAD struct generation to use ctypes
fix more potential cases where map lumps would not be detected correctly
fix Texture.simple not setting the new texture's name correctly [jmtd]
0.4.0 (2018/02/22)
add support for importing/exporting/editing sound effect lumps
add 'Thing' and 'Linedef' members to MapEditor class (aliases to the
thing and linedef classes used by the current map format)
add support for data stored inside map header lumps (e.g. FraggleScript)
add ability for WAD and WadIO classes to use empty space in WAD file, if
possible ('use_free' argument in relevant methods)
enforce maximum size for patch lumps
remove some old chr() calls for Python 3 users
fix previously broken WAD.load method
fix palette index 0 in patches becoming transparent when exporting
fix some lumps falsely being loaded as maps (e.g. aaliens.wad "LOADACS",
which was followed by a "SCRIPTS" lump and thus detected as a map)
0.3.0 (2017/10/06)
add support for Python 3.x (experimental; 3.5.0 or higher recommended)
add support for Hexen / ZDoom maps
add better map loading (supports names other than ExMx and MAPxx,
doesn't mistake MAPINFO for an actual map)
add better support for "limit removing" maps
add tall patch support
add support for importing/exporting RGBA images (converted to the WAD's
256-color palette on import, but can contain true transparency)
add better handling of missing map data
add draw_sector and misc. helper functions to MapEditor [jmickle66666666]
add ability to individually change single flags (by name) with MapEditor
fix a colormap generation bug and add Colormap.set_position [jmickle66666666]
omgifol-0.5.1/LICENSE 0000664 0000000 0000000 00000002067 14433250142 0014161 0 ustar 00root root 0000000 0000000 Copyright (c) 2005 Fredrik Johansson, 2017 Devin Acker
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
omgifol-0.5.1/demo/ 0000775 0000000 0000000 00000000000 14433250142 0014073 5 ustar 00root root 0000000 0000000 omgifol-0.5.1/demo/drawmaps.py 0000664 0000000 0000000 00000004463 14433250142 0016272 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from omg import *
import sys
from PIL import Image, ImageDraw
def drawmap(wad, name, filename, width, format):
xsize = width - 8
try:
edit = UMapEditor(wad.udmfmaps[name])
except KeyError:
edit = UMapEditor(wad.maps[name])
xmin = ymin = float('inf')
xmax = ymax = float('-inf')
for v in edit.vertexes:
xmin = min(xmin, v.x)
xmax = max(xmax, v.x)
ymin = min(ymin, -v.y)
ymax = max(ymax, -v.y)
scale = xsize / float(xmax - xmin)
xmax = xmax * scale
xmin = xmin * scale
ymax = ymax * scale
ymin = ymin * scale
for v in edit.vertexes:
v.x = v.x * scale
v.y = -v.y * scale
im = Image.new('RGB', (int(xmax - xmin) + 8, int(ymax - ymin) + 8), (255,255,255))
draw = ImageDraw.Draw(im)
edit.linedefs.sort(key=lambda a: not a.twosided)
for line in edit.linedefs:
p1x = edit.vertexes[line.v1].x - xmin + 4
p1y = edit.vertexes[line.v1].y - ymin + 4
p2x = edit.vertexes[line.v2].x - xmin + 4
p2y = edit.vertexes[line.v2].y - ymin + 4
color = (0, 0, 0)
if line.twosided:
color = (144, 144, 144)
if line.special:
color = (220, 130, 50)
draw.line((p1x, p1y, p2x, p2y), fill=color)
draw.line((p1x+1, p1y, p2x+1, p2y), fill=color)
draw.line((p1x-1, p1y, p2x-1, p2y), fill=color)
draw.line((p1x, p1y+1, p2x, p2y+1), fill=color)
draw.line((p1x, p1y-1, p2x, p2y-1), fill=color)
del draw
im.save(filename, format)
if (len(sys.argv) < 5):
print("\n Omgifol script: draw maps to image files\n")
print(" Usage:")
print(" drawmaps.py source.wad pattern width format\n")
print(" Draw all maps whose names match the given pattern (eg E?M4 or MAP*)")
print(" to image files of a given format (PNG, BMP, etc). width specifies the")
print(" desired width of the output images.")
else:
print("Loading %s..." % sys.argv[1])
inwad = WAD()
inwad.from_file(sys.argv[1])
width = int(sys.argv[3])
format = sys.argv[4].upper()
for name in inwad.maps.find(sys.argv[2]) + inwad.udmfmaps.find(sys.argv[2]):
print("Drawing %s" % name)
drawmap(inwad, name, name + "_map" + "." + format.lower(), width, format)
omgifol-0.5.1/demo/listdir.py 0000775 0000000 0000000 00000001704 14433250142 0016124 0 ustar 00root root 0000000 0000000 #!/usr/bin/python3
# list WAD directory with lump names/sizes or verbose info from Omgifol itself,
# by Frans P. de Vries (Xymph)
import sys, getopt
from omg import *
if len(sys.argv) < 2:
print("\n Omgifol script: list WAD directory\n")
print(" Usage:")
print(" listdir.py [-v] source.wad\n")
else:
# process optional flag
verbose = False
try:
opts, args = getopt.getopt(sys.argv[1:], 'v')
for o, a in opts:
if o == '-v':
verbose = True
except getopt.GetoptError as err:
print(str(err))
sys.exit(2)
# load WAD
if os.path.exists(args[0]):
wadio = WadIO(args[0])
# comprehensive info or simple list?
if verbose:
print(wadio.info_text())
else:
for i, entry in enumerate(wadio.entries):
print("%-8s %9d" % (entry.name, entry.size))
else:
print("%s: no such file" % args[0])
omgifol-0.5.1/demo/merge.py 0000664 0000000 0000000 00000000763 14433250142 0015552 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import omg, sys
if (len(sys.argv) < 3):
print("\n Omgifol script: merge WADs\n")
print(" Usage:")
print(" merge.py input1.wad input2.wad ... [-o output.wad]\n")
print(" Default output is merged.wad")
else:
w = omg.WAD()
for a in sys.argv[1:]:
if a == "-o":
break
print("Adding %s..." % a)
w += omg.WAD(a)
outpath = "merged.wad"
if "-o" in sys.argv: outpath = sys.argv[-1]
w.to_file(outpath)
omgifol-0.5.1/demo/mirror.py 0000664 0000000 0000000 00000002274 14433250142 0015764 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from sys import argv
from omg import *
from omg.mapedit import *
def mirror(map):
ed = MapEditor(map)
for v in ed.vertexes:
v.x = -v.x
for l in ed.linedefs:
l.vx_a, l.vx_b = l.vx_b, l.vx_a
for t in ed.things:
t.x = -t.x
t.angle = (180 - t.angle) % 360
ed.nodes = []
return ed.to_lumps()
def main(args):
if (len(args) < 2):
print(" Omgifol script: mirror maps\n")
print(" Usage:")
print(" mirror.py input.wad output.wad [pattern]\n")
print(" Mirror all maps or those whose name match the given pattern")
print(" (eg E?M4 or MAP*).")
print(" Note: nodes will have to be rebuilt externally.\n")
else:
print("Loading %s..." % args[0])
inwad = WAD()
outwad = WAD()
inwad.from_file(args[0])
pattern = "*"
if (len(args) == 3):
pattern = args[2]
for name in inwad.maps.find(pattern):
print("Mirroring %s" % name)
outwad.maps[name] = mirror(inwad.maps[name])
print("Saving %s..." % args[1])
outwad.to_file(args[1])
if __name__ == "__main__": main(argv[1:])
omgifol-0.5.1/manual.html 0000664 0000000 0000000 00000025205 14433250142 0015316 0 ustar 00root root 0000000 0000000
Omgifol manual
Note: this is ridiculously incomplete.
Installation
- Install Python 3, which can be downloaded from https://python.org
- Use pip to install Omgifol: pip install omgifol
- Or, if pip is unavailable, extract the "omg" directory in the Omgifol package into pythondir/Lib/site-packages (replace pythondir with the directory where Python is installed).
Optionally:
- Install the Pillow library (https://python-pillow.github.io). This is required to import or export images.
- Install the PySoundFile library (https://pysoundfile.readthedocs.io). This is required to import or export sound files.
Using Omgifol
At the beginning of an interactive session, or as the first line in a Python script file, enter
from omg import *
WAD objects
A WAD is an abstract representation of a WAD file. A WAD object can load content from a WAD file, or save content to a WAD file, but is entirely memory-resident.
Loading from WAD files
The following are all equivalent:
a = WAD('wadfile.wad')
a = WAD(from_file='wadfile.wad')
f = open('wadfile.wad', 'rb')
a = WAD(from_file=f)
a = WAD()
a.from_file('wadfile.wad')
f = open('wadfile.wad', 'rb')
a = WAD()
a.from_file(f)
You can load more than one file to the same object:
a = WAD()
a.from_file(file1)
a.from_file(file2)
a.from_file(file3)
In this case, lumps from file2 will overwrite those from file1
with the same name, etc.
Writing to WAD files
If a is a WAD instance:
a.to_file('some_wad.wad')
Accessing lumps
Lumps are stored in groups. Each WAD holds a number of groups, representing different categories of lumps. Each group is an ordered dictionary; that is, it works just like a Python collections.OrderedDict object.
All lumps are instances of the Lump class; see below for its documentation.
To retrieve the sprite called CYBR1A from the WAD object a, do:
a.sprites['CYBR1A']
And to replace it with some other lump object called some_lump:
a.sprites['CYBR1A'] = some_lump
To add a new lump, simply do as above with a lump name that does not yet exist.
Renaming and deleting is done as follows:
a.sprites.rename('CYBR1A', 'NEW_NAME')
del a.sprites['CYBR1A']
Lump groups
By default, WADs recognize the following lump groups:
sprites Sprite graphics (between S and SS markers)
patches Wall graphics (between P and PP markers)
flats Flat graphics (between F and FF markers)
colormaps Boom colormaps (between C markers)
ztextures ZDoom textures (between TX markers)
maps Map data
udmfmaps Map data (UDMF)
glmaps GL nodes map data
music Music (all lumps named D_*)
sounds Sound effects (all lumps named DS* or DP*)
txdefs TEXTURE1, TEXTURE2 and PNAMES
graphics Titlepic, status bar, miscellaneous graphics
data Everything else
This scheme can be modified if desired; refer to wad.py for the details.
The maps and glmaps are special. These do not contain lumps, but additional groups of lumps, one for each map. So if you access E1M1:
a.maps['E1M1']
you will retrieve a group of lumps containing all the map's data. To retrieve
the individual lumps, do:
a.maps['E1M1']['SIDEDEFS']
etc.
Merging
To merge two WADs a and b:
c = a + b
Note that (for efficiency reasons) this only copies references to lumps,
which means that subsequent changes to lumps in a or b will affect the
corresponding lumps in c. To give c its own set of lumps, do:
c = (a + b).copy()
When lumps in a and b have the same name,
lumps from b will replace those from a.
It is also possible to merge individual sections:
a.sprites += b.sprites
Use with care for sections of different types.
Note that some sections do more than just copy over the list of lumps
when they merge. For example, adding two txdefs sections together
will automagically merge the TEXTURE1, TEXTURE2 and PNAMES lumps. txdefs
also get merged this way when two WAD objects are merged on the top level.
Lumps
The Lump class holds a single lump. The class provides the following data and methods:
.data The lump's raw data as a string
.to_file(filename) Save from a file
.from_file(filename) Load from a file
.copy() Return a copy
Creating a new lump called 'FOOF' containing the text 'Hello!' and inserting it into a WAD w would be done as follows:
w.data['FOOF'] = Lump('Hello!')
Graphic lumps
There are subclasses of Lump for different types of lumps. Currently, only these provide special functionality: Graphic, Flat, and Sound.
Graphic, used to represent Doom format graphics, provides the following settable attributes:
.offsets (x, y) offsets
.x_offset x offset
.y_offset y offset
.dimensions (width, height)
.width width in pixels
.height height in pixels
Graphic defines the following methods in adddition to those defined by Lump:
.from_raw Load from a raw image
.to_raw Return the image converted to raw pixels
.from_Image Load from a PIL Image instance
.to_Image Return the image converted to a PIL image
.translate Translate to another palette
For the argument lists used by these functions, refer to the code and the inline documentation in lump.py.
Flat works similarly to Graphic, but handles format conversions slightly differently.
Sound, used to represent Doom format sounds, provides the following settable attributes:
.format Sound effect format (0-3)
.length Length of sound in samples
.sample_rate Sample rate for digitized sounds (defaults to 11025)
.midi_bank MIDI patch bank number (formats 1-2 only)
.midi_patch MIDI patch number (formats 1-2 only)
Sound defines the following methods in adddition to those defined by Lump:
.from_raw Load from a raw sound file
.to_raw Return the sound file converted to raw samples
.from_file Load from a sound file
.to_file Save the sound to a file
Editors
Editors are used to edit lumps or lump groups. They represent lump data with high-level objects and structures, and provide methods to modify the data. The following editors have been implemented so far:
- Colormap for the COLORMAP lump
- Playpal for the PLAYPAL lump
- Textures for TEXTURE1/TEXTURE2/PNAMES
- MapEditor for maps
- UMapEditor for maps in UDMF format
All editors provide the following methods:
.to_lump
.from_lump
or, if the editor represents more than one lump:
.to_lumps
.from_lumps
In the latter case, the editor is initialized with a lump group instead of a single lump.
Map editor
Example (moving one vertex one unit):
m = MapEditor(wad.maps["E1M1"])
m.vertexes[103].x += 1
wad.maps["E1M1"] = m.to_lumps()
UDMF map editor
UMapEditor works similarly to MapEditor, except the attributes of map data are named based on how they appear in the TEXTMAP lump itself. See the UDMF specification for examples.
UMapEditor can also import non-UDMF maps, and line specials will automatically be translated to their UDMF equivalents when necessary:
# Load and automatically convert a Doom-format map
m = UMapEditor(wad.maps["E1M1"])
# Load a UDMF-format map
m = UMapEditor(wad.udmfmaps["MAP01"])
Refer to the source code for more information.
omgifol-0.5.1/omg/ 0000775 0000000 0000000 00000000000 14433250142 0013731 5 ustar 00root root 0000000 0000000 omgifol-0.5.1/omg/__init__.py 0000664 0000000 0000000 00000002470 14433250142 0016045 0 ustar 00root root 0000000 0000000 """
Omgifol - a Python library for Doom WAD files
Website: http://fredrikj.net/
Copyright (c) 2005 Fredrik Johansson
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
__version__ = '0.5.0'
__author__ = 'Devin Acker, Fredrik Johansson'
from omg.wadio import *
from omg.wad import *
from omg.lump import *
from omg.mapedit import *
from omg.udmf import *
omgifol-0.5.1/omg/colormap.py 0000664 0000000 0000000 00000004476 14433250142 0016132 0 ustar 00root root 0000000 0000000 import omg.palette
import omg.lump
class Colormap:
"""An editor for Doom's COLORMAP lump. The colormap holds 34 tables
of indices to the game's palette. The first 32 tables hold data
for different brightness levels, the 33rd holds the indices used
by the invulnerability powerup, and the 34th is unused."""
def __init__(self, from_lump=None):
"""Create new, optionally from an existing lump."""
self.tables = [[0 for x in range(256)] for y in range(34)]
if from_lump:
self.from_lump(from_lump)
def build_fade(self, palette=None, fade=(0,0,0)):
"""Build fade tables. The default fade color is black;
this may be overriden. Light color is not yet supported."""
palette = palette or omg.palette.default
x, y, z = fade
for n in range(32):
e = 31-n
for c in range(256):
r, g, b = palette.colors[c]
r = (r*n + x*e) // 32
g = (g*n + y*e) // 32
b = (b*n + z*e) // 32
self.tables[e][c] = palette.match((r,g,b))
def build_invuln(self, palette=None, start=(0,0,0), end=(255,255,255)):
"""Build range used by the invulnerability powerup."""
palette = palette or omg.palette.default
ar, ag, ab = start
br, bg, bb = end
for i in range(256):
bright = sum(palette.colors[i]) // 3
r = (ar*bright + br*(256-bright)) // 256
g = (ag*bright + bg*(256-bright)) // 256
b = (ab*bright + bb*(256-bright)) // 256
self.tables[32][i] = palette.match((r,g,b))
def from_lump(self, lump):
"""Load from a COLORMAP lump."""
assert len(lump.data) == 34*256
for n in range(34):
self.tables[n] = [lump.data[i] for i in range(n*256,(n+1)*256)]
def to_lump(self):
"""Pack to a COLORMAP lump."""
output = bytes()
for t in self.tables:
output += bytes(t)
return omg.lump.Lump(output)
# packed = [''.join([chr(c) for c in t]) for t in self.tables]
# return omg.lump.Lump(''.join(packed))
def set_position(self,table,index,pal_index):
"""Sets a specified position in the colormap to the specified
index in the playpal."""
self.tables[table][index] = pal_index
omgifol-0.5.1/omg/lineinfo.py 0000664 0000000 0000000 00000047271 14433250142 0016121 0 ustar 00root root 0000000 0000000 """
lineinfo.py -- dealing with Doom linedef trigger types.
Provides functions to create a human-readable description
code from a trigger number, and the inverse operation.
Guide to Trigger Description Codes (R):
Example: "FLOOR SR UP SLOW CRUSH LNC-8"
Categories:
DOOR - Doors (regular and locked)
FLOOR - Floor movers
CEIL - Ceiling movers
PLAT - Platforms and lifts
CRUSHER - Crushers
STAIR - Stair builders
ELEVATOR - Boom elevators
LIGHT - Light changers
EXIT - Exits
TELEPORT - Teleports
DONUT - Donuts (lower pillar, raise surrounding sector)
TRANSFER - Transfer properties (Boom)
SCROLL - Scroll lines and sectors (Boom)
Trigger types:
P1 - Push(door) trigger, works once
PR - Push(door) trigger, works repeatedly
S1 - Switch, works once
SR - Switch, works repeatedly
W1 - Walk across, works once
WR - Walk across, works repeatedly
G1 - Shoot, works once
GR - Shoot, works repeatedly
Door locks:
YEL - Yellow key lock
RED - Red key lock
BLU - Blue key lock
Door types:
OWC - Open, Wait, Close
CWO - Close, Wait, Open
OSO - Open, Stay Open
CSC - Close, Stay Closed
Motion speed
SLOW - Slow
NORM - Normal
FAST - Fast
TURB - Turbo
INST - Instant
Delay times
3SEC - 3 seconds
4SEC - 4 seconds
30SEC - 30 seconds
Sector property changers:
CPYTEX - Copy Texture
CPYTEX+DELTYPE - Copy Texture, Reset type
CPYTEX+TYPE - Copy Texture and Type
Directions:
DOWN - Down
UP - Up
NOMOVE - Stay (only change properties)
Miscellaneous:
SECRET - A secret exit
MONSTER - Monsters can activate the trigger
LINE - Line teleporters
REVERSE - Line teleporter, reversed
SILENT - Make crushers or teleporters silent
CRUSH - Enable crushing (for CEILs and FLOORs,
not to be confused with CRUSHERs)
Destinations/platforms:
LNF - Lowest Neighbor Floor
LNC - Lowest Neighbor Ceiling
HNF - Highest Neighbor Floor
HNC - Highest Neighbor Ceiling
NNF - Next Neighbor Floor
NNC - Next Neighbor Ceiling
HNF+8 - 8 above Highest neighbor Floor
LNC+8 - 8 under Lowest neighbor Ceiling
F+8 - 8 above floor
8 - 8 units Absolute (STAIRs only)
16 - 16 units Absolute (STAIRs only)
24 - 24 units Absolute
32 - 32 units Absolute
512 - 512 units absolute
SLT - Shortest Lower Texture
SUT - Shortest Upper Texture
NLF - Next Lowest neighbor Floor
NLC - Next Lowest neighbor Ceiling
NHF - Next Highest neighbor Floor
CURRF - Current Floor (ELEVATORs)
FLR - Floor
CL - Ceiling
NAF - Next adjacent floor
PERP - Perpetual
STOP - Stop ongoing motion
Models:
TRIG - Use trigger sector as model
NUM - Lookup adjacent model numerically
Lighting:
35 - 35 units
255 - 255 units
MAXN - Maximum Neighbor
MINN - Minimum Neighbor
BLINK - Blinking
Transfers (check boomref.txt for more info):
FLIGHT - Transfer floor light level
CLIGHT - Transfer ceiling light level
TRANSLUCENCY - Transfer line translucency
HEIGHTS - The famous 242!
FRICTION - Transfer friction
WIND - Transfer current
POINTFORCE - Transfer force point (?)
Scrollers (check boomref.txt for more info):
CARRY - Carry objects (conveyor)
WRTSECTOR - With respect to 1st side's sector
ACCEL - Accelerate scrolling
RIGHT - Right direction
LEFT - Left direction
WALL - Scroll wall
SYNCED - Sync scrolling to sector
OFFSETS - Scroll by offsets
"""
from fnmatch import fnmatchcase
# Define description codes for the standard triggers
desc2num = \
{
"": 0,
"NO ACTION":0,
# Doors
"DOOR PR SLOW OWC 4SEC MONSTER":1,
"DOOR PR FAST OWC 4SEC":117,
"DOOR SR SLOW OWC 4SEC":63,
"DOOR SR FAST OWC 4SEC":114,
"DOOR S1 SLOW OWC 4SEC":29,
"DOOR S1 FAST OWC 4SEC":111,
"DOOR WR SLOW OWC 4SEC":90,
"DOOR WR FAST OWC 4SEC":105,
"DOOR W1 SLOW OWC 4SEC":4,
"DOOR W1 FAST OWC 4SEC":108,
"DOOR P1 SLOW OSO":31,
"DOOR P1 FAST OSO":118,
"DOOR SR SLOW OSO":61,
"DOOR SR FAST OSO":114,
"DOOR S1 SLOW OSO":103,
"DOOR S1 FAST OSO":112,
"DOOR WR SLOW OSO":86,
"DOOR WR FAST OSO":106,
"DOOR W1 SLOW OSO":2,
"DOOR W1 FAST OSO":109,
"DOOR GR FAST OSO":46,
"DOOR SR SLOW CSC":42,
"DOOR SR FAST CSC":116,
"DOOR S1 SLOW CSC":50,
"DOOR S1 FAST CSC":113,
"DOOR WR SLOW CSC":75,
"DOOR WR FAST CSC":107,
"DOOR W1 SLOW CSC":3,
"DOOR W1 FAST CSC":110,
"DOOR SR SLOW CWO 30SEC":196,
"DOOR S1 SLOW CWO 30SEC":175,
"DOOR WR SLOW CWO 30SEC":76,
"DOOR W1 SLOW CWO 30SEC":16,
"DOOR PR SLOW OWC 4SEC BLU":26,
"DOOR PR SLOW OWC 4SEC RED":28,
"DOOR PR SLOW OWC 4SEC YEL":27,
"DOOR P1 SLOW OSO BLU":32,
"DOOR P1 SLOW OSO RED":33,
"DOOR P1 SLOW OSO YEL":34,
"DOOR SR FAST OSO BLU":99,
"DOOR SR FAST OSO RED":134,
"DOOR SR FAST OSO YEL":136,
"DOOR S1 FAST OSO BLU":133,
"DOOR S1 FAST OSO RED":135,
"DOOR S1 FAST OSO YEL":137,
# Floors
"FLOOR SR DOWN SLOW LNF":60,
"FLOOR S1 DOWN SLOW LNF":23,
"FLOOR WR DOWN SLOW LNF":82,
"FLOOR W1 DOWN SLOW LNF":38,
"FLOOR SR DOWN SLOW LNF CPYTEX+TYPE NUM":177,
"FLOOR S1 DOWN SLOW LNF CPYTEX+TYPE NUM":159,
"FLOOR WR DOWN SLOW LNF CPYTEX+TYPE NUM":84,
"FLOOR W1 DOWN SLOW LNF CPYTEX+TYPE NUM":37,
"FLOOR SR UP SLOW NNF":69,
"FLOOR S1 UP SLOW NNF":18,
"FLOOR WR UP SLOW NNF":128,
"FLOOR W1 UP SLOW NNF":119,
"FLOOR SR UP FAST NNF":132,
"FLOOR S1 UP FAST NNF":131,
"FLOOR WR UP FAST NNF":129,
"FLOOR W1 UP FAST NNF":130,
"FLOOR SR DOWN SLOW NNF":222,
"FLOOR S1 DOWN SLOW NNF":221,
"FLOOR WR DOWN SLOW NNF":220,
"FLOOR W1 DOWN SLOW NNF":219,
"FLOOR SR UP SLOW LNC":64,
"FLOOR S1 UP SLOW LNC":101,
"FLOOR WR UP SLOW LNC":91,
"FLOOR W1 UP SLOW LNC":5,
"FLOOR G1 UP SLOW LNC":24,
"FLOOR SR UP SLOW LNC-8 CRUSH":65,
"FLOOR S1 UP SLOW LNC-8 CRUSH":55,
"FLOOR WR UP SLOW LNC-8 CRUSH":94,
"FLOOR W1 UP SLOW LNC-8 CRUSH":56,
"FLOOR SR DOWN SLOW HNF":45,
"FLOOR S1 DOWN SLOW HNF":102,
"FLOOR WR DOWN SLOW HNF":83,
"FLOOR W1 DOWN SLOW HNF":19,
"FLOOR SR DOWN FAST HNF+8":70,
"FLOOR S1 DOWN FAST HNF+8":71,
"FLOOR WR DOWN FAST HNF+8":98,
"FLOOR W1 DOWN FAST HNF+8":36,
"FLOOR SR UP SLOW 24":180,
"FLOOR S1 UP SLOW 24":161,
"FLOOR WR UP SLOW 24":92,
"FLOOR W1 UP SLOW 24":58,
"FLOOR SR UP SLOW 24 CPYTEX+TYPE TRIG":179,
"FLOOR S1 UP SLOW 24 CPYTEX+TYPE TRIG":160,
"FLOOR WR UP SLOW 24 CPYTEX+TYPE TRIG":93,
"FLOOR W1 UP SLOW 24 CPYTEX+TYPE TRIG":59,
"FLOOR SR UP SLOW SLT":176,
"FLOOR S1 UP SLOW SLT":158,
"FLOOR WR UP SLOW SLT":96,
"FLOOR W1 UP SLOW SLT":30,
"FLOOR SR UP SLOW 512":178,
"FLOOR S1 UP SLOW 512":140,
"FLOOR WR UP SLOW 512":147,
"FLOOR W1 UP SLOW 512":142,
"FLOOR SR NOMOVE CPYTEX+TYPE SLT TRIG":190,
"FLOOR S1 NOMOVE CPYTEX+TYPE SLT TRIG":189,
"FLOOR WR NOMOVE CPYTEX+TYPE SLT TRIG":154,
"FLOOR W1 NOMOVE CPYTEX+TYPE SLT TRIG":153,
"FLOOR SR NOMOVE CPYTEX+TYPE SLT NUM":78,
"FLOOR S1 NOMOVE CPYTEX+TYPE SLT NUM":241,
"FLOOR WR NOMOVE CPYTEX+TYPE SLT NUM":240,
"FLOOR W1 NOMOVE CPYTEX+TYPE SLT NUM":239,
# Ceilings
"CEIL SR DOWN FAST FLR":43,
"CEIL S1 DOWN FAST FLR":41,
"CEIL WR DOWN FAST FLR":152,
"CEIL W1 DOWN FAST FLR":145,
"CEIL SR UP SLOW HNC":186,
"CEIL S1 UP SLOW HNC":166,
"CEIL WR UP SLOW HNC":151,
"CEIL W1 UP SLOW HNC":40,
"CEIL SR DOWN SLOW F+8":187,
"CEIL S1 DOWN SLOW F+8":167,
"CEIL WR DOWN SLOW F+8":72,
"CEIL W1 DOWN SLOW F+8":44,
"CEIL SR DOWN SLOW LNC":205,
"CEIL S1 DOWN SLOW LNC":203,
"CEIL WR DOWN SLOW LNC":201,
"CEIL W1 DOWN SLOW LNC":199,
"CEIL SR DOWN SLOW HNF":205,
"CEIL S1 DOWN SLOW HNF":204,
"CEIL WR DOWN SLOW HNF":202,
"CEIL W1 DOWN SLOW HNF":200,
# Platforms and lifts
"PLAT SR SLOW CPYTEX TRIG 24":66,
"PLAT S1 SLOW CPYTEX TRIG 24":15,
"PLAT WR SLOW CPYTEX TRIG 24":148,
"PLAT W1 SLOW CPYTEX TRIG 24":143,
"PLAT SR SLOW CPYTEX TRIG 32":67,
"PLAT S1 SLOW CPYTEX TRIG 32":14,
"PLAT WR SLOW CPYTEX TRIG 32":149,
"PLAT W1 SLOW CPYTEX TRIG 32":144,
"PLAT SR SLOW CPYTEX+DELTYPE TRIG NAF":68,
"PLAT S1 SLOW CPYTEX+DELTYPE TRIG NAF":20,
"PLAT WR SLOW CPYTEX+DELTYPE TRIG NAF":95,
"PLAT W1 SLOW CPYTEX+DELTYPE TRIG NAF":22,
"PLAT G1 SLOW CPYTEX+DELTYPE TRIG NAF":47,
"PLAT SR SLOW 3SEC PERP":181,
"PLAT S1 SLOW 3SEC PERP":162,
"PLAT WR SLOW 3SEC PERP":87,
"PLAT W1 SLOW 3SEC PERP":53,
"PLAT SR STOP":182,
"PLAT S1 STOP":163,
"PLAT WR STOP":89,
"PLAT W1 STOP":54,
"PLAT SR SLOW 3SEC LNF":62,
"PLAT S1 SLOW 3SEC LNF":21,
"PLAT WR SLOW 3SEC LNF":88,
"PLAT W1 SLOW 3SEC LNF":10,
"PLAT SR FAST 3SEC LNF":123,
"PLAT S1 FAST 3SEC LNF":122,
"PLAT WR FAST 3SEC LNF":120,
"PLAT W1 FAST 3SEC LNF":121,
"PLAT SR INST CL":211,
"PLAT WR INST CL":212,
# Crushers
"CRUSHER SR SLOW":184,
"CRUSHER S1 SLOW":49,
"CRUSHER WR SLOW":73,
"CRUSHER W1 SLOW":25,
"CRUSHER SR FAST":183,
"CRUSHER S1 FAST":164,
"CRUSHER WR FAST":77,
"CRUSHER W1 FAST":6,
"CRUSHER SR SLOW SILENT":185,
"CRUSHER S1 SLOW SILENT":165,
"CRUSHER WR SLOW SILENT":150,
"CRUSHER W1 SLOW SILENT":141,
"CRUSHER SR STOP":188,
"CRUSHER S1 STOP":168,
"CRUSHER WR STOP":74,
"CRUSHER W1 STOP":57,
# Stairs
"STAIR SR UP SLOW 8":258,
"STAIR S1 UP SLOW 8":7,
"STAIR WR UP SLOW 8":256,
"STAIR W1 UP SLOW 8":8,
"STAIR SR UP FAST 16":259,
"STAIR S1 UP FAST 16":127,
"STAIR WR UP FAST 16":257,
"STAIR W1 UP FAST 16":100,
# Boom elevators
"ELEVATOR SR FAST NHF":230,
"ELEVATOR S1 FAST NHF":229,
"ELEVATOR WR FAST NHF":228,
"ELEVATOR W1 FAST NHF":227,
"ELEVATOR SR FAST NHF":234,
"ELEVATOR S1 FAST NLF":233,
"ELEVATOR WR FAST NLF":232,
"ELEVATOR W1 FAST NLF":231,
"ELEVATOR SR FAST CURRF":238,
"ELEVATOR S1 FAST CURRF":237,
"ELEVATOR WR FAST CURRF":236,
"ELEVATOR W1 FAST CURRF":235,
# Lighting
"LIGHT SR 35":139,
"LIGHT S1 35":170,
"LIGHT WR 35":79,
"LIGHT W1 35":35,
"LIGHT SR 255":138,
"LIGHT S1 255":171,
"LIGHT WR 255":81,
"LIGHT W1 255":13,
"LIGHT SR MAXN":192,
"LIGHT S1 MAXN":169,
"LIGHT WR MAXN":80,
"LIGHT W1 MAXN":12,
"LIGHT SR MINN":194,
"LIGHT S1 MINN":173,
"LIGHT WR MINN":157,
"LIGHT W1 MINN":104,
"LIGHT SR BLINK":193,
"LIGHT S1 BLINK":172,
"LIGHT WR BLINK":156,
"LIGHT W1 BLINK":17,
# Exits
"EXIT S1":11,
"EXIT W1":52,
"EXIT G1":197,
"EXIT S1 SECRET":51,
"EXIT W1 SECRET":124,
"EXIT G1 SECRET":198,
# Teleports
"TELEPORT SR":195,
"TELEPORT S1":174,
"TELEPORT WR":97,
"TELEPORT W1":39,
"TELEPORT WR MONSTER":126,
"TELEPORT W1 MONSTER":125,
"TELEPORT SR MONSTER":269,
"TELEPORT S1 MONSTER":268,
"TELEPORT SR SILENT":210,
"TELEPORT S1 SILENT":209,
"TELEPORT WR SILENT":208,
"TELEPORT W1 SILENT":207,
"TELEPORT WR SILENT LINE":244,
"TELEPORT W1 SILENT LINE":243,
"TELEPORT WR SILENT LINE REVERSE":263,
"TELEPORT W1 SILENT LINE REVERSE":262,
"TELEPORT WR SILENT LINE MONSTER":267,
"TELEPORT W1 SILENT LINE MONSTER":266,
"TELEPORT WR SILENT LINE REVERSE MONSTER":265,
"TELEPORT W1 SILENT LINE REVERSE MONSTER":264,
# Donuts
"DONUT SR":191,
"DONUT S1":9,
"DONUT WR":155,
"DONUT W1":146,
# Boom property transfer
"TRANSFER FLIGHT":213,
"TRANSFER CLIGHT":261,
"TRANSFER TRANSLUCENCY":260,
"TRANSFER HEIGHTS":242,
"TRANSFER FRICTION":223,
"TRANSFER WIND":224,
"TRANSFER CURRENT":225,
"TRANSFER POINTFORCE":226,
# Scrollers
"SCROLL CL":250,
"SCROLL FLR":251,
"SCROLL CARRY":252,
"SCROLL FLR+CARRY":253,
"SCROLL WALL SYNCED":254,
"SCROLL WALL OFFSETS":255,
"SCROLL WALL RIGHT":85,
"SCROLL WALL LEFT":48,
"SCROLL CL WRTSECTOR":245,
"SCROLL FLR WRTSECTOR":246,
"SCROLL CARRY WRTSECTOR":247,
"SCROLL F+CARRY WRTSECTOR":248,
"SCROLL WALL WRTSECTOR":249,
"SCROLL CL ACCEL":214,
"SCROLL FLR ACCEL":215,
"SCROLL CARRY ACCEL":216,
"SCROLL FLR+CARRY ACCEL":217,
"SCROLL WALL ACCEL":218
}
num2desc = {}
for d, n in desc2num.items(): num2desc[n] = d
del(d)
del(n)
trigcompat = \
[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, 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, 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,
0, 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, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
def check_compat(num):
"""Check the compatibility for a trigger number."""
if 8192 <= num < 32768:
return "BOOM GENERALIZED"
try:
return ["UNKNOWN", "DOOM19", "BOOM EXTENDED"][trigcompat[num]]
except:
return "UNKNOWN"
def decode(n):
"""Generate a description code for a number."""
d = []
if n < 8192:
if n in num2desc:
return num2desc[n]
return "UNKNOWN"
# Boom generalized
elif 0x2F80 <= n < 0x3000:
n -= 0x2F80
d += ["CRUSHER"]
d += [("W1","WR","S1","SR","G1","GR","P1","PR") [n&0x0007]]
d += [("SLOW","NORMAL","FAST","TURBO") [(n&0x0018)>>3]]
d += [("MONSTER","") [(n&0x0020)>>5]]
d += [("SILENT","") [(n&0x00c0)>>6]]
elif 0x3000 <= n < 0x3400:
n -= 0x3000
d += ["STAIR"]
d += [("W1","WR","S1","SR","G1","GR","P1","PR") [n&0x0007]]
d += [("SLOW","NORMAL","FAST","TURBO") [(n&0x0018)>>3]]
d += [("","MONSTER") [(n&0x0020)>>5]]
d += [("4","8","16","24") [(n&0x00c0)>>6]]
d += [("DOWN","UP") [(n&0x0100)>>8]]
d += [("", "IGNTXT") [(n&0x0200)>>9]]
elif 0x3400 <= n < 0x3800:
n -= 0x3400
d += ["PLATFORM"]
d += [("W1","WR","S1","SR","G1","GR","P1","PR") [n&0x0007]]
d += [("SLOW","NORMAL","FAST","TURBO") [(n&0x0018)>>3]]
d += [("MONSTER","") [(n&0x0020)>>5]]
d += [("1","3","5","10") [(n&0x00c0)>>6]]
d += [("LNF","NNF","LNC","PERP") [(n&0x0300)>>8]]
elif 0x3800 <= n < 0x3c00:
n -= 0x3800
d += ["DOOR"]
d += [("W1","WR","S1","SR","G1","GR","P1","PR") [n&0x0007]]
d += [("SLOW","NORMAL","FAST","TURBO") [(n&0x0018)>>3]]
d += [("OWC","OSO") [(n&0x0020)>>5]]
d += [("ANY","RED","YELLOW","BLUE","RED",
"BLUE","YELLOW","ALL") [(n&0x01c0)>>6]]
d += [("3KEYS","6KEYS") [(n&0x0200)>>9]]
elif 0x3c00 <= n < 0x4000:
n -= 0x3c00
d += ["DOOR"]
d += [("W1","WR","S1","SR","G1","GR","P1","PR") [n&0x0007]]
d += [("SLOW","NORMAL","FAST","TURBO") [(n&0x0018)>>3]]
d += [("OWC","OSO","CWO","CSC") [(n&0x0060)>>5]]
d += [("MONSTER","") [(n&0x0080)>>7]]
d += [("1SECS","4SECS","9SECS","30SECS") [(n&0x0300)>>8]]
elif 0x4000 <= n < 0x6000:
n -= 0x4000
d += ["CEIL"]
d += [("W1","WR","S1","SR","G1","GR","P1","PR") [n&0x0007]]
d += [("SLOW","NORMAL","FAST","TURBO") [(n&0x0018)>>3]]
d += [("TRIG","NUM") [(n&0x0020)>>5]]
d += [("DOWN","UP") [(n&0x0040)>>6]]
d += [("HNC","LNC","NNC","HNF","FLR",
"SUT","24","32") [(n&0x0380)>>7]]
d += [("","CPYTEX+DELTYPE","CPYTEX","CHGTYPE") [(n&0x0c00)>>10]]
d += [("CRUSH","") [(n&0x1000)>>12]]
elif 0x6000 <= n < 0x8000:
n -= 0x6000
d += ["FLOOR"]
d += [("W1","WR","S1","SR","G1","GR","P1","PR") [n&0x0007]]
d += [("SLOW","NORMAL","FAST","TURBO") [(n&0x0018)>>3]]
d += [("TRIG","NUM") [(n&0x0020)>>5]]
d += [("DOWN","UP") [(n&0x0040)>>6]]
d += [("HNF","LNF","NNF","LNC","CL",
"SLT","24","32") [(n&0x0380)>>7]]
d += [("","CPYTEX+DELTYPE","CPYTEX","CHGTYPE") [(n&0x0c00)>>10]]
d += [("CRUSH","") [(n&0x1000)>>12]]
# Bit of a hack, but works
return (" ".join(d)).replace(" "," ").rstrip(" ")
def encode_std(desc):
"""Encode an exact description of a trigger into its corresponding number.
For inexact descriptions, use find_std."""
try:
return desc2num[desc.upper()]
except:
raise Exception("Description not recognized")
def encode_gen(desc):
"""Encode a generalized (Boom) trigger description to a trigger
number. Invalid or incompatible terms get converted to the default
value."""
desc = desc.upper()
num = 0
def pk(seq, shift):
for i in range(len(seq)):
if seq[i] in desc:
return i << shift
return 0
num |= pk(("W1","WR","S1","SR","G1","GR","P1","PR"), 0)
num |= pk(("SLOW","NORMAL","FAST","TURBO"), 3)
if ("FLOOR" in desc) or ("CEIL" in desc):
num |= pk(("TRIG","NUM"), 5)
num |= pk(("DOWN","UP"), 6)
num |= pk(("xx","CPYTEX+DELTYPE","CPYTEX","CHGTYPE"), 10)
num |= pk(("CRUSH",), 12)
if "FLOOR" in desc:
num |= pk(("HNF","LNF","NNF","LNC","CL","SLT","24","32"), 7)
num += 0x6000
else:
num |= pk(("HNC","LNC","NNC","HNF","FLR","SUT","24","32"), 7)
num += 0x4000
elif "CRUSHER" in desc:
num |= pk(("MONSTER",), 5)
num |= pk(("SILENT",), 6)
num += 0x2F80
elif "STAIR" in desc:
num |= pk(("xx","MONSTER"), 5)
num |= pk(("4","8","16","24"), 6)
num |= pk(("DOWN","UP"), 8)
num |= pk(("xx","IGNTXT"), 9)
num += 0x3000
elif "PLATFORM" in desc:
num |= pk(("MONSTER",), 5)
num |= pk(("1","3","5","10"), 6)
num |= pk(("LNF","NNF","LNC","PERP"), 8)
num += 0x3400
elif "DOOR" in desc:
num |= pk(("SLOW","NORMAL","FAST","TURBO"), 3)
if ("BLU" in desc) or ("YEL" in desc) or ("RED" in desc) or\
("ALL" in desc) or ("ANY" in desc):
num |= pk(("OWC","OSO"), 5)
num |= pk(("ANY","RED","YELLOW","BLUE","RED","BLUE","YELLOW","ALL"), 6)
num |= pk(("3KEYS","6KEYS"), 9)
num += 0x3800
else:
num |= pk(("OWC","OSO","CWO","CSC"), 5)
num |= pk(("MONSTER",), 7)
num |= pk(("1SECS","4SECS","9SECS","30SECS"), 8)
num += 0x3c00
else:
raise LookupError("Insufficient information provided")
return num
def find_std(desc):
"""Search the standard (non-generalized) triggers. A list of
matches is returned. All terms must match. Wildcards are allowed.
Example:
find_std("CEIL UP S?") should return:
['CEIL S1 UP SLOW HNC', 'CEIL SR UP SLOW HNC']"""
desc = desc.upper()
terms = desc.split()
matches = []
for dsc in num2desc.values():
d = dsc.split()
matchedterms = 0
for term in terms:
for key in d:
if fnmatchcase(key, term):
matchedterms += 1
if matchedterms == len(terms):
matches.append(dsc)
return matches
__all__ = [find_std, encode_std, encode_gen, decode, check_compat]
omgifol-0.5.1/omg/lump.py 0000664 0000000 0000000 00000057673 14433250142 0015302 0 ustar 00root root 0000000 0000000 # Import the Python Imaging Library if it is available. On error, ignore
# the problem and continue. PIL being absent should only affect the
# graphic lump loading/saving methods and the user may not be interested
# in installing PIL just to pass this line if not interested in using the
# graphics functionality at all.
try:
from PIL import Image, ImageDraw, ImageOps
except:
pass
# Import PySoundFile for sound file loading/saving. Equally optional.
try:
from soundfile import SoundFile, check_format
import numpy as np
except:
pass
import os
import omg.palette
from omg.util import *
class Lump(object):
"""Basic lump class. Instances of Lump (and its subclasses)
always have the following:
.data -- a bytes object holding the lump's data
.from_file -- load the data to a file
.to_file -- save the data to a file
The default Lump class merely copies the raw data when
loading/saving to files, but subclasses may convert data
appropriately (for example, Graphic supports various image
formats)."""
def __init__(self, data=None, from_file=None):
"""Create a new instance. The `data` parameter may be a string
representing data for the lump. The `source` parameter may be
a path to a file or a file-like object to load from."""
self.data = bytes()
if issubclass(type(data), Lump):
self.data = data.data
elif data is not None:
self.data = data or bytes()
if from_file:
self.from_file(from_file)
def from_file(self, source):
"""Load data from a file. Source may be a path name string
or a file-like object (with a `write` method)."""
self.data = readfile(source)
def to_file(self, target):
"""Write data to a file. Target may be a path name string
or a file-like object (with a `write` method)."""
writefile(target, self.data)
def copy(self):
return deepcopy(self)
class Music(Lump):
"""Subclass of Lump, for music lumps. Not yet implemented."""
pass
class Sound(Lump):
"""Subclass of Lump, for Doom format sounds. Supports
conversion from/to RAWs (sequences of bytes), as well as
saving to/loading from various file formats (via PySoundFile).
Useful attributes:
.format -- DMX sound format
.length -- (in frames/samples)
.sample_rate --
.midi_bank -- MIDI patch bank (format 1/2 only)
.midi_patch -- MIDI patch number (format 1/2 only)
Possible values for the 'format' attribute:
0: PC speaker sound
Raw data consists of values 0-127 corresponding to pitch.
Sample rate is fixed at 140Hz.
1: MIDI sound sequence
Raw data consists of MIDI note and pitch bend info.
Sample rate is fixed at 140Hz.
2: MIDI note
Raw data consists of a single MIDI note.
Sample rate is undefined. Length is MIDI note length.
3: Digitized sound (default)
Raw data is 8-bit unsigned PCM.
Sample rate defaults to 11025 Hz, but can be changed.
Only format 3 can be exported to an audio file.
"""
def __init__(self, data=None, from_file=None):
Lump.__init__(self, data, from_file)
# default to an empty digitized sound effect if no data loaded
try:
if self.format is None:
self.format = 3
except TypeError:
pass
def get_format(self):
"""Retrieve the format of the sound."""
if len(self.data) < 2:
format = None
else:
format = unpack(' 3:
raise TypeError("Unknown or invalid sound format")
return format
def set_format(self, format):
"""Change the format of the sound.
Warning: Changing a sound's format will erase any existing sound data!"""
try:
if format == self.format:
return # don't do anything if format is the same as before
except TypeError:
pass
if format == 0:
# PC speaker sound
self.data = pack(' 65535:
raise ValueError("sound effect length must be between 0-65535")
if format == 2:
# single MIDI note
self.data = self.data[:8] + pack(' 65535:
raise ValueError("sound effect length must be between 0-65535")
# optionally change format if needed
if format is None:
format = self.format
else:
self.format = format
if format == 0:
# PC speaker sound
self.data = self.data[:2] + pack('> 8) + 128
if file.channels > 1:
sound = np.mean(sound, axis=1)
# create new format 3 sound
self.from_raw(sound.astype('uint8').tobytes(), 3, file.samplerate)
def to_file(self, filename, subtype='PCM_U8'):
"""Save the sound to an audio file.
The output format is selected based on the filename extension.
For example, "file.wav" saves to WAV format. If the file has
no extension, WAV format is used.
See the PySoundFile documentation for possible values of 'subtype'.
Possible values depend on the output format; if the given value is
not supported, the format's default will be used.
Special cases: ".lmp" saves the raw lump data, and ".raw" saves
the raw sound data.
"""
format = os.path.splitext(filename)[1][1:].upper() or 'WAV'
if format == 'LMP': writefile(filename, self.data)
elif format == 'RAW': writefile(filename, self.to_raw())
elif self.format == 3:
if check_format(format, subtype): pass
elif check_format(format, 'PCM_U8'): subtype = 'PCM_U8'
elif check_format(format, 'PCM_S8'): subtype = 'PCM_S8'
else: subtype = None # use default for format
with SoundFile(filename, 'w', self.sample_rate, 1, subtype, format=format) as file:
# convert to signed 16-bit (since SoundFile doesn't directly support 8-bit input)
# the result will just be converted back in the file though
sound = (np.frombuffer(self.to_raw(), dtype='uint8').astype('int16') - 128) << 8
file.write(sound)
else:
raise TypeError("audio file export only supported for digitized sounds (format 3)")
class Graphic(Lump):
"""Subclass of Lump, for Doom format graphics. Supports
conversion from/to RAWs (sequences of bytes) and PIL
Image objects, as well as saving to/loading from various
file formats (via PIL).
Useful attributes:
.dimensions -- (width, height)
.width -- width of the image
.height -- height of the image
.x_offset -- x offset
.y_offset -- y offset
"""
def __init__(self, data=None, from_file=None, palette=None):
self.palette = palette or omg.palette.default
Lump.__init__(self, data, from_file)
def get_offsets(self):
"""Retrieve the (x, y) offsets of the graphic."""
return unpack(' 32767:
raise ValueError("image width and height must be between 0-32767")
# First pass: extract pixel data in column+post format
columns_in = [data[n:width*height:width] for n in range(width)]
columns_out = []
for column in columns_in:
# Find the y position where each chunk starts
start_rows = []
postdata = []
in_trans = True
tall = False
offset = 0
for y in range(height):
# split at 128 for vanilla-compatible images without premature tiling
if height < 256:
if y == 128:
in_trans = True
# for tall patch support
elif offset == 254:
in_trans = True
tall = True
# dummy post
start_rows.append(254)
postdata.append(bytearray())
# start relative offsets
offset = 0
if column[y] is None:
in_trans = True
else:
if in_trans:
# start a new post
start_rows.append(offset)
postdata.append(bytearray())
in_trans = False
if tall:
# reset relative offset for tall patches
offset = 0
postdata[-1].append(column[y])
offset += 1
columns_out.append(zip(start_rows, postdata))
# Second pass: compile column+post data, adding pointers
data = []
columnptrs = []
pointer = 4*width + 8
for column in columns_out:
columnptrs.append(pack('= len(data):
continue
while data[pointer] != 0xff:
offset = data[pointer]
if offset <= y:
y += offset # for tall patches
else:
y = offset
post_length = data[pointer + 1]
op = y*width + x
for p in range(pointer + 3, pointer + post_length + 3):
if op >= len(output) or p >= len(data):
break
output[op] = data[p]
op += width
pointer += post_length + 4
return output
def to_raw(self, tran_index=None):
"""Returns self converted to a raw (8-bpp) image.
`tran_index` specifies the palette index to use for
transparent pixels. The value defaults to that of the
Graphic object's palette instance.
"""
tran_index = tran_index or self.palette.tran_index
output = [i if i is not None else tran_index for i in self.to_pixels()]
return bytes(bytearray(output))
def to_Image(self, mode='P'):
"""Convert to a PIL Image instance."""
if mode != 'RGBA' or isinstance(self, Flat):
# target image has no alpha,
# or source image is a flat (which has no transparent pixels)
im = Image.new('P', self.dimensions, None)
if isinstance(self, Flat):
im.frombytes(self.data)
else:
im.frombytes(self.to_raw())
im.putpalette(self.palette.save_bytes)
return im.convert(mode)
else:
# target image is RGBA and source image is not a flat
im = Image.new('RGBA', self.dimensions, None)
data = bytes().join([self.palette.bytes[i*3:i*3+3] + b'\xff' if i is not None \
else b'\0\0\0\0' for i in self.to_pixels()])
im.frombytes(data)
return im
def from_Image(self, im, translate=False):
"""Load from a PIL Image instance.
If the input image is 24-bit or 32-bit, the colors will be
looked up in the current palette.
If the input image is 8-bit, indices will simply be copied
from the input image. To properly translate colors between
palettes, set the `translate` parameter.
"""
pixels = im.tobytes()
width, height = im.size
xoff, yoff = (width // 2)-1, height-5
if im.mode == "RGB":
pixels = bytes([self.palette.match(unpack('BBB', \
pixels[i*3:(i+1)*3])) for i in range(width*height)])
self.from_raw(pixels, width, height, xoff, yoff, self.palette)
elif im.mode == "RGBA":
pixels = [unpack('BBBB', pixels[i*4:(i+1)*4]) for i in range(width*height)]
pixels = [self.palette.match(i[0:3]) if i[3] > 0 else None for i in pixels]
self.from_pixels(pixels, width, height, xoff, yoff)
elif im.mode == 'P':
srcpal = im.palette.tobytes()
if im.palette.mode == "RGB":
palsize = 3
elif im.palette.mode == "RGBA":
palsize = 4
else:
raise TypeError("palette mode must be 'RGB' or 'RGBA'")
if translate:
R = [c for c in srcpal[0::palsize]]
G = [c for c in srcpal[1::palsize]]
B = [c for c in srcpal[2::palsize]]
srcpal = zip(R, G, B)
lexicon = [self.palette.match(c) for c in srcpal]
pixels = bytes([lexicon[b] for b in pixels])
else:
# Simply copy pixels. However, make sure to translate
# all colors matching the transparency color to the
# right index. This is necessary because programs
# aren't consistent in choice of position for the
# transparent entry.
packed_color = pack("BBB", *self.palette.tran_color)
packed_index = pack("B", self.palette.tran_index)
ri = 0
while ri != -1:
ri = srcpal.find(packed_color, ri+palsize)
if not ri % palsize and ri//palsize != self.palette.tran_index:
pixels = pixels.replace(pack("B", ri//palsize), packed_index)
self.from_raw(pixels, width, height, xoff, yoff, self.palette)
else:
raise TypeError("image mode must be 'P', 'RGB', or 'RGBA'")
def from_file(self, filename, translate=False):
"""Load graphic from an image file."""
if filename[-4:].lower() == '.lmp':
self.data = readfile(filename)
else:
im = Image.open(filename)
self.from_Image(im, translate)
def to_file(self, filename, mode='P'):
"""Save the graphic to an image file.
The output format is selected based on the filename extension.
For example, "file.jpg" saves to JPEG format. If the file has
no extension, PNG format is used.
Special cases: ".lmp" saves the raw lump data, and ".raw" saves
the raw pixel data.
`mode` may be be 'P', 'RGB', or 'RGBA' for palette or 24/32 bit
output, respectively. However, .raw ignores this parameter and
always writes in palette mode.
"""
format = os.path.splitext(filename)[1][1:].upper()
if format == 'LMP': writefile(filename, self.data)
elif format == 'RAW': writefile(filename, self.to_raw())
else:
im = self.to_Image(mode)
if format:
im.save(filename)
else:
im.save(filename, "PNG")
def translate(self, pal):
"""Translate (in-place) the graphic to another palette."""
lexicon = [pal.match(self.palette.colors[i]) for i in range(256)]
lexicon[self.palette.tran_index] = pal.tran_index
if isinstance(self, Flat):
self.data = bytes([lexicon[b] for b in self.data])
else:
raw = self.to_raw()
self.load_raw(bytes([lexicon[b] for b in raw]),
self.width, self.height,
self.x_offset, self.y_offset)
class Flat(Graphic):
"""Subclass of Graphic, for flat graphics."""
def get_dimensions(self):
sz = len(self.data)
if sz == 4096: return (64, 64)
if sz == 4160: return (64, 65)
if sz == 8192: return (64, 128)
root = int(sz**0.5)
if root**2 != sz:
raise TypeError("unable to determine size: not a square number")
return (root, root)
dimensions = property(get_dimensions)
width = property(lambda self: self.dimensions[0])
height = property(lambda self: self.dimensions[1])
def load_raw(self, data, *unused):
self.data = data
def to_raw(self):
return self.data
omgifol-0.5.1/omg/mapedit.py 0000664 0000000 0000000 00000042251 14433250142 0015732 0 ustar 00root root 0000000 0000000 from omg.util import *
from omg.lump import *
from omg.wad import NameGroup
import omg.lineinfo as lineinfo
import omg.thinginfo as thinginfo
class Vertex(WADStruct):
"""Represents a map vertex."""
_fields_ = [
("x", ctypes.c_int16),
("y", ctypes.c_int16)
]
class GLVertex(WADStruct):
"""Represents a map GL vertex."""
_fields_ = [
("x", ctypes.c_int32),
("y", ctypes.c_int32)
]
class Sidedef(WADStruct):
"""Represents a map sidedef."""
_fields_ = [
("off_x", ctypes.c_int16),
("off_y", ctypes.c_int16),
("tx_up", ctypes.c_char * 8),
("tx_low", ctypes.c_char * 8),
("tx_mid", ctypes.c_char * 8),
("sector", ctypes.c_uint16)
]
def __init__(self, *args, **kwargs):
self.tx_up = self.tx_low = self.tx_mid = "-"
super().__init__(*args, **kwargs)
class Linedef(WADStruct):
"""
Represents a map linedef.
Linedef.NONE is the placeholder value for unused sidedefs.
Using the value -1 is no longer supported.
"""
NONE = 0xffff
_flags_ = [
("impassable", 1),
("block_monsters", 1),
("two_sided", 1),
("upper_unpeg", 1),
("lower_unpeg", 1),
("secret", 1),
("block_sound", 1),
("invisible", 1),
("automap", 1)
]
_fields_ = [
("vx_a", ctypes.c_uint16),
("vx_b", ctypes.c_uint16),
("flags", WADFlags(_flags_)),
("action", ctypes.c_uint16),
("tag", ctypes.c_uint16),
("front", ctypes.c_uint16),
("back", ctypes.c_uint16)
]
_anonymous_ = ("flags",)
def __init__(self, *args, **kwargs):
self.front = self.back = Linedef.NONE
super().__init__(*args, **kwargs)
# TODO: an enum or something for triggers
class ZLinedef(WADStruct):
"""
Represents a map linedef (Hexen / ZDoom).
Linedef.NONE is the placeholder value for unused sidedefs.
Using the value -1 is no longer supported.
"""
_flags_ = [
("impassable", 1),
("block_monsters", 1),
("two_sided", 1),
("upper_unpeg", 1),
("lower_unpeg", 1),
("secret", 1),
("block_sound", 1),
("invisible", 1),
("automap", 1),
("repeat", 1),
("trigger", 3),
("activate_any", 1),
("dummy", 1),
("block_all", 1)
]
_fields_ = [
("vx_a", ctypes.c_uint16),
("vx_b", ctypes.c_uint16),
("flags", WADFlags(_flags_)),
("action", ctypes.c_ubyte),
("arg0", ctypes.c_ubyte),
("arg1", ctypes.c_ubyte),
("arg2", ctypes.c_ubyte),
("arg3", ctypes.c_ubyte),
("arg4", ctypes.c_ubyte),
("front", ctypes.c_uint16),
("back", ctypes.c_uint16)
]
_anonymous_ = ("flags",)
def __init__(self, *args, **kwargs):
self.front = self.back = Linedef.NONE
super().__init__(*args, **kwargs)
class Thing(WADStruct):
"""Represents a map thing."""
_flags_ = [
("easy", 1),
("medium", 1),
("hard", 1),
("deaf", 1),
("multiplayer", 1)
]
_fields_ = [
("x", ctypes.c_int16),
("y", ctypes.c_int16),
("angle", ctypes.c_uint16),
("type", ctypes.c_uint16),
("flags", WADFlags(_flags_))
]
_anonymous_ = ("flags",)
class ZThing(WADStruct):
"""Represents a map thing (Hexen / ZDoom)."""
_flags_ = [
("easy", 1),
("medium", 1),
("hard", 1),
("deaf", 1),
("dormant", 1),
("fighter", 1),
("cleric", 1),
("mage", 1),
("solo", 1),
("multiplayer", 1),
("deathmatch", 1)
]
_fields_ = [
("tid", ctypes.c_uint16),
("x", ctypes.c_int16),
("y", ctypes.c_int16),
("height", ctypes.c_int16),
("angle", ctypes.c_uint16),
("type", ctypes.c_uint16),
("flags", WADFlags(_flags_)),
("action", ctypes.c_ubyte),
("arg0", ctypes.c_ubyte),
("arg1", ctypes.c_ubyte),
("arg2", ctypes.c_ubyte),
("arg3", ctypes.c_ubyte),
("arg4", ctypes.c_ubyte)
]
_anonymous_ = ("flags",)
class Sector(WADStruct):
"""Represents a map sector."""
_fields_ = [
("z_floor", ctypes.c_int16),
("z_ceil", ctypes.c_int16),
("tx_floor", ctypes.c_char * 8),
("tx_ceil", ctypes.c_char * 8),
("light", ctypes.c_uint16),
("type", ctypes.c_uint16),
("tag", ctypes.c_uint16)
]
def __init__(self, *args, **kwargs):
self.z_ceil = 128
self.light = 160
self.tx_floor = "FLOOR4_8"
self.tx_ceil = "CEIL3_5"
super().__init__(*args, **kwargs)
class Node(WADStruct):
"""Represents a BSP tree node."""
_fields_ = [
("x_start", ctypes.c_int16),
("y_start", ctypes.c_int16),
("x_vector", ctypes.c_int16),
("y_vector", ctypes.c_int16),
("right_bbox_top", ctypes.c_int16),
("right_bbox_bottom", ctypes.c_int16),
("right_bbox_left", ctypes.c_int16),
("right_bbox_right", ctypes.c_int16),
("left_bbox_top", ctypes.c_int16),
("left_bbox_bottom", ctypes.c_int16),
("left_bbox_left", ctypes.c_int16),
("left_bbox_right", ctypes.c_int16),
("right_index", ctypes.c_uint16),
("left_index", ctypes.c_uint16)
]
class Seg(WADStruct):
"""Represents a map seg."""
_fields_ = [
("vx_a", ctypes.c_uint16),
("vx_b", ctypes.c_uint16),
("angle", ctypes.c_uint16),
("line", ctypes.c_uint16),
("side", ctypes.c_uint16),
("offset", ctypes.c_uint16)
]
class GLSeg(WADStruct):
"""Represents a map GL seg."""
_fields_ = [
("vx_a", ctypes.c_uint16),
("vx_b", ctypes.c_uint16),
("line", ctypes.c_uint16),
("side", ctypes.c_uint16),
("partner", ctypes.c_uint16)
]
class SubSector(WADStruct):
"""Represents a map subsector."""
_fields_ = [
("numsegs", ctypes.c_uint16),
("seg_a", ctypes.c_uint16)
]
class MapEditor:
"""Doom map editor.
Data members:
header Lump object consisting of data in map header (if any)
vertexes List containing Vertex objects
sidedefs List containing Sidedef objects
linedefs List containing Linedef or ZLinedef objects
sectors List containing Sector objects
things List containing Thing or ZThing objects
Data members (Hexen/ZDoom formats only):
behavior Lump object containing compiled ACS scripts
scripts Lump object containing ACS script source
Other members:
Thing alias to Thing or ZThing class, depending on format
Linedef alias to Linedef or ZLinedef class, depending on format
Currently present but unused:
segs List containing Seg objects
ssectors List containing SubSector objects
nodes List containing Node objects
blockmap Lump object containing blockmap data
reject Lump object containing reject table data
(These five lumps are not updated when saving; you will need to use
an external node builder utility)
"""
def __init__(self, from_lumps=None):
"""Create new, optionally from a lump group."""
if from_lumps is not None:
self.from_lumps(from_lumps)
else:
self.Thing = Thing
self.Linedef = Linedef
self.header = Lump()
self.vertexes = []
self.sidedefs = []
self.linedefs = []
self.sectors = []
self.things = []
self.segs = []
self.ssectors = []
self.nodes = []
self.blockmap = Lump("")
self.reject = Lump("")
def _unpack_lump(self, class_, data):
s = ctypes.sizeof(class_)
return [class_(bytes=data[i:i+s]) for i in range(0,len(data),s)]
def from_lumps(self, lumpgroup):
"""Load entries from a lump group."""
m = lumpgroup
try:
self.header = m["_HEADER_"]
self.vertexes = self._unpack_lump(Vertex, m["VERTEXES"].data)
self.sidedefs = self._unpack_lump(Sidedef, m["SIDEDEFS"].data)
self.sectors = self._unpack_lump(Sector, m["SECTORS"].data)
if "BEHAVIOR" in m:
# Hexen / ZDoom map
self.Thing = ZThing
self.Linedef = ZLinedef
self.behavior = m["BEHAVIOR"]
# optional script sources
try:
self.scripts = m[wcinlist(m, "SCRIPT*")[0]]
except IndexError:
self.scripts = Lump()
else:
self.Thing = Thing
self.Linedef = Linedef
try:
del self.behavior
del self.scripts
except AttributeError:
pass
self.things = self._unpack_lump(self.Thing, m["THINGS"].data)
self.linedefs = self._unpack_lump(self.Linedef, m["LINEDEFS"].data)
except KeyError as e:
raise ValueError("map is missing %s lump" % e)
from struct import error as StructError
try:
self.ssectors = self._unpack_lump(SubSector, m["SSECTORS"].data)
self.segs = self._unpack_lump(Seg, m["SEGS"].data)
self.nodes = self._unpack_lump(Node, m["NODES"].data)
self.blockmap = m["BLOCKMAP"]
self.reject = m["REJECT"]
except (KeyError, StructError):
# nodes failed to build - we don't really care
# TODO: this also "handles" (read: ignores) expanded zdoom nodes)
self.ssectors = []
self.segs = []
self.nodes = []
self.blockmap = Lump()
self.reject = Lump()
def load_gl(self, mapobj):
"""Load GL nodes entries from a map."""
vxdata = mapobj["GL_VERT"].data[4:] # s[:4] == "gNd3" ?
self.gl_vert = self._unpack_lump(GLVertex, vxdata)
self.gl_segs = self._unpack_lump(GLSeg, mapobj["GL_SEGS"].data)
self.gl_ssect = self._unpack_lump(SubSector, mapobj["GL_SSECT"].data)
def to_lumps(self):
m = NameGroup()
m["_HEADER_"] = self.header
m["VERTEXES"] = Lump(join([x.pack() for x in self.vertexes]))
m["THINGS" ] = Lump(join([x.pack() for x in self.things ]))
m["LINEDEFS"] = Lump(join([x.pack() for x in self.linedefs]))
m["SIDEDEFS"] = Lump(join([x.pack() for x in self.sidedefs]))
m["SECTORS" ] = Lump(join([x.pack() for x in self.sectors ]))
m["NODES"] = Lump(join([x.pack() for x in self.nodes ]))
m["SEGS"] = Lump(join([x.pack() for x in self.segs ]))
m["SSECTORS"] = Lump(join([x.pack() for x in self.ssectors]))
m["BLOCKMAP"] = self.blockmap
m["REJECT"] = self.reject
# hexen / zdoom script lumps
try:
m["BEHAVIOR"] = self.behavior
m["SCRIPTS"] = self.scripts
except AttributeError:
pass
return m
def draw_sector(self, vertexes, sector=None, sidedef=None):
"""Draw a polygon from a list of vertexes. The vertexes may be
either Vertex objects or simple (x, y) tuples. A sector object
and prototype sidedef may be provided."""
assert len(vertexes) > 2
firstv = len(self.vertexes)
firsts = len(self.sidedefs)
if sector is None: sector = Sector()
if sidedef is None: sidedef = Sidedef()
self.sectors.append(copy(sector))
for i, v in enumerate(vertexes):
if isinstance(v, tuple):
x, y = v
else:
x, y = v.x, v.y
self.vertexes.append(Vertex(x, y))
for i in range(len(vertexes)):
side = copy(sidedef)
side.sector = len(self.sectors)-1
self.sidedefs.append(side)
#check if the new line is being written over an existing
#and merge them if so.
new_linedef = Linedef(vx_a=firstv+((i+1)%len(vertexes)),
vx_b=firstv+i, front=firsts+i, flags=1)
match_existing = False
for lc in self.linedefs:
if (self.compare_linedefs(new_linedef,lc) > 0):
#remove midtexture and apply it to the upper/lower
side.tx_low = self.sidedefs[lc.front].tx_mid
side.tx_up = self.sidedefs[lc.front].tx_mid
self.sidedefs[lc.front].tx_low = side.tx_mid
self.sidedefs[lc.front].tx_up = side.tx_mid
side.tx_mid = "-"
self.sidedefs[lc.front].tx_mid = "-"
lc.back = len(self.sidedefs)-1
match_existing = True
lc.two_sided = True
lc.impassable = False
break
if (match_existing == False):
self.linedefs.append(new_linedef)
def compare_vertex_positions(self,vertex1,vertex2):
"""Compares the positions of two vertices."""
if (vertex1.x == vertex2.x):
if (vertex1.y == vertex2.y):
return True
return False
def compare_linedefs(self,linedef1,linedef2):
"""Compare the vertex positions of two linedefs.
Returns 0 for mismatch.
Returns 1 when the vertex positions are the same.
Returns 2 when the vertex positions are in the same order.
Returns 3 when the linedefs use the same vertices, but flipped.
Returns 4 when the linedefs use the exact same vertices.
"""
if (linedef1.vx_a == linedef2.vx_a):
if (linedef1.vx_b == linedef2.vx_b):
return 4
if (linedef1.vx_a == linedef2.vx_b):
if (linedef1.vx_b == linedef2.vx_a):
return 3
if (self.compare_vertex_positions(self.vertexes[linedef1.vx_a],
self.vertexes[linedef2.vx_a])):
if (self.compare_vertex_positions(self.vertexes[linedef1.vx_b],
self.vertexes[linedef2.vx_b])):
return 2
if (self.compare_vertex_positions(self.vertexes[linedef1.vx_a],
self.vertexes[linedef2.vx_b])):
if (self.compare_vertex_positions(self.vertexes[linedef1.vx_b],
self.vertexes[linedef2.vx_a])):
return 1
return 0
def compare_sectors(self,sect1,sect2):
"""Compare two sectors' data and returns True when they match."""
if (sect1.z_floor == sect2.z_floor and
sect1.z_ceil == sect2.z_ceil and
sect1.tx_floor == sect2.tx_floor and
sect1.tx_ceil == sect2.tx_ceil and
sect1.light == sect2.light and
sect1.type == sect2.type and
sect1.tag == sect2.tag):
return True
return False
def combine_sectors(self,sector1,sector2,remove_linedefs=True):
"""Combines two sectors together, replacing all references to
the second with the first. If remove_linedefs is true, any
linedefs that connect the two sectors will be removed."""
for sd in self.sidedefs:
if (self.sectors[sd.sector] == sector2):
sd.sector = self.sectors.index(sector1)
if (remove_linedefs):
for lc in self.linedefs:
if (lc.back != Linedef.NONE):
if (self.sectors[self.sidedefs[lc.front].sector] == sector1 and
self.sectors[self.sidedefs[lc.back].sector] == sector1):
self.linedefs.remove(lc)
# we can rely on a nodebuilder to remove unused sectors
# self.sectors[self.sectors.index(sector2)].tx_floor = "_REMOVED"
def paste(self, other, offset=(0,0)):
"""Insert content of another map."""
vlen = len(self.vertexes)
ilen = len(self.sidedefs)
slen = len(self.sectors)
for vx in other.vertexes:
x, y = vx.x, vx.y
self.vertexes.append(Vertex(x+offset[0], y+offset[1]))
for line in other.linedefs:
z = copy(line)
z.vx_a += vlen
z.vx_b += vlen
if z.front != Linedef.NONE: z.front += ilen
if z.back != Linedef.NONE: z.back += ilen
self.linedefs.append(z)
for side in other.sidedefs:
z = copy(side)
z.sector += slen
self.sidedefs.append(z)
for sector in other.sectors:
z = copy(sector)
self.sectors.append(z)
for thing in other.things:
z = copy(thing)
z.x += offset[0]
z.y += offset[1]
self.things.append(z)
omgifol-0.5.1/omg/palette.py 0000664 0000000 0000000 00000023546 14433250142 0015753 0 ustar 00root root 0000000 0000000 from struct import pack, unpack
from omg.util import *
class Palette:
"""Used for storing a list of colors and doing things with them
(such as looking up the best match for arbitrary RGB values).
Palette is distinct from Colormap and Playpal; these two provide
lump- and WAD-related operations while delegating (some of) their
color processing to Palette.
Fields containing useful public data (modifying them directly
probably isn't a good idea, however):
.colors List of (r, g, b) tuples
.bytes Palette's colors as a bytes object (rgbrgbrgb...)
.save_bytes Same as above, but with the transparency color set;
useful when saving files
.tran_color (r, g, b) value for transparency
.tran_index Index in palette of transparency color
The following fields are intended for internal use:
.memo Table for RGB lookup memoization
.grays List of indices of colors with zero saturation
.bright_lut Brightness LUT, used internally to speed up
lookups (when not memoized).
"""
def __init__(self, colors=None, tran_index=None, tran_color=None):
"""Creates a new Palette object. The 'colors' argument may be
either a list of (r,g,b) tuples or an RGBRGBRGB... string/bytes.
'tran_index' specifies the index in the palette where the
transparent color should be placed. Note that this is only used
when saving images, and thus doesn't affect color lookups.
'tran_color' is the color to use for transparency."""
colors = colors or default_colors
tran_index = tran_index or default_tran_index
tran_color = tran_color or default_tran_color
if isinstance(colors, str):
colors = colors.encode('latin-1')
if isinstance(colors, list):
self.colors = colors[:]
elif isinstance(colors, bytes):
self.colors = [unpack('BBB', colors[i:i+3]) for i in range(0,768,3)]
else:
raise TypeError("Argument 'colors' must be list or string or bytes")
# Doom graphics don't actually use indices for transparency; the
# following data is only used when converting between image formats.
self.tran_index = tran_index
self.tran_color = tran_color
self.make_bytes()
self.make_grays()
# Memoizing color translations can speed up RGB-to-palette
# conversions significantly, in particular when converting
# lots of graphics in one session. See docstring for build_lut
# below for description of what bright_lut does.
self.memo = {}
self.bright_lut = []
self.reset_memo()
def make_bytes(self):
"""Create/update 'bytes' and 'save_bytes' from the current set of
colors and the 'tran_index' and 'tran_color' fields."""
self.bytes = bytes().join([pack('BBB', *rgb) for rgb in self.colors])
self.save_bytes = \
self.bytes[:self.tran_index*3] + \
pack('BBB', *self.tran_color) + \
self.bytes[(self.tran_index+1)*3:]
def make_grays(self):
"""Create 'grays' table containing the indices of all grays
in the current set of colors."""
self.grays = [i for i, rgb in enumerate(self.colors) \
if (rgb[0]==rgb[1]==rgb[2])]
def reset_memo(self):
"""Clear the memo table (but (re)add the palette's colors)"""
self.memo = {}
for i in range(len(self.colors)):
if i != self.tran_index:
self.memo[self.colors[i]] = i
def build_lut(self, distance=16):
"""Build 256-entry LUT for looking up colors in the palette
close to a given brightness (range 0-255). Each entry is a
list of indices. No position is empty; in the worst case,
the closest gray can be used.
The 'distance' parameter defines what "close" is, and should
be an integer 0-256. Lower distance means faster lookups,
but worse precision.
A good value for Doom is 10. Anything over 32 only wastes time.
"""
self.bright_lut = []
assert 0 <= distance <= 256
for level in range(256):
candidates = []
for j, rgb in enumerate(self.colors):
if abs(level - (sum(rgb) // 3)) < distance:
candidates.append(j)
# Make sure each entry contains at least one gray
# color that can be relied on in the worst case
best_d = 256
best_i = 0
for gray_index in self.grays:
r, g, b = self.colors[gray_index]
dist = abs(r - level)
if dist < best_d:
best_i = gray_index
if dist == 0:
break
best_d = dist
if best_i not in candidates:
candidates.append(best_i)
self.bright_lut.append(candidates)
def match(self, color):
"""Find the closest match in the palette for a color.
Takes an (r,g,b) tuple as argument and returns a palette index."""
if color == self.tran_color:
return self.tran_index
if color in self.memo:
return self.memo[color]
if len(self.bright_lut) == 0:
self.build_lut()
best_dist = 262144
best_i = 0
ar, ag, ab = color
candidates = self.bright_lut[int(sum(color)) // 3]
for i in candidates:
br, bg, bb = self.colors[i]
dr = ar-br
dg = ag-bg
db = ab-bb
dist = dr*dr + dg*dg + db*db
if dist < best_dist:
if dist == 0:
return i
best_dist = dist
best_i = i
self.memo[color] = best_i
return best_i
def blend(self, color, intensity=0.5):
"""Blend the entire palette against a color (given as an RGB triple).
Intensity must be a floating-point number in the range 0-1."""
assert 0.0 <= intensity <= 1.0
nr = color[0] * intensity
ng = color[1] * intensity
nb = color[2] * intensity
remain = 1.0 - intensity
for i in range(len(self.colors)):
ar, ag, ab = self.colors[i]
self.colors[i] = (int(ar*remain + nr),
int(ag*remain + ng),
int(ab*remain + nb))
self.make_bytes()
self.make_grays()
self.reset_memo()
self.bright_lut = []
# Colors of the Doom palette, used by default
default_colors = b"\
\x00\x00\x00\x1f\x17\x0b\x17\x0f\x07\x4b\x4b\x4b\xff\xff\xff\x1b\
\x1b\x1b\x13\x13\x13\x0b\x0b\x0b\x07\x07\x07\x2f\x37\x1f\x23\x2b\
\x0f\x17\x1f\x07\x0f\x17\x00\x4f\x3b\x2b\x47\x33\x23\x3f\x2b\x1b\
\xff\xb7\xb7\xf7\xab\xab\xf3\xa3\xa3\xeb\x97\x97\xe7\x8f\x8f\xdf\
\x87\x87\xdb\x7b\x7b\xd3\x73\x73\xcb\x6b\x6b\xc7\x63\x63\xbf\x5b\
\x5b\xbb\x57\x57\xb3\x4f\x4f\xaf\x47\x47\xa7\x3f\x3f\xa3\x3b\x3b\
\x9b\x33\x33\x97\x2f\x2f\x8f\x2b\x2b\x8b\x23\x23\x83\x1f\x1f\x7f\
\x1b\x1b\x77\x17\x17\x73\x13\x13\x6b\x0f\x0f\x67\x0b\x0b\x5f\x07\
\x07\x5b\x07\x07\x53\x07\x07\x4f\x00\x00\x47\x00\x00\x43\x00\x00\
\xff\xeb\xdf\xff\xe3\xd3\xff\xdb\xc7\xff\xd3\xbb\xff\xcf\xb3\xff\
\xc7\xa7\xff\xbf\x9b\xff\xbb\x93\xff\xb3\x83\xf7\xab\x7b\xef\xa3\
\x73\xe7\x9b\x6b\xdf\x93\x63\xd7\x8b\x5b\xcf\x83\x53\xcb\x7f\x4f\
\xbf\x7b\x4b\xb3\x73\x47\xab\x6f\x43\xa3\x6b\x3f\x9b\x63\x3b\x8f\
\x5f\x37\x87\x57\x33\x7f\x53\x2f\x77\x4f\x2b\x6b\x47\x27\x5f\x43\
\x23\x53\x3f\x1f\x4b\x37\x1b\x3f\x2f\x17\x33\x2b\x13\x2b\x23\x0f\
\xef\xef\xef\xe7\xe7\xe7\xdf\xdf\xdf\xdb\xdb\xdb\xd3\xd3\xd3\xcb\
\xcb\xcb\xc7\xc7\xc7\xbf\xbf\xbf\xb7\xb7\xb7\xb3\xb3\xb3\xab\xab\
\xab\xa7\xa7\xa7\x9f\x9f\x9f\x97\x97\x97\x93\x93\x93\x8b\x8b\x8b\
\x83\x83\x83\x7f\x7f\x7f\x77\x77\x77\x6f\x6f\x6f\x6b\x6b\x6b\x63\
\x63\x63\x5b\x5b\x5b\x57\x57\x57\x4f\x4f\x4f\x47\x47\x47\x43\x43\
\x43\x3b\x3b\x3b\x37\x37\x37\x2f\x2f\x2f\x27\x27\x27\x23\x23\x23\
\x77\xff\x6f\x6f\xef\x67\x67\xdf\x5f\x5f\xcf\x57\x5b\xbf\x4f\x53\
\xaf\x47\x4b\x9f\x3f\x43\x93\x37\x3f\x83\x2f\x37\x73\x2b\x2f\x63\
\x23\x27\x53\x1b\x1f\x43\x17\x17\x33\x0f\x13\x23\x0b\x0b\x17\x07\
\xbf\xa7\x8f\xb7\x9f\x87\xaf\x97\x7f\xa7\x8f\x77\x9f\x87\x6f\x9b\
\x7f\x6b\x93\x7b\x63\x8b\x73\x5b\x83\x6b\x57\x7b\x63\x4f\x77\x5f\
\x4b\x6f\x57\x43\x67\x53\x3f\x5f\x4b\x37\x57\x43\x33\x53\x3f\x2f\
\x9f\x83\x63\x8f\x77\x53\x83\x6b\x4b\x77\x5f\x3f\x67\x53\x33\x5b\
\x47\x2b\x4f\x3b\x23\x43\x33\x1b\x7b\x7f\x63\x6f\x73\x57\x67\x6b\
\x4f\x5b\x63\x47\x53\x57\x3b\x47\x4f\x33\x3f\x47\x2b\x37\x3f\x27\
\xff\xff\x73\xeb\xdb\x57\xd7\xbb\x43\xc3\x9b\x2f\xaf\x7b\x1f\x9b\
\x5b\x13\x87\x43\x07\x73\x2b\x00\xff\xff\xff\xff\xdb\xdb\xff\xbb\
\xbb\xff\x9b\x9b\xff\x7b\x7b\xff\x5f\x5f\xff\x3f\x3f\xff\x1f\x1f\
\xff\x00\x00\xef\x00\x00\xe3\x00\x00\xd7\x00\x00\xcb\x00\x00\xbf\
\x00\x00\xb3\x00\x00\xa7\x00\x00\x9b\x00\x00\x8b\x00\x00\x7f\x00\
\x00\x73\x00\x00\x67\x00\x00\x5b\x00\x00\x4f\x00\x00\x43\x00\x00\
\xe7\xe7\xff\xc7\xc7\xff\xab\xab\xff\x8f\x8f\xff\x73\x73\xff\x53\
\x53\xff\x37\x37\xff\x1b\x1b\xff\x00\x00\xff\x00\x00\xe3\x00\x00\
\xcb\x00\x00\xb3\x00\x00\x9b\x00\x00\x83\x00\x00\x6b\x00\x00\x53\
\xff\xff\xff\xff\xeb\xdb\xff\xd7\xbb\xff\xc7\x9b\xff\xb3\x7b\xff\
\xa3\x5b\xff\x8f\x3b\xff\x7f\x1b\xf3\x73\x17\xeb\x6f\x0f\xdf\x67\
\x0f\xd7\x5f\x0b\xcb\x57\x07\xc3\x4f\x00\xb7\x47\x00\xaf\x43\x00\
\xff\xff\xff\xff\xff\xd7\xff\xff\xb3\xff\xff\x8f\xff\xff\x6b\xff\
\xff\x47\xff\xff\x23\xff\xff\x00\xa7\x3f\x00\x9f\x37\x00\x93\x2f\
\x00\x87\x23\x00\x4f\x3b\x27\x43\x2f\x1b\x37\x23\x13\x2f\x1b\x0b\
\x00\x00\x53\x00\x00\x47\x00\x00\x3b\x00\x00\x2f\x00\x00\x23\x00\
\x00\x17\x00\x00\x0b\x00\x00\x00\xff\x9f\x43\xff\xe7\x4b\xff\x7b\
\xff\xff\x00\xff\xcf\x00\xcf\x9f\x00\x9b\x6f\x00\x6b\xa7\x6b\x6b\
"
# Defaults for image transparency
default_tran_index = 247
default_tran_color = (255, 0, 255)
# Default palette object, using the default values
default = Palette()
omgifol-0.5.1/omg/playpal.py 0000664 0000000 0000000 00000005257 14433250142 0015756 0 ustar 00root root 0000000 0000000 from omg.lump import Lump
from omg.util import *
import omg.palette
class Playpal:
"""An editor for Doom's PLAYPAL lump. The PLAYPAL lump contains 14
palettes: the game's base palette, the palettes used when the
player is in pain or uses the berserk powerup, the palettes used
when the player picks up an item, and the palette used when the
player is wearing the radiation suit.
The palettes are located in a member list called 'palettes'. Each
palette is a Palette instance."""
def __init__(self, source=None):
"""Construct a new EditPlaypal object. Source may be a PLAYPAL
lump or a Palette instance. If a Palette instance, all 14
palettes are set to copies of it. If no source is specified,
the default palette is used."""
if isinstance(source, Lump):
self.from_lump(source)
else:
self.set_base(source)
def build_defaults(self):
"""Build all 13 extra palettes, using default values (red for
pain, yellow for item pickups, green for the radiation suit).
The values used here are not exactly the same as those in
Doom's PLAYPAL, but decent approximations."""
self.build_pain()
self.build_item()
self.build_suit()
def build_suit (self, color=(0,255,0), intensity=0.2):
"""Set the color and intensity for the radiation suit palette."""
self.palettes[13].blend(color, intensity)
def build_pain (self, color=(255,0,0), minintensity=0.1, maxintensity=0.8):
"""Set the color and intensities for the player-in-pain (also
used by the berserk powerup) palettes."""
step = (maxintensity - minintensity) / 8.0
for i in range(8):
self.palettes[i+1].blend(color, step*i + minintensity)
def build_item (self, color=(255,255,64), minintensity=0.1, maxintensity=0.3):
"""Set color and intensity for the item pick-up palettes."""
step = (maxintensity - minintensity) / 3.0
for i in range(3):
self.palettes[i+10].blend(color, step * i + minintensity)
def from_lump(self, lump):
"""Load data from a PLAYPAL lump."""
self.palettes = [omg.palette.Palette(lump.data[i*768:(i+1)*768], 0, 0)
for i in range(14)]
def to_lump(self):
"""Compile to a Doom-ready PLAYPAL Lump."""
return Lump(join([p.bytes for p in self.palettes]))
def set_base(self, palette=None):
"""Set all palettes to copies of a given Palette object. If the
palette parameter is not provided, the default palette is used."""
palette = palette or omg.palette.default
self.palettes = [deepcopy(palette) for i in range(14)]
omgifol-0.5.1/omg/thinginfo.py 0000664 0000000 0000000 00000006723 14433250142 0016300 0 ustar 00root root 0000000 0000000 # Map thing types... this needs to be expanded
# Map descriptions to numbers and vice versa
all_desc2num = {}
all_num2desc = {}
class ThingCategory:
def __init__(self, table):
global all_desc2num
global all_num2desc
rev = dict([(b, a) for a, b in table.items()])
all_desc2num.update(table)
all_num2desc.update(rev)
self.table = dict([(x, None) for x in table])
def __contains__(self, item):
global all_desc2num
global all_num2desc
if isinstance(item, str):
return item in self.table
elif isinstance(item, int):
return all_num2desc[item] in self.table
else:
raise TypeError
monsters = ThingCategory({
"zombie":3004,
"sergeant":9,
"commando":65,
"imp":3001,
"demon":3002,
"spectre":58,
"lost soul":3006,
"cacodemon":3005,
"hell knight":69,
"baron of hell":3003,
"revenant":66,
"mancubus":67,
"arachnotron":68,
"pain elemental":71,
"archvile":64,
"cyberdemon":16,
"spider mastermind":7,
"ss guy":84,
"spawn target":87,
"spawn shooter":89,
"romero head":88,
"commander keen":72
})
weapons = ThingCategory({
"shotgun":2001,
"super shotgun":82,
"chaingun":2002,
"rocket launcher":2003,
"plasma gun":2004,
"chainsaw":2005,
"bfg 9000":2006
})
ammo = ThingCategory({
"ammo clip":2007,
"ammo box":2048,
"shells":2008,
"shell box":2049,
"rocket":2010,
"rocket box":2046,
"cell charge":2047,
"cell pack":17,
"backpack":8
})
powerups = ThingCategory({
"stimpack":2011,
"medikit":2012,
"supercharge":2013,
"health bonus":2014,
"armor bonus":2015,
"green armor":2018,
"blue armor":2019,
"invulnerability":2022,
"berserk":2023,
"invisibility":2024,
"radiation suit":2025,
"computer map":2026,
"goggles":2048,
"megasphere":83
})
keys = ThingCategory({
"red keycard":13,
"yellow keycard":6,
"blue keycard":5,
"red skull key":38,
"yellow skull key":39,
"blue skull key":40
})
starts = ThingCategory({
"player 1 start":1,
"player 2 start":2,
"player 3 start":3,
"player 4 start":4,
"deathmatch start":11,
"teleport destination":14
})
"""
doom2_only = ThingCategory([
"super shotgun",
"megasphere",
"archvile",
"chaingunner",
"revenant",
"mancubus",
"arachnotron",
"hell knight",
"pain elemental",
"ss guy",
"spawn target",
"spawn shooter",
"romero head",
"commander keen"
"flaming barrel",
])
"""
corpses = ThingCategory({
"gibs 1":10,
"gibs 2":12,
"dead marine":15,
"dead zombie":18,
"dead sergeant":19,
"dead imp":20,
"dead demon":21,
"dead cacodemon":22,
"dead lost soul":23,
"pool of blood":24,
"impaled human 1":25,
"impaled human 2":26,
"skull on pole":27,
"five skulls":28,
"skull pile":29,
"hangman 1":49,
"hangman 2":50,
"hangman 3":51,
"hangman 4":52,
"hangman 5":53,
"hangman 2 (passable)":59,
"hangman 4 (passable)":60,
"hangman 3 (passable)":61,
"hangman 5 (passable)":62,
"hangman 1 (passable)":63
})
decorations = ThingCategory({
"green pillar":30,
"short green pillar":31,
"red pillar":32,
"short red pillar":33,
"candle":34,
"candelabra":35,
"green pillar with heart":36,
"red pillar with skull":37,
"eye":41,
"skull rock":42,
"gray tree":43,
"blue torch":44,
"green torch":45,
"red torch":46,
"scrub":47,
"tech column":48,
"brown tree":54,
"short blue torch":55,
"short green torch":56,
"short red torch":57,
"floor lamp":2028,
"barrel":2035
})
omgifol-0.5.1/omg/txdef.py 0000664 0000000 0000000 00000010514 14433250142 0015416 0 ustar 00root root 0000000 0000000 from omg.lump import Lump
from omg.util import *
from omg.wad import TxdefGroup
class TextureDef(WADStruct):
"""Class for texture definitions."""
_fields_ = [
("name", ctypes.c_char * 8),
("dummy1", ctypes.c_uint32),
("width", ctypes.c_int16),
("height", ctypes.c_int16),
("dummy2", ctypes.c_uint32),
("npatches", ctypes.c_int16),
]
def __init__(self, *args, **kwargs):
self.name = "-"
self.patches = []
super().__init__(*args, **kwargs)
class PatchDef(WADStruct):
"""Class for patches."""
_fields_ = [
("x", ctypes.c_int16),
("y", ctypes.c_int16),
("id", ctypes.c_int16),
("dummy1", ctypes.c_uint16),
("dummy2", ctypes.c_uint16)
]
def __init__(self, *args, **kwargs):
self.name = "-"
self.id = -1
super().__init__(*args, **kwargs)
# TODO: integrate with textures lump group instead?
class Textures(OrderedDict):
"""An editor for Doom's TEXTURE1, TEXTURE2 and PNAMES lumps."""
def __init__(self, *args):
"""Create new, optionally loading content from given
TEXTURE1/2 and PNAMES lumps or a txdefs group. E.g.:
Textures(texture1, pnames)
Textures(txdefs)
"""
OrderedDict.__init__(self)
if len(args):
self.from_lumps(*args)
def from_lumps(self, *args):
"""Load texture definitions from a TEXTURE1/2 lump and its
associated PNAMES lump, or a lump group containing the lumps."""
from omg.wad import LumpGroup
if len(args) == 1:
g = args[0]
assert isinstance(g, LumpGroup)
if "TEXTURE1" in g: self.from_lumps(g["TEXTURE1"], g["PNAMES"])
if "TEXTURE2" in g: self.from_lumps(g["TEXTURE2"], g["PNAMES"])
elif len(args) == 2:
self._from_lumps(args[0], args[1])
def _from_lumps(self, texture1, pnames):
# Unpack PNAMES
numdefs = unpack16(pnames.data[0:2])
pnames = [zstrip(pnames.data[ptr:ptr+8]) \
for ptr in range(4, 8*numdefs+4, 8)]
# Unpack TEXTURE1
data = texture1.data
numtextures = unpack('