Skip to content

Commit 78dd6d9

Browse files
committed
Fix Python 3 conversion issues
1 parent 6d156fe commit 78dd6d9

10 files changed

+65
-62
lines changed

README.md

+14-16
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,17 @@ To use this, you'll need a few things:
5252

5353
- A copy of Fallout 2 (already installed)
5454

55-
- Python 2.7
55+
- Python 3.9, earlier minor versions of Python 3 may work, but are not tested. Python 2 is not supported.
5656

5757
- [Pillow](https://pillow.readthedocs.io/en/4.0.x) (just `pip install pillow`)
5858

5959
- [NumPy](http://www.numpy.org/) (Windows binaries available [here](http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy).)
6060

61-
- The TypeScript compiler, installed via `npm install -g typescript` (you'll need [node.js](https://nodejs.org/en/)).
61+
- The TypeScript compiler, installed via `npm install` (you'll need [node.js](https://nodejs.org/en/)).
6262

6363
You'll need an HTTP server to run (despite being all static content) due to the way browsers sandbox requests.
6464
If you're comfortable with setting up nginx, lighttpd, or Apache, go for that. If not, a simple way is to use Python:
6565

66-
- Python 2: `python -m SimpleHTTPServer` (Python 2 is already required anyway)
6766
- Python 3: `python -m http.server`
6867

6968
Alternatively, Firefox can load directly from `file://`.
@@ -78,7 +77,7 @@ This will take a few minutes, it's unpacking the game archives and converting re
7877

7978
NOTE: You may need to use `python2` instead, as some Linux distributions package `python` as Python 3. Run `python --version` to check!
8079

81-
Then run `tsc` to compile the source code.
80+
Then run `npx tsc` after you've run `npm install` to compile the source code.
8281

8382
Browse to `http://localhost/play.html?artemple` (or whatever port you're using). If all went well, it should begin the game. If not, check the JavaScript console for errors.
8483

@@ -89,32 +88,32 @@ OPTIONAL: If you want sound, run `python convertAudio.py`. You'll need the `acm2
8988
## FAQ
9089

9190
- **Q:** Why TypeScript? Why a browser?
92-
91+
9392
A: Everyone has a browser: it's a portable platform for running code with more features than people expect.
94-
There are other projects that use native code already... and are already seeing segfaults. :)
93+
There are other projects that use native code already... and are already seeing segfaults. :)
9594

96-
The project started out in JavaScript and was ported to TypeScript as it was continuing to grow. TypeScript strikes
97-
an excellent balance between useful and safe.
95+
The project started out in JavaScript and was ported to TypeScript as it was continuing to grow. TypeScript strikes
96+
an excellent balance between useful and safe.
9897

9998
- **Q:** But why Python?
100-
99+
101100
A: Python is actually quite fast when written well, despite many peoples' expectations. It is very elegant and allows me to write
102-
backend code like file parsers and exporters with tiny code, very few troubles, and that I know is portable and safe.
101+
backend code like file parsers and exporters with tiny code, very few troubles, and that I know is portable and safe.
103102

104103
- **Q:** Why do I need `acm2wav` for sound?
105-
104+
106105
A: Because it hasn't been ported to Python yet. If you're willing to contribute, give it a shot: the original Pascal source code is available online.
107106

108107
Additionally, FFmpeg might be able to transcode ACM audio, so give that a shot. (See #30.)
109-
108+
110109
- **Q:** Why convert all assets up front, why not load them directly?
111-
110+
112111
A: Because it would require more processing time to load them each time they're needed rather than having them already in a sane, modern format.
113-
112+
114113
By converting, for example, FRMs (a proprietary Interplay format) to PNGs (a ubiquitous, open modern format) we allow normal browsers or image viewers to open them, as well as edit them -- a huge win for modders. Other games or tools could take advantage of the new formats as well.
115114

116115
- **Q:** Why do this at all?
117-
116+
118117
A: Why not? It's a fun project, and I love Fallout. Fallout 1 and 2 do not run particularly well on modern machines, even with engine hacks. They're also hard to mod -- I'd like to change that.
119118

120119
## License
@@ -129,7 +128,6 @@ Testing is more than welcome: if you have issues running DarkFO, or if you find
129128

130129
To contribute code, simply submit a pull request with your changes. Take care to write sensible commit messages, and if you want to change major parts of the code, please discuss it with other developers first (see the Contact section below).
131130
I apologize in advance for any injury sustained while reading the code. :)
132-
133131

134132
Thanks!
135133

dat2.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ def mkdirs(path):
9999
if not os.path.exists(dir):
100100
os.mkdir(dir)
101101

102-
def dumpFiles(f, outDir, verbose=True):
102+
def dumpFiles(f, outDir, verbose=False):
103103
dirTree = readDAT(f, posixPaths=True)
104104
numFiles = len(dirTree)
105105
i = 1
106106

107-
for filename, fileEntry in dirTree.iteritems():
107+
for filename, fileEntry in dirTree.items():
108108
outPath = os.path.join(outDir, filename)
109109
mkdirs(outDir + "/" + os.path.dirname(filename))
110110

exportImages.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def main():
6868
i += 1
6969
continue
7070

71-
print "[%d/%d/%d] %s..." % (i, len(FRMs), totalNum, name)
71+
print("[%d/%d/%d] %s..." % (i, len(FRMs), totalNum, name))
7272
imageInfo['art/'+name] = frmpixels.exportFRM(FRM, outpath, palette, exportImage)
7373

7474
i += 1
@@ -79,13 +79,13 @@ def main():
7979
imageMap.update(imageInfo)
8080
imageInfo = imageMap
8181

82-
print "writing image map..."
82+
print("writing image map...")
8383

8484
# write new imageMap
8585
json.dump(imageInfo, open('%s/imageMap.json' % OUT_DIR, "w"))
8686

8787

8888
if __name__ == '__main__':
89-
start_time = time.clock()
89+
start_time = time.perf_counter()
9090
main()
91-
print("--- %s seconds ---" % (time.clock() - start_time))
91+
print("--- %s seconds ---" % (time.perf_counter() - start_time))

exportImagesPar.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def readPAL(path):
7474
def convertAll(palette, dataDir, outDir, mode='both', imageMapMode='yes', nProcs=N_PROCS, verbose=False):
7575
# Convert FRMs and FR[0-9]s, and output an image map
7676

77-
start_time = time.clock()
77+
start_time = time.perf_counter()
7878

7979
if not os.path.exists(outDir):
8080
os.mkdir(outDir)
@@ -122,7 +122,7 @@ def convertAll(palette, dataDir, outDir, mode='both', imageMapMode='yes', nProcs
122122
# write new imageMap
123123
json.dump(imageInfo, open(outDir + "/imageMap.json", "w"))
124124

125-
return time.clock() - start_time
125+
return time.perf_counter() - start_time
126126

127127
def main():
128128
if len(sys.argv) < 5:

exportPRO.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def extractPROs(dataProtoPath, outDir, onError=onError, verbose=False):
5454

5555
with open(os.path.join(outDir, "pro.json"), "w") as fp:
5656
print("Writing master JSON map...")
57-
json.dump(root_map, fp)
57+
json.dump(root_map, fp, indent=2)
5858

5959
def main():
6060
extractPROs(os.path.join("data", "proto"), "proto", verbose=True)

fomap.py

+28-25
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,26 @@
1717
# Parser/converter for Fallout 1 (partially) and 2 .MAP files to a JSON format
1818

1919
from __future__ import print_function
20+
from io import BufferedReader
2021
import sys, os, struct, json
22+
from typing import Any, Dict
2123

2224
FO1_MODE = None # global
2325
DATA_DIR = None # global
2426
SCRIPT_TYPES = ["s_system", "s_spatial", "s_time", "s_item", "s_critter"]
2527

26-
mapScriptPIDs = [{} for _ in range(5)]
28+
mapScriptPIDs: list[Dict[int, Any]] = [{} for _ in range(5)]
2729

28-
def read16(f):
30+
def read16(f) -> int:
2931
return struct.unpack("!h", f.read(2))[0]
3032

31-
def readU16(f):
33+
def readU16(f) -> int:
3234
return struct.unpack("!H", f.read(2))[0]
3335

34-
def read32(f):
36+
def read32(f) -> int:
3537
return struct.unpack("!l", f.read(4))[0]
3638

37-
def readU32(f):
39+
def readU32(f) -> int:
3840
return struct.unpack("!L", f.read(4))[0]
3941

4042
def stripExt(path):
@@ -78,7 +80,7 @@ def parseTiles(f, numLevels):
7880

7981
return floorTiles, roofTiles
8082

81-
def parseMapScripts(f, scriptLst):
83+
def parseMapScripts(f, scriptLst: list[bytes]):
8284
totalScriptCount = 0
8385
spatials = []
8486

@@ -133,7 +135,7 @@ def parseMapScripts(f, scriptLst):
133135
print("script:", script)
134136
else:
135137
#print "script id:", script.spatialScriptID, "or", (script.spatialScriptID & 0xffff)
136-
scriptName = stripExt(scriptLst[scriptID].split()[0])
138+
scriptName = stripExt(scriptLst[scriptID].decode('ascii').split()[0])
137139
spatials.append({"tileNum": tileNum & 0xffff,
138140
"elevation": ((tileNum >> 28) & 0xf) >> 1,
139141
"range": spatialRange,
@@ -155,7 +157,7 @@ def parseMapScripts(f, scriptLst):
155157
return {"count": totalScriptCount, "spatials": spatials, "mapScriptPIDs": mapScriptPIDs}
156158

157159
# TODO: rewrite this
158-
def getCritterArtPath(frmPID, critterLst):
160+
def getCritterArtPath(frmPID, critterLst: list[bytes]):
159161
idx = frmPID & 0x00000fff
160162
id1 = (frmPID & 0x0000f000) >> 12
161163
id2 = (frmPID & 0x00ff0000) >> 16
@@ -167,7 +169,7 @@ def getCritterArtPath(frmPID, critterLst):
167169
id2 == 0x21 or id2 == 0x40):
168170
raise Exception("reindex")
169171

170-
path = "art/critters/" + critterLst[idx].split(",")[0].upper()
172+
path = "art/critters/" + critterLst[idx].decode('ascii').split(",")[0].upper()
171173

172174
if (id1 >= 0x0b):
173175
raise Exception("?")
@@ -209,9 +211,9 @@ def getCritterArtPath(frmPID, critterLst):
209211

210212
def parseItemObj(f, frmPID, protoPID, itemsLst, itemsProtoLst):
211213
item = {"type": "item",
212-
"artPath": "art/items/" + stripExt(itemsLst[frmPID & 0xffff])
214+
"artPath": "art/items/" + (stripExt(itemsLst[frmPID & 0xffff]).decode('ascii'))
213215
}
214-
subtype = getProSubType("proto/items/" + itemsProtoLst[(protoPID & 0xffff) - 1])
216+
subtype = getProSubType("proto/items/" + itemsProtoLst[(protoPID & 0xffff) - 1].decode('ascii'))
215217

216218
if subtype == 4: # ammo
217219
item["subtype"] = "ammo"
@@ -234,7 +236,7 @@ def parseItemObj(f, frmPID, protoPID, itemsLst, itemsProtoLst):
234236

235237
return item
236238

237-
def parseObject(f, lstFiles):
239+
def parseObject(f, lstFiles: dict[str, list[bytes]]):
238240
f.read(4) # unknown (separator)
239241
position = read32(f)
240242
f.read(4*4) # unknown
@@ -275,7 +277,7 @@ def parseObject(f, lstFiles):
275277

276278
elif objType == 3: # wall
277279
namedType = "wall"
278-
art = "art/walls/" + stripExt(lstFiles["walls"][frmPID & 0xffff])
280+
art = "art/walls/" + stripExt(lstFiles["walls"][frmPID & 0xffff].decode('ascii'))
279281

280282
elif objType == 1: # critter
281283
namedType = "critter"
@@ -290,8 +292,9 @@ def parseObject(f, lstFiles):
290292

291293
elif objType == 2: # scenery
292294
namedType = "scenery"
293-
art = "art/scenery/" + stripExt(lstFiles["scenery"][frmPID & 0xffff])
294-
subtype = getProSubType("proto/scenery/" + lstFiles["proto_scenery"][(protoPID & 0xffff) - 1])
295+
art = "art/scenery/" + stripExt(lstFiles["scenery"][frmPID & 0xffff].decode('ascii'))
296+
proto_scenery = lstFiles["proto_scenery"]
297+
subtype = getProSubType("proto/scenery/" + (proto_scenery[(protoPID & 0xffff) - 1]).decode('ascii'))
295298

296299
"""
297300
# TODO: why have this? this just goes into the real "extra" field anyway
@@ -322,7 +325,7 @@ def parseObject(f, lstFiles):
322325

323326
elif objType == 5: # misc
324327
namedType = "misc"
325-
art = "art/misc/" + stripExt(lstFiles["misc"][frmPID & 0xffff])
328+
art = "art/misc/" + stripExt(lstFiles["misc"][frmPID & 0xffff].decode('ascii'))
326329

327330
# exit grids
328331
if (protoPID & 0xffff) != 1 and (protoPID & 0xffff) != 12:
@@ -366,28 +369,28 @@ def parseObject(f, lstFiles):
366369
# obj["art"] = extra["artPath"].lower()
367370

368371
if scriptID != -1:
369-
scriptName = stripExt(lstFiles["scripts"][scriptID].split()[0])
372+
scriptName = stripExt(lstFiles["scripts"][scriptID].decode('ascii').split()[0])
370373
obj["script"] = scriptName
371374
elif scriptID == -1 and mapPID != 0xFFFFFFFF:
372375
# try to use the script IDs mapped in parseMapScripts
373376
scriptType = (mapPID >> 24) & 0xff
374377
scriptPID = mapPID & 0xffff
375378
print("using map script for %s (script PID %d)" % (art, scriptPID))
376-
scriptName = mapScriptPIDs[scriptType][scriptPID]
379+
scriptName = mapScriptPIDs[scriptType][scriptPID].decode('ascii')
377380
print("(map script %d type %d = %s)" % (scriptPID, scriptType, scriptName))
378381
obj["script"] = scriptName
379382

380383
return obj
381384

382-
def parseLevelObjects(f, lstFiles):
385+
def parseLevelObjects(f, lstFiles: dict[str, list[bytes]]):
383386
numObjects = readU32(f)
384387
return [parseObject(f, lstFiles) for _ in range(numObjects)]
385388

386-
def parseMapObjects(f, numLevels, lstFiles):
389+
def parseMapObjects(f, numLevels, lstFiles: dict[str, list[bytes]]):
387390
numObjects = readU32(f)
388391
return [parseLevelObjects(f, lstFiles) for _ in range(numLevels)]
389392

390-
def parseMap(f, lstFiles):
393+
def parseMap(f: BufferedReader, lstFiles: dict[str, list[bytes]]):
391394
global FO1_MODE
392395

393396
version = readU32(f)
@@ -430,7 +433,7 @@ def parseMap(f, lstFiles):
430433
levels = []
431434

432435
def transformTile(idx):
433-
return stripExt(lstFiles["tiles"][idx].rstrip()).lower()
436+
return stripExt(lstFiles["tiles"][idx].decode('ascii').rstrip()).lower()
434437

435438
def transformTileMap(tileMap):
436439
return [[transformTile(idx) for idx in row] for row in tileMap]
@@ -470,7 +473,7 @@ def getImageList(map):
470473

471474
return list(images)
472475

473-
def exportMap(dataDir, mapFile, outFile, verbose=False):
476+
def exportMap(dataDir: str, mapFile: str, outFile: str, verbose=False):
474477
global DATA_DIR
475478

476479
DATA_DIR = dataDir # TODO: make this not global
@@ -487,14 +490,14 @@ def exportMap(dataDir, mapFile, outFile, verbose=False):
487490
}
488491

489492
with open(mapFile, "rb") as fin:
490-
with open(outFile, "wb") as fout:
493+
with open(outFile, "w") as fout:
491494
if verbose: print("writing %s..." % outFile)
492495
map = parseMap(fin, lstFiles)
493496
json.dump(map, fout)
494497

495498
# write image list
496499
if verbose: print("writing image list...")
497-
with open(stripExt(outFile) + ".images.json", "wb") as fimg:
500+
with open(stripExt(outFile) + ".images.json", "w") as fimg:
498501
json.dump(getImageList(map), fimg)
499502

500503
if verbose: print("done")

frmpixels.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
# FRM parsing/conversion library
1818

19+
from io import BufferedReader
1920
import sys, os, struct, json
2021
import pal
2122
from PIL import Image
@@ -33,7 +34,7 @@ def read16At(buf, idx):
3334
def read32At(buf, idx):
3435
return struct.unpack('!l', buf[idx:idx + 4])[0]
3536

36-
def readFRMInfo(f, exportImage=True):
37+
def readFRMInfo(f: BufferedReader, exportImage=True):
3738
_version = read32(f)
3839
fps = read16(f)
3940
_actionFrame = read16(f)
@@ -67,7 +68,7 @@ def readFRMInfo(f, exportImage=True):
6768

6869
frameSize = read32At(framesData, ptr + 4) # w*h
6970
if exportImage:
70-
framePixels[nDir].append(np.array([ord(byte) for byte in framesData[ptr + 12 : ptr + 12 + frameSize]], np.uint8))
71+
framePixels[nDir].append(np.array([byte for byte in framesData[ptr + 12 : ptr + 12 + frameSize]], np.uint8))
7172

7273
ptr += 12 + pixelDataSize
7374

0 commit comments

Comments
 (0)