Allow dumping decompiled bytecode to file for both raw afp/bsi pairs and TXP2 containers.
This commit is contained in:
parent
ee3e272787
commit
d9550122de
@ -1,5 +1,23 @@
|
|||||||
from .geo import Shape, DrawParams
|
from .geo import Shape, DrawParams
|
||||||
from .swf import SWF, NamedTagReference
|
from .swf import (
|
||||||
|
SWF,
|
||||||
|
NamedTagReference,
|
||||||
|
Frame,
|
||||||
|
Tag,
|
||||||
|
TagPointer,
|
||||||
|
AP2ShapeTag,
|
||||||
|
AP2DefineFontTag,
|
||||||
|
AP2TextChar,
|
||||||
|
AP2TextLine,
|
||||||
|
AP2DefineMorphShapeTag,
|
||||||
|
AP2DefineButtonTag,
|
||||||
|
AP2DefineTextTag,
|
||||||
|
AP2DoActionTag,
|
||||||
|
AP2PlaceObjectTag,
|
||||||
|
AP2RemoveObjectTag,
|
||||||
|
AP2DefineSpriteTag,
|
||||||
|
AP2DefineEditTextTag,
|
||||||
|
)
|
||||||
from .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2
|
from .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2
|
||||||
from .render import AFPRenderer
|
from .render import AFPRenderer
|
||||||
from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer
|
from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer
|
||||||
@ -9,6 +27,21 @@ __all__ = [
|
|||||||
'Shape',
|
'Shape',
|
||||||
'DrawParams',
|
'DrawParams',
|
||||||
'SWF',
|
'SWF',
|
||||||
|
'Frame',
|
||||||
|
'Tag',
|
||||||
|
'TagPointer',
|
||||||
|
'AP2ShapeTag',
|
||||||
|
'AP2DefineFontTag',
|
||||||
|
'AP2TextChar',
|
||||||
|
'AP2TextLine',
|
||||||
|
'AP2DefineMorphShapeTag',
|
||||||
|
'AP2DefineButtonTag',
|
||||||
|
'AP2DefineTextTag',
|
||||||
|
'AP2DoActionTag',
|
||||||
|
'AP2PlaceObjectTag',
|
||||||
|
'AP2RemoveObjectTag',
|
||||||
|
'AP2DefineSpriteTag',
|
||||||
|
'AP2DefineEditTextTag',
|
||||||
'NamedTagReference',
|
'NamedTagReference',
|
||||||
'TXP2File',
|
'TXP2File',
|
||||||
'PMAN',
|
'PMAN',
|
||||||
|
@ -177,7 +177,7 @@ class AP2TextLine:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2DefineMorphShape(Tag):
|
class AP2DefineMorphShapeTag(Tag):
|
||||||
def __init__(self, id: int) -> None:
|
def __init__(self, id: int) -> None:
|
||||||
# TODO: I need to figure out what morph shapes actually DO, and take the
|
# TODO: I need to figure out what morph shapes actually DO, and take the
|
||||||
# values that I parsed out store them here...
|
# values that I parsed out store them here...
|
||||||
@ -189,7 +189,7 @@ class AP2DefineMorphShape(Tag):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AP2DefineButton(Tag):
|
class AP2DefineButtonTag(Tag):
|
||||||
def __init__(self, id: int) -> None:
|
def __init__(self, id: int) -> None:
|
||||||
# TODO: I need to figure out what buttons actually DO, and take the
|
# TODO: I need to figure out what buttons actually DO, and take the
|
||||||
# values that I parsed out store them here...
|
# values that I parsed out store them here...
|
||||||
@ -326,7 +326,7 @@ class AP2RemoveObjectTag(Tag):
|
|||||||
|
|
||||||
|
|
||||||
class AP2DefineSpriteTag(Tag):
|
class AP2DefineSpriteTag(Tag):
|
||||||
def __init__(self, id: int, tags: List[Tag], frames: List[Frame], references: Dict[int, str]) -> None:
|
def __init__(self, id: int, tags: List[Tag], frames: List[Frame], references: Dict[str, int]) -> None:
|
||||||
super().__init__(id)
|
super().__init__(id)
|
||||||
|
|
||||||
# The list of tags that this sprite consists of. Sprites are, much like vanilla
|
# The list of tags that this sprite consists of. Sprites are, much like vanilla
|
||||||
@ -428,7 +428,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.frames: List[Frame] = []
|
self.frames: List[Frame] = []
|
||||||
|
|
||||||
# Reference LUT for mapping object reference IDs to names a used in bytecode.
|
# Reference LUT for mapping object reference IDs to names a used in bytecode.
|
||||||
self.references: Dict[int, str] = {}
|
self.references: Dict[str, int] = {}
|
||||||
|
|
||||||
# SWF string table. This is used for faster lookup of strings as well as
|
# SWF string table. This is used for faster lookup of strings as well as
|
||||||
# tracking which strings in the table have been parsed correctly.
|
# tracking which strings in the table have been parsed correctly.
|
||||||
@ -934,7 +934,18 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
|
|
||||||
return ByteCode(bytecode_name, actions, offset_ptr)
|
return ByteCode(bytecode_name, actions, offset_ptr)
|
||||||
|
|
||||||
def __parse_tag(self, ap2_version: int, afp_version: int, ap2data: bytes, tagid: int, size: int, dataoffset: int, tag_frame: int, prefix: str = "") -> Tag:
|
def __parse_tag(
|
||||||
|
self,
|
||||||
|
ap2_version: int,
|
||||||
|
afp_version: int,
|
||||||
|
ap2data: bytes,
|
||||||
|
tagid: int,
|
||||||
|
size: int,
|
||||||
|
dataoffset: int,
|
||||||
|
tag_parent_sprite: Optional[int],
|
||||||
|
tag_frame: int,
|
||||||
|
prefix: str = "",
|
||||||
|
) -> Tag:
|
||||||
if tagid == AP2Tag.AP2_SHAPE:
|
if tagid == AP2Tag.AP2_SHAPE:
|
||||||
if size != 4:
|
if size != 4:
|
||||||
raise Exception(f"Invalid shape size {size}")
|
raise Exception(f"Invalid shape size {size}")
|
||||||
@ -961,7 +972,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.add_coverage(dataoffset + 4, 4)
|
self.add_coverage(dataoffset + 4, 4)
|
||||||
|
|
||||||
self.vprint(f"{prefix} Tag ID: {sprite_id}")
|
self.vprint(f"{prefix} Tag ID: {sprite_id}")
|
||||||
tags, frames, references = self.__parse_tags(ap2_version, afp_version, ap2data, subtags_offset, tag_frame, prefix=" " + prefix)
|
tags, frames, references = self.__parse_tags(ap2_version, afp_version, ap2data, subtags_offset, sprite_id, prefix=" " + prefix)
|
||||||
|
|
||||||
return AP2DefineSpriteTag(sprite_id, tags, frames, references)
|
return AP2DefineSpriteTag(sprite_id, tags, frames, references)
|
||||||
elif tagid == AP2Tag.AP2_DEFINE_FONT:
|
elif tagid == AP2Tag.AP2_DEFINE_FONT:
|
||||||
@ -997,7 +1008,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
return AP2DefineFontTag(font_id, fontname, xml_prefix, heights, text_indexes)
|
return AP2DefineFontTag(font_id, fontname, xml_prefix, heights, text_indexes)
|
||||||
elif tagid == AP2Tag.AP2_DO_ACTION:
|
elif tagid == AP2Tag.AP2_DO_ACTION:
|
||||||
datachunk = ap2data[dataoffset:(dataoffset + size)]
|
datachunk = ap2data[dataoffset:(dataoffset + size)]
|
||||||
bytecode = self.__parse_bytecode(f"on_enter_frame_{tag_frame}", datachunk, prefix=prefix)
|
bytecode = self.__parse_bytecode(f"on_enter_{f'sprite_{tag_parent_sprite}' if tag_parent_sprite is not None else 'main'}_frame_{tag_frame}", datachunk, prefix=prefix)
|
||||||
self.add_coverage(dataoffset, size)
|
self.add_coverage(dataoffset, size)
|
||||||
|
|
||||||
return AP2DoActionTag(bytecode)
|
return AP2DoActionTag(bytecode)
|
||||||
@ -1593,7 +1604,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
|
|
||||||
self.vprint(f"{prefix} Floats: {fv1} {fv2} {fv3} {fv4} {fv5} {fv6} {fv7} {fv8}")
|
self.vprint(f"{prefix} Floats: {fv1} {fv2} {fv3} {fv4} {fv5} {fv6} {fv7} {fv8}")
|
||||||
|
|
||||||
return AP2DefineMorphShape(define_shape_id)
|
return AP2DefineMorphShapeTag(define_shape_id)
|
||||||
elif tagid == AP2Tag.AP2_DEFINE_BUTTON:
|
elif tagid == AP2Tag.AP2_DEFINE_BUTTON:
|
||||||
flags, button_id, source_tags_count, bytecode_count = struct.unpack("<HHHH", ap2data[dataoffset:(dataoffset + 8)])
|
flags, button_id, source_tags_count, bytecode_count = struct.unpack("<HHHH", ap2data[dataoffset:(dataoffset + 8)])
|
||||||
self.add_coverage(dataoffset, 8)
|
self.add_coverage(dataoffset, 8)
|
||||||
@ -1757,12 +1768,20 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.vprint(f"{prefix} Offset: {hex(loc)}, Sound Unk1: {unk1}, Source Tag ID: {sound_source_tag}")
|
self.vprint(f"{prefix} Offset: {hex(loc)}, Sound Unk1: {unk1}, Source Tag ID: {sound_source_tag}")
|
||||||
raise Exception("TODO: Need to examine this section further if I find data with it!")
|
raise Exception("TODO: Need to examine this section further if I find data with it!")
|
||||||
|
|
||||||
return AP2DefineButton(button_id)
|
return AP2DefineButtonTag(button_id)
|
||||||
else:
|
else:
|
||||||
self.vprint(f"Unknown tag {hex(tagid)} with data {ap2data[dataoffset:(dataoffset + size)]!r}")
|
self.vprint(f"Unknown tag {hex(tagid)} with data {ap2data[dataoffset:(dataoffset + size)]!r}")
|
||||||
raise Exception(f"Unimplemented tag {hex(tagid)}!")
|
raise Exception(f"Unimplemented tag {hex(tagid)}!")
|
||||||
|
|
||||||
def __parse_tags(self, ap2_version: int, afp_version: int, ap2data: bytes, tags_base_offset: int, sprite_frame: int, prefix: str = "") -> Tuple[List[Tag], List[Frame], Dict[int, str]]:
|
def __parse_tags(
|
||||||
|
self,
|
||||||
|
ap2_version: int,
|
||||||
|
afp_version: int,
|
||||||
|
ap2data: bytes,
|
||||||
|
tags_base_offset: int,
|
||||||
|
sprite: Optional[int],
|
||||||
|
prefix: str = "",
|
||||||
|
) -> Tuple[List[Tag], List[Frame], Dict[str, int]]:
|
||||||
name_reference_flags, name_reference_count, frame_count, tags_count, name_reference_offset, frame_offset, tags_offset = struct.unpack(
|
name_reference_flags, name_reference_count, frame_count, tags_count, name_reference_offset, frame_offset, tags_offset = struct.unpack(
|
||||||
"<HHIIIII",
|
"<HHIIIII",
|
||||||
ap2data[tags_base_offset:(tags_base_offset + 24)]
|
ap2data[tags_base_offset:(tags_base_offset + 24)]
|
||||||
@ -1807,19 +1826,19 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
raise Exception(f"Invalid tag size {size} ({hex(size)})")
|
raise Exception(f"Invalid tag size {size} ({hex(size)})")
|
||||||
|
|
||||||
self.vprint(f"{prefix} Tag: {hex(tagid)} ({AP2Tag.tag_to_name(tagid)}), Size: {hex(size)}, Offset: {hex(tags_offset + 4)}")
|
self.vprint(f"{prefix} Tag: {hex(tagid)} ({AP2Tag.tag_to_name(tagid)}), Size: {hex(size)}, Offset: {hex(tags_offset + 4)}")
|
||||||
tags.append(self.__parse_tag(ap2_version, afp_version, ap2data, tagid, size, tags_offset + 4, tag_to_frame[i] + sprite_frame, prefix=prefix))
|
tags.append(self.__parse_tag(ap2_version, afp_version, ap2data, tagid, size, tags_offset + 4, sprite, tag_to_frame[i], prefix=prefix))
|
||||||
tags_offset += ((size + 3) & 0xFFFFFFFC) + 4 # Skip past tag header and data, rounding to the nearest 4 bytes.
|
tags_offset += ((size + 3) & 0xFFFFFFFC) + 4 # Skip past tag header and data, rounding to the nearest 4 bytes.
|
||||||
|
|
||||||
# Finally, parse place object name references.
|
# Finally, parse place object name references.
|
||||||
self.vprint(f"{prefix}Number of Object Name References: {name_reference_count}, Flags: {hex(name_reference_flags)}")
|
self.vprint(f"{prefix}Number of Object Name References: {name_reference_count}, Flags: {hex(name_reference_flags)}")
|
||||||
references: Dict[int, str] = {}
|
references: Dict[str, int] = {}
|
||||||
for i in range(name_reference_count):
|
for i in range(name_reference_count):
|
||||||
index, stringoffset = struct.unpack("<HH", ap2data[name_reference_offset:(name_reference_offset + 4)])
|
frameno, stringoffset = struct.unpack("<HH", ap2data[name_reference_offset:(name_reference_offset + 4)])
|
||||||
strval = self.__get_string(stringoffset)
|
strval = self.__get_string(stringoffset)
|
||||||
self.add_coverage(name_reference_offset, 4)
|
self.add_coverage(name_reference_offset, 4)
|
||||||
references[index] = strval
|
references[strval] = frameno
|
||||||
|
|
||||||
self.vprint(f"{prefix} Name Reference: {index}, Name: {strval}")
|
self.vprint(f"{prefix} Frame Number: {frameno}, Name: {strval}")
|
||||||
name_reference_offset += 4
|
name_reference_offset += 4
|
||||||
|
|
||||||
return tags, frames, references
|
return tags, frames, references
|
||||||
@ -2014,7 +2033,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
# Tag sections
|
# Tag sections
|
||||||
tags_offset = struct.unpack("<I", data[36:40])[0]
|
tags_offset = struct.unpack("<I", data[36:40])[0]
|
||||||
self.add_coverage(36, 4)
|
self.add_coverage(36, 4)
|
||||||
self.tags, self.frames, self.references = self.__parse_tags(ap2_data_version, version, data, tags_offset, 0)
|
self.tags, self.frames, self.references = self.__parse_tags(ap2_data_version, version, data, tags_offset, None)
|
||||||
|
|
||||||
# Imported tags sections
|
# Imported tags sections
|
||||||
imported_tags_count = struct.unpack("<h", data[34:36])[0]
|
imported_tags_count = struct.unpack("<h", data[34:36])[0]
|
||||||
|
@ -7,17 +7,67 @@ import os.path
|
|||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from PIL import Image, ImageDraw # type: ignore
|
from PIL import Image, ImageDraw # type: ignore
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from bemani.format.afp import TXP2File, Shape, SWF, AFPRenderer, Color
|
from bemani.format.afp import TXP2File, Shape, SWF, Frame, Tag, AP2DoActionTag, AP2PlaceObjectTag, AP2DefineSpriteTag, AFPRenderer, Color
|
||||||
from bemani.format import IFS
|
from bemani.format import IFS
|
||||||
|
|
||||||
|
|
||||||
|
def write_bytecode(swf: SWF, directory: str, verbose: bool=False) -> None:
|
||||||
|
# Actually place the files down.
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
# Buffer for where the decompiled data goes.
|
||||||
|
buff: List[str] = []
|
||||||
|
lut: Dict[str, int] = {}
|
||||||
|
|
||||||
|
def bytecode_from_frames(frames: List[Frame]) -> None:
|
||||||
|
for frame in frames:
|
||||||
|
for tag in frame.imported_tags:
|
||||||
|
if tag.init_bytecode:
|
||||||
|
buff.append(tag.init_bytecode.decompile(verbose=verbose))
|
||||||
|
|
||||||
|
def bytecode_from_tags(tags: List[Tag]) -> None:
|
||||||
|
for tag in tags:
|
||||||
|
if isinstance(tag, AP2DoActionTag):
|
||||||
|
buff.append(tag.bytecode.decompile(verbose=verbose))
|
||||||
|
elif isinstance(tag, AP2PlaceObjectTag):
|
||||||
|
for _, triggers in tag.triggers.items():
|
||||||
|
for trigger in triggers:
|
||||||
|
buff.append(trigger.decompile(verbose=verbose))
|
||||||
|
elif isinstance(tag, AP2DefineSpriteTag):
|
||||||
|
lut.update(tag.references)
|
||||||
|
bytecode_from_frames(tag.frames)
|
||||||
|
bytecode_from_tags(tag.tags)
|
||||||
|
|
||||||
|
lut.update(swf.references)
|
||||||
|
bytecode_from_frames(swf.frames)
|
||||||
|
bytecode_from_tags(swf.tags)
|
||||||
|
|
||||||
|
# If we have references, put them at the top as global defines.
|
||||||
|
if lut:
|
||||||
|
buff = [
|
||||||
|
os.linesep.join([
|
||||||
|
'// Defined string references from SWF container, as used for frame lookups.',
|
||||||
|
'FRAME_LUT = {',
|
||||||
|
*[f" {name!r}: {frame}," for name, frame in lut.items()],
|
||||||
|
'};',
|
||||||
|
]),
|
||||||
|
*buff,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Now, write it out.
|
||||||
|
filename = os.path.join(directory, swf.exported_name) + ".code"
|
||||||
|
print(f"Writing code to {filename}...")
|
||||||
|
with open(filename, "wb") as bfp:
|
||||||
|
bfp.write(f"{os.linesep}{os.linesep}".join(buff).encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Konami AFP graphic file unpacker/repacker")
|
parser = argparse.ArgumentParser(description="Konami AFP graphic file unpacker/repacker")
|
||||||
subparsers = parser.add_subparsers(help='Action to take', dest='action')
|
subparsers = parser.add_subparsers(help='Action to take', dest='action')
|
||||||
|
|
||||||
extract_parser = subparsers.add_parser('extract', help='Extract relevant textures from TXP2 container')
|
extract_parser = subparsers.add_parser('extract', help='Extract relevant file data and textures from a TXP2 container')
|
||||||
extract_parser.add_argument(
|
extract_parser.add_argument(
|
||||||
"file",
|
"file",
|
||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
@ -70,6 +120,12 @@ def main() -> int:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Write binary SWF files to disk",
|
help="Write binary SWF files to disk",
|
||||||
)
|
)
|
||||||
|
extract_parser.add_argument(
|
||||||
|
"-y",
|
||||||
|
"--write-bytecode",
|
||||||
|
action="store_true",
|
||||||
|
help="Write decompiled bytecode files to disk",
|
||||||
|
)
|
||||||
|
|
||||||
update_parser = subparsers.add_parser('update', help='Update relevant textures in a TXP2 container from a directory')
|
update_parser = subparsers.add_parser('update', help='Update relevant textures in a TXP2 container from a directory')
|
||||||
update_parser.add_argument(
|
update_parser.add_argument(
|
||||||
@ -138,6 +194,32 @@ def main() -> int:
|
|||||||
help="Display verbuse debugging output",
|
help="Display verbuse debugging output",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
decompile_parser = subparsers.add_parser('decompile', help='Decompile bytecode in a raw AFP/BSI file pair previously extracted from an IFS or TXP2 container')
|
||||||
|
decompile_parser.add_argument(
|
||||||
|
"afp",
|
||||||
|
metavar="AFPFILE",
|
||||||
|
help="The AFP file to parse",
|
||||||
|
)
|
||||||
|
decompile_parser.add_argument(
|
||||||
|
"bsi",
|
||||||
|
metavar="BSIFILE",
|
||||||
|
help="The BSI file to parse",
|
||||||
|
)
|
||||||
|
decompile_parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--directory",
|
||||||
|
metavar="DIR",
|
||||||
|
default='.',
|
||||||
|
type=str,
|
||||||
|
help="Directory to extract to after decompiling. Defaults to current directory.",
|
||||||
|
)
|
||||||
|
decompile_parser.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Display verbuse debugging output",
|
||||||
|
)
|
||||||
|
|
||||||
parsegeo_parser = subparsers.add_parser('parsegeo', help='Parse a raw GEO file previously extracted from an IFS or TXP2 container')
|
parsegeo_parser = subparsers.add_parser('parsegeo', help='Parse a raw GEO file previously extracted from an IFS or TXP2 container')
|
||||||
parsegeo_parser.add_argument(
|
parsegeo_parser.add_argument(
|
||||||
"geo",
|
"geo",
|
||||||
@ -390,6 +472,9 @@ def main() -> int:
|
|||||||
if not announced.get(texturename, False):
|
if not announced.get(texturename, False):
|
||||||
print(f"Cannot extract sprites from {texturename} because it is not a supported format!")
|
print(f"Cannot extract sprites from {texturename} because it is not a supported format!")
|
||||||
announced[texturename] = True
|
announced[texturename] = True
|
||||||
|
if args.write_bytecode:
|
||||||
|
for swf in afpfile.swfdata:
|
||||||
|
write_bytecode(swf, args.dir, verbose=args.verbose)
|
||||||
|
|
||||||
if args.action == "update":
|
if args.action == "update":
|
||||||
# First, parse the file out
|
# First, parse the file out
|
||||||
@ -451,6 +536,16 @@ def main() -> int:
|
|||||||
swf.parse(verbose=args.verbose)
|
swf.parse(verbose=args.verbose)
|
||||||
print(json.dumps(swf.as_dict(decompile_bytecode=args.decompile_bytecode, verbose=args.verbose), sort_keys=True, indent=4))
|
print(json.dumps(swf.as_dict(decompile_bytecode=args.decompile_bytecode, verbose=args.verbose), sort_keys=True, indent=4))
|
||||||
|
|
||||||
|
if args.action == "decompile":
|
||||||
|
# First, load the AFP and BSI files
|
||||||
|
with open(args.afp, "rb") as bafp:
|
||||||
|
with open(args.bsi, "rb") as bbsi:
|
||||||
|
swf = SWF("<unnamed>", bafp.read(), bbsi.read())
|
||||||
|
|
||||||
|
# Now, decompile it
|
||||||
|
swf.parse(verbose=args.verbose)
|
||||||
|
write_bytecode(swf, args.directory, verbose=args.verbose)
|
||||||
|
|
||||||
if args.action == "parsegeo":
|
if args.action == "parsegeo":
|
||||||
# First, load the AFP and BSI files
|
# First, load the AFP and BSI files
|
||||||
with open(args.geo, "rb") as bfp:
|
with open(args.geo, "rb") as bfp:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user