Compare commits

..

9 commits
iss10 ... trunk

Author SHA1 Message Date
fa819503e1 Merge pull request 'Minor fixes to testing whether a demo exists.' (#14) from iss13 into trunk
Reviewed-on: #14
2026-01-19 18:09:32 +00:00
116c21456f Minor fixes to testing whether a demo exists.
Add a unit test to validate functionality of this tricky corner of code.
2026-01-19 13:05:29 -05:00
2809f00191 Minor tweak to license specification. 2026-01-19 13:03:52 -05:00
54887efd02 Minor fix for loading map names from config. 2026-01-19 13:02:30 -05:00
f3db413a3f Merge pull request 'Rename dcc to doomcc' (#12) from iss1 into trunk
Reviewed-on: #12
2025-12-25 06:23:09 +00:00
6c17ae717b Rename references to dcc to doomcc. 2025-12-24 23:01:25 -05:00
5b2528831d Rename the dcc directory to doomcc.
Don't try and edit the files in the same change; that introduces a lot
of headaches.
2025-12-24 22:55:42 -05:00
07d079f204 Blacken the code base. 2025-12-24 18:35:43 -05:00
d585c1529d Merge pull request 'Use os.execv instead of subprocess.run when no further action is taken.' (#11) from iss10 into trunk
Reviewed-on: #11
2025-12-24 23:20:33 +00:00
27 changed files with 301 additions and 301 deletions

View file

@ -1,2 +1,2 @@
install:
cp dist/pyinstaller/manylinux_2_39_x86_64/dcc /home/tynan/.local/bin/dcc
cp dist/pyinstaller/manylinux_2_39_x86_64/doomcc /home/tynan/.local/bin/dcc

View file

@ -1,5 +0,0 @@
import sys
from dcc.main import main
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View file

@ -1,72 +0,0 @@
import sys
from cliff.app import App
from cliff.commandmanager import CommandManager
import dcc.concat
import dcc.configure
import dcc.dsda
import dcc.eureka
import dcc.extract
import dcc.fabricate
import dcc.fetch
import dcc.ls
import dcc.pb
import dcc.play
import dcc.put
import dcc.record
import dcc.rib
import dcc.ss
import dcc.text
import dcc.thumb
class DCC(App):
def __init__(self):
cm = CommandManager(None)
commands = {
"concat": dcc.concat.Concat,
"configure": dcc.configure.Configure,
"dsda": dcc.dsda.DSDA,
"eureka": dcc.eureka.Eureka,
"extract": dcc.extract.Extract,
"fabricate": dcc.fabricate.Fabricate,
"fetch": dcc.fetch.Fetch,
"ls": dcc.ls.List,
"ls demos": dcc.ls.ListDemos,
"ls videos": dcc.ls.ListVideos,
"pb": dcc.pb.PB,
"play": dcc.play.Play,
"put": dcc.put.Put,
"record": dcc.record.Record,
"rib": dcc.rib.RIB,
"ss": dcc.ss.SS,
"text": dcc.text.Text,
"thumb": dcc.thumb.Thumb,
}
for n, c in commands.items():
cm.add_command(n, c)
super().__init__(
description="Doom Command Center",
version="0.0.1",
command_manager=cm,
deferred_help=True,
)
def initialize_app(self, argv):
pass
def prepare_to_run_command(self, cmd):
pass
def clean_up(self, cmd, result, err):
pass
def main(argv=sys.argv[1:]):
dcc = DCC()
return dcc.run(argv)
if __name__ == '__main__':
sys.exit(main())

5
doomcc/__main__.py Normal file
View file

@ -0,0 +1,5 @@
import sys
from doomcc.main import main
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View file

@ -1,6 +1,6 @@
import av
import copy
import dcc.doom_base
import doomcc.doom_base
import enum
import fractions
import io
@ -16,7 +16,7 @@ class State(enum.Enum):
DONE = 3
class Concat(dcc.doom_base.Wad):
class Concat(doomcc.doom_base.Wad):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("start_map")
@ -34,12 +34,10 @@ class Concat(dcc.doom_base.Wad):
+ f"to{parsed_args.end_map}"
)
output = av.open(
self.fabricate.joinpath(parsed_args.wad).joinpath(
f"{fn_base}.mp4"), "w"
self.fabricate.joinpath(parsed_args.wad).joinpath(f"{fn_base}.mp4"), "w"
)
summary_file = open(
self.fabricate.joinpath(parsed_args.wad).joinpath(
f"{fn_base}.txt"), "w"
self.fabricate.joinpath(parsed_args.wad).joinpath(f"{fn_base}.txt"), "w"
)
self._offset = 0
@ -66,11 +64,13 @@ class Concat(dcc.doom_base.Wad):
start_time = self._offset / 1000000
text = self._add_chunk(
videodir.joinpath(f"{parsed_args.wad}_map{idx}.mp4"),
output, not parsed_args.nooverlay
output,
not parsed_args.nooverlay,
)
list.append(
summary, f"{text} {math.floor(start_time / 60):02}:"
+ f"{math.floor(start_time % 60):02}"
summary,
f"{text} {math.floor(start_time / 60):02}:"
+ f"{math.floor(start_time % 60):02}",
)
if state == State.DONE:
break
@ -82,8 +82,7 @@ class Concat(dcc.doom_base.Wad):
def _add_chunk(self, v, output, overlay):
chunk = av.open(v)
if not (len(chunk.streams.video) == 1
and len(chunk.streams.audio) == 1):
if not (len(chunk.streams.video) == 1 and len(chunk.streams.audio) == 1):
raise Exception(
f"irregular chunk {v}: streams {chunk.streams} "
+ f"(expected 1 video & 1 audio)"
@ -96,16 +95,11 @@ class Concat(dcc.doom_base.Wad):
text = ""
if overlay:
img = wand.image.Image(
height=chunk.streams[0].height,
width=chunk.streams[0].width
height=chunk.streams[0].height, width=chunk.streams[0].width
)
mapstring = v.name[-6:-4]
text = self._config["map_names"][f"map{mapstring}"]
self.draw_text(
img,
f"MAP{mapstring}: {text}",
font_size=120
)
self.draw_text(img, f"MAP{mapstring}: {text}", font_size=120)
img.trim(reset_coords=True)
img.border("graya(25%, 25%)", 10, 10)
img.border(self.thumbnail_text_stroke, 16, 16)
@ -113,17 +107,14 @@ class Concat(dcc.doom_base.Wad):
# multiple of 8. dude whyyyyyyy
padfactor = 8
img.border("transparent", padfactor, 0)
img.crop(
width=img.width - img.width % padfactor,
height=img.height
)
img.crop(width=img.width - img.width % padfactor, height=img.height)
if len(output.streams.get()) == 0:
# We can't use the input stream as a template here; it doesn't
# have everything needed to do encoding and will fail
# mysteriously later.
vs = chunk.streams.video[0]
vr = int(vs.time_base.denominator/vs.time_base.numerator)
vr = int(vs.time_base.denominator / vs.time_base.numerator)
ovs = output.add_stream("h264", rate=vr)
ovs.extradata = copy.deepcopy(vs.extradata)
ovs.height = vs.height
@ -145,12 +136,10 @@ class Concat(dcc.doom_base.Wad):
oas.bit_rate = astr.bit_rate
src = ograph.add_buffer(
template=chunk.streams.video[0],
time_base=chunk.streams.video[0].time_base
template=chunk.streams.video[0], time_base=chunk.streams.video[0].time_base
)
asrc = ograph.add_abuffer(
template=chunk.streams.audio[0],
time_base=chunk.streams.audio[0].time_base
template=chunk.streams.audio[0], time_base=chunk.streams.audio[0].time_base
)
# TODO: video fades are absolute relative to the input video; audio
# fades need to have their timestamps offset by the position in the
@ -163,19 +152,18 @@ class Concat(dcc.doom_base.Wad):
iafade_start = self._offset * sample_rate / 1000000
iafade = ograph.add("afade", args=f"in:{iafade_start}:{sample_rate}")
oafade_start = (
(self._offset + chunk.duration) * sample_rate / 1000000
- sample_rate
)
self._offset + chunk.duration
) * sample_rate / 1000000 - sample_rate
oafade = ograph.add("afade", args=f"out:{oafade_start}:{sample_rate}")
if overlay:
overlay = ograph.add_buffer(
width=img.width, height=img.height,
format="rgba", time_base=chunk.streams.video[0].time_base
)
overlay_fo = ograph.add(
"fade", args=f"out:{4 * frame_rate}:{frame_rate}"
width=img.width,
height=img.height,
format="rgba",
time_base=chunk.streams.video[0].time_base,
)
overlay_fo = ograph.add("fade", args=f"out:{4 * frame_rate}:{frame_rate}")
overlay.link_to(overlay_fo, 0, 0)
composite = ograph.add("overlay", args="x=4:y=4")
src.link_to(composite, 0, 0)
@ -194,9 +182,8 @@ class Concat(dcc.doom_base.Wad):
for packet in chunk.demux():
if packet.dts is None:
continue
pof = (
(self._offset * packet.time_base.denominator)
/ (packet.time_base.numerator * 1000000)
pof = (self._offset * packet.time_base.denominator) / (
packet.time_base.numerator * 1000000
)
packet.dts += pof
packet.pts += pof
@ -221,9 +208,7 @@ class Concat(dcc.doom_base.Wad):
def _make_text_frame(self, img, ifr):
# We need to give each frame its own memory it can own.
text_frame = av.video.frame.VideoFrame(
img.width, img.height, format="rgba"
)
text_frame = av.video.frame.VideoFrame(img.width, img.height, format="rgba")
text_frame.planes[0].update(img.make_blob(format="rgba"))
text_frame.pts = ifr.pts
text_frame.dts = ifr.dts

View file

@ -12,13 +12,15 @@ class ConfigBase(object):
self._doom = pathlib.Path(parsed_args.doom)
self._config_name = parsed_args.config_name
self._config = tomlkit.toml_file.TOMLFile(
self.doom.joinpath(self.config_name)).read()
self.doom.joinpath(self.config_name)
).read()
self._dsda = self._config.get("dsda")
if self._dsda is None:
raise Exception(
"required key 'dsda' not set in config "
+ f"{self.doom.joinpath(self.config_name)}.")
+ f"{self.doom.joinpath(self.config_name)}."
)
for d in ("iwads", "pwads", "demos", "fabricate"):
self._init_attr([d], d, fn=self.doom.joinpath)
@ -29,10 +31,7 @@ class ConfigBase(object):
self._init_attr(["thumbnail", "text_fill"], "white")
self._init_attr(["thumbnail", "text_stroke"], "red")
self._init_attr(["thumbnail", "overlay_name"], "M_DOOM_scaled.png")
self._init_attr(
["fetch", "mirror"],
"https://youfailit.net/pub/idgames"
)
self._init_attr(["fetch", "mirror"], "https://youfailit.net/pub/idgames")
def _init_attr(self, what, default, fn=lambda x: x):
propname = "_".join(what)
@ -45,8 +44,7 @@ class ConfigBase(object):
setattr(self, f"_{propname}", fn(val))
setattr(
type(self), propname,
property(lambda self: getattr(self, f"_{propname}"))
type(self), propname, property(lambda self: getattr(self, f"_{propname}"))
)
@property
@ -65,10 +63,10 @@ class ConfigBase(object):
def get_parser_func(toc):
def add_common_args(self, prog_name):
parser = super(toc, self).get_parser(prog_name)
parser.add_argument(
"--doom", default=pathlib.Path.home().joinpath("doom"))
parser.add_argument("--doom", default=pathlib.Path.home().joinpath("doom"))
parser.add_argument("--config-name", default="config.toml")
return parser
return add_common_args

View file

@ -1,9 +1,9 @@
import dcc.doom_base
import doomcc.doom_base
import omg
import tomlkit.toml_file
class Configure(dcc.doom_base.Wad):
class Configure(doomcc.doom_base.Wad):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("--complevel", "--cl")
@ -22,12 +22,10 @@ class Configure(dcc.doom_base.Wad):
wad = omg.WadIO(w)
complevel = wad.read("COMPLVL").decode("ascii").strip()
except Exception as e:
print(
f"Wad {w} likely has no lump COMPLVL (exception {e})")
print(f"Wad {w} likely has no lump COMPLVL (exception {e})")
if complevel is None:
complevel = input("Complevel? ")
doc = tomlkit.document()
doc.add("complevel", complevel)
if parsed_args.iwad is not None:

View file

@ -1,13 +1,13 @@
from abc import abstractmethod
from cliff.command import Command
import dcc.config
import doomcc.config
import io
import os
import re
import tomlkit
class Wad(dcc.config.Base):
class Wad(doomcc.config.Base):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("wad")
@ -70,9 +70,8 @@ class Wad(dcc.config.Base):
return []
def thumb_overlay_path(self):
return (
self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self.thumbnail_overlay_name)
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(
self.thumbnail_overlay_name
)
@ -129,55 +128,51 @@ class WadMap(Wad):
def demo_in_path(self):
candidates = [
x for x in self.demos.joinpath(self.wad)
.glob(self._file_base("*.lmp"))
x for x in self.demos.joinpath(self.wad).glob(self._file_base("*.lmp"))
]
if len(candidates) == 0:
raise Exception(
"no suitable demo candidates for WAD {} MAP {} name {}."
.format(self.wad, self.map, self.name_string)
"no suitable demo candidates for WAD {} MAP {}{}.".format(
self.wad,
self.map,
f" name {self._name}" if self._name else "",
)
)
if len(candidates) == 1:
return candidates[0]
return sorted(filter(lambda s: re.search("-", str(s)), candidates))[-1]
def dsda_text_path(self):
return (
self._ensure(self.demos.joinpath(self.wad))
.joinpath(self._file_base(".txt"))
return self._ensure(self.demos.joinpath(self.wad)).joinpath(
self._file_base(".txt")
)
def video_path(self):
return (
self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base(".mp4"))
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(
self._file_base(".mp4")
)
def demo_out_path(self):
return (
self._ensure(self.demos.joinpath(self.wad))
.joinpath(self._file_base(".lmp"))
return self._ensure(self.demos.joinpath(self.wad)).joinpath(
self._file_base(".lmp")
)
def target_bucket(self):
return "doom/" + self._file_base(".lmp")
def base_thumb_path(self):
return (
self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base("_base.png"))
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(
self._file_base("_base.png")
)
def text_thumb_path(self):
return (
self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base("_text.png"))
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(
self._file_base("_text.png")
)
def thumb_path(self):
return (
self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base("_thumb.png"))
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(
self._file_base("_thumb.png")
)
def _file_base(self, ext):

View file

@ -1,5 +1,5 @@
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import os
import re
import shutil
@ -7,7 +7,7 @@ import subprocess
import zipfile
class DSDA(dcc.doom_base.WadMap):
class DSDA(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("-s", "--single", action="store_true")
@ -22,10 +22,9 @@ class DSDA(dcc.doom_base.WadMap):
if shutil.which("xvfb-run") is not None:
command = ["xvfb-run"] + command
subprocess.run(
command + self.dsda_preamble(warp=False) + [
"-fastdemo", dip, "-nosound",
"-skiptic", "-1", "-export_text_file"
]
command
+ self.dsda_preamble(warp=False)
+ ["-fastdemo", dip, "-nosound", "-skiptic", "-1", "-export_text_file"]
)
editor = "nano"
if "EDITOR" in os.environ:
@ -38,7 +37,7 @@ class DSDA(dcc.doom_base.WadMap):
else:
fh1 = self.wad[0:2] + self.map
if parsed_args.single:
fh1 = self.wad[0:min(len(self.wad), 4)]
fh1 = self.wad[0 : min(len(self.wad), 4)]
fh2 = ""
with open(dtp, mode="r") as f:
for line in f:
@ -46,7 +45,7 @@ class DSDA(dcc.doom_base.WadMap):
m = re.search("[^0-9]*([0-9]*):([0-9]*).[0-9]*", line)
if m is None:
continue
fh2 = m[1]+m[2]
fh2 = m[1] + m[2]
if len(fh2) % 2 == 1:
fh2 = "0" + fh2
break

View file

@ -1,9 +1,9 @@
import dcc.doom_base
import dcc.config
import doomcc.doom_base
import doomcc.config
import os
class Eureka(dcc.doom_base.WadMap):
class Eureka(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("--main")
@ -24,7 +24,11 @@ class Eureka(dcc.doom_base.WadMap):
if complevel == "11" or complevel == "21":
port = "mbf"
os.execvp("eureka",
["eureka"] + ["-iwad", iwad] + ["-w", parsed_args.map]
+ ["-p", port] + [mw]
os.execvp(
"eureka",
["eureka"]
+ ["-iwad", iwad]
+ ["-w", parsed_args.map]
+ ["-p", port]
+ [mw],
)

View file

@ -1,14 +1,14 @@
import dcc.doom_base
import doomcc.doom_base
import omg
import numpy as np
import wand.color
import wand.image
class Extract(dcc.doom_base.Wad):
class Extract(doomcc.doom_base.Wad):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument('lump')
parser.add_argument("lump")
return parser
def take_action(self, parsed_args):
@ -24,8 +24,9 @@ class Extract(dcc.doom_base.Wad):
) as img:
img.transparent_color(wand.color.Color("#ff00ff"), 0.0)
img.save(
filename=self.fabricate.joinpath(parsed_args.wad)
.joinpath(parsed_args.lump + ".png")
filename=self.fabricate.joinpath(parsed_args.wad).joinpath(
parsed_args.lump + ".png"
)
)
return
except Exception as e:
@ -35,6 +36,5 @@ class Extract(dcc.doom_base.Wad):
)
print(
f"Lump {parsed_args.lump} not found in any wad in "
+ f"{parsed_args.wad}"
f"Lump {parsed_args.lump} not found in any wad in " + f"{parsed_args.wad}"
)

View file

@ -1,12 +1,12 @@
import contextlib
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import os
import shutil
import tempfile
class Fabricate(dcc.doom_base.WadMap):
class Fabricate(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("--fg", action="store_true")
@ -18,10 +18,12 @@ class Fabricate(dcc.doom_base.WadMap):
command = [self.dsda]
if not parsed_args.fg and shutil.which("xvfb-run") is not None:
command = ["xvfb-run"] + command
os.execvp(command[0],
command + self.dsda_preamble()
os.execvp(
command[0],
command
+ self.dsda_preamble()
+ ["-timedemo", self.demo_in_path()]
+ ["-viddump", self.video_path()]
+ ["-viddump", self.video_path()],
)
def options_dict(self):

View file

@ -1,4 +1,4 @@
import dcc.config
import doomcc.config
import io
import json
import pathlib
@ -9,7 +9,7 @@ import urllib.request
import zipfile
class Fetch(dcc.config.Base):
class Fetch(doomcc.config.Base):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("id_or_name")
@ -21,14 +21,12 @@ class Fetch(dcc.config.Base):
idgames_id = self.search_idgames(parsed_args.id_or_name)
reply = self.fetch_url(
"https://www.doomworld.com/idgames/api/" +
"api.php?action=get&id={}&out=json".format(idgames_id)
"https://www.doomworld.com/idgames/api/"
+ "api.php?action=get&id={}&out=json".format(idgames_id)
)
rpath = "/".join(
[self.fetch_mirror, reply["content"]["dir"], reply["content"]["filename"]]
)
rpath = "/".join([
self.fetch_mirror,
reply["content"]["dir"],
reply["content"]["filename"]
])
wad = reply["content"]["filename"][0:-4]
with urllib.request.urlopen(rpath) as response:
@ -38,8 +36,8 @@ class Fetch(dcc.config.Base):
# TODO: explicit error handling. Let users choose when >1 result.
def search_idgames(self, wad):
reply = self.fetch_url(
"https://www.doomworld.com/idgames/api/" +
"api.php?action=search&query={}&out=json".format(wad)
"https://www.doomworld.com/idgames/api/"
+ "api.php?action=search&query={}&out=json".format(wad)
)
if "content" not in reply:
sys.exit(f"No WAD named {wad} found on idgames.")
@ -62,8 +60,5 @@ class Fetch(dcc.config.Base):
if fetcher_path is None:
raise Exception(f"Fetch util {fetcher} not found on PATH.")
proc = subprocess.run(
[fetcher_path, url],
capture_output=True, check=True
)
proc = subprocess.run([fetcher_path, url], capture_output=True, check=True)
return json.loads(proc.stdout)

View file

@ -1,8 +1,8 @@
import dcc.config
import doomcc.config
import os
class List(dcc.config.ListerBase):
class List(doomcc.config.ListerBase):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("target")
@ -12,22 +12,19 @@ class List(dcc.config.ListerBase):
match parsed_args.target:
case "pwads":
return (
("pwads",), sorted(
(x.name,) for x in os.scandir(self.pwads) if x.is_dir()
)
("pwads",),
sorted((x.name,) for x in os.scandir(self.pwads) if x.is_dir()),
)
case "iwads":
return (
("iwads",), sorted(
(x.name,) for x in
os.scandir(self.iwads) if x.is_file()
)
("iwads",),
sorted((x.name,) for x in os.scandir(self.iwads) if x.is_file()),
)
case _:
raise Exception(f"unknown target {parsed_args.target}")
class WadList(dcc.config.ListerBase):
class WadList(doomcc.config.ListerBase):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("wad")
@ -35,11 +32,12 @@ class WadList(dcc.config.ListerBase):
def _get_results(self, name, base, wad, ext):
return (
(name,), sorted(
(x.name,) for x in
os.scandir(base.joinpath(wad))
(name,),
sorted(
(x.name,)
for x in os.scandir(base.joinpath(wad))
if x.name.endswith(ext)
)
),
)
@ -50,6 +48,4 @@ class ListDemos(WadList):
class ListVideos(WadList):
def take_action(self, parsed_args):
return self._get_results(
"videos", self.fabricate, parsed_args.wad, ".mp4"
)
return self._get_results("videos", self.fabricate, parsed_args.wad, ".mp4")

72
doomcc/main.py Normal file
View file

@ -0,0 +1,72 @@
import sys
from cliff.app import App
from cliff.commandmanager import CommandManager
import doomcc.concat
import doomcc.configure
import doomcc.dsda
import doomcc.eureka
import doomcc.extract
import doomcc.fabricate
import doomcc.fetch
import doomcc.ls
import doomcc.pb
import doomcc.play
import doomcc.put
import doomcc.record
import doomcc.rib
import doomcc.ss
import doomcc.text
import doomcc.thumb
class DoomCC(App):
def __init__(self):
cm = CommandManager(None)
commands = {
"concat": doomcc.concat.Concat,
"configure": doomcc.configure.Configure,
"dsda": doomcc.dsda.DSDA,
"eureka": doomcc.eureka.Eureka,
"extract": doomcc.extract.Extract,
"fabricate": doomcc.fabricate.Fabricate,
"fetch": doomcc.fetch.Fetch,
"ls": doomcc.ls.List,
"ls demos": doomcc.ls.ListDemos,
"ls videos": doomcc.ls.ListVideos,
"pb": doomcc.pb.PB,
"play": doomcc.play.Play,
"put": doomcc.put.Put,
"record": doomcc.record.Record,
"rib": doomcc.rib.RIB,
"ss": doomcc.ss.SS,
"text": doomcc.text.Text,
"thumb": doomcc.thumb.Thumb,
}
for n, c in commands.items():
cm.add_command(n, c)
super().__init__(
description="Doom Command Center",
version="0.0.1",
command_manager=cm,
deferred_help=True,
)
def initialize_app(self, argv):
pass
def prepare_to_run_command(self, cmd):
pass
def clean_up(self, cmd, result, err):
pass
def main(argv=sys.argv[1:]):
doomcc = DoomCC()
return doomcc.run(argv)
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,8 +1,8 @@
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import os
class PB(dcc.doom_base.WadMap):
class PB(doomcc.doom_base.WadMap):
def take_action(self, parsed_args):
os.execvp("ffplay", ["ffplay", self.video_path()])

View file

@ -1,8 +1,8 @@
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import os
class Play(dcc.doom_base.WadMap):
class Play(doomcc.doom_base.WadMap):
def take_action(self, parsed_args):
os.execv(self.dsda, [self.dsda] + self.dsda_preamble())

View file

@ -1,23 +1,22 @@
import boto3
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
class Put(dcc.doom_base.WadMap):
class Put(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
return parser
# TODO: accept configuration for bucket name
def take_action(self, parsed_args):
s3_client = boto3.client('s3')
s3_client = boto3.client("s3")
demo = self.demo_in_path()
bucket = self.target_bucket()
print("Uploading {} to bucket {}.".format(demo, bucket))
s3_client.upload_file(
demo, 'yrriban', bucket,
ExtraArgs={
'ContentType': 'binary/octet-stream',
'ACL': 'public-read'
}
demo,
"yrriban",
bucket,
ExtraArgs={"ContentType": "binary/octet-stream", "ACL": "public-read"},
)

View file

@ -1,13 +1,13 @@
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import os
class Record(dcc.doom_base.WadMap):
class Record(doomcc.doom_base.WadMap):
def take_action(self, parsed_args):
os.execv(self.dsda,
[self.dsda] + self.dsda_preamble() +
["-record", self.demo_out_path()]
os.execv(
self.dsda,
[self.dsda] + self.dsda_preamble() + ["-record", self.demo_out_path()],
)
def options_dict(self):

View file

@ -1,10 +1,10 @@
import dcc.doom_base
import doomcc.doom_base
import pathlib
import os
import time
class RIB(dcc.doom_base.WadMap):
class RIB(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("-s", "--secs_before", type=int, default=10)
@ -16,10 +16,10 @@ class RIB(dcc.doom_base.WadMap):
demo = ""
dt = 0
demodir = (
pathlib.Path.home() /
".dsda-doom" /
"dsda_doom_data" /
self.iwad_path.stem.lower()
pathlib.Path.home()
/ ".dsda-doom"
/ "dsda_doom_data"
/ self.iwad_path.stem.lower()
)
for w in self.load_order():
demodir = demodir.joinpath(w.stem.lower())
@ -41,8 +41,10 @@ class RIB(dcc.doom_base.WadMap):
+ f"and map {parsed_args.map} (tried to look in {demodir})"
)
os.execv(self.dsda,
[self.dsda] + self.dsda_preamble(warp=False)
os.execv(
self.dsda,
[self.dsda]
+ self.dsda_preamble(warp=False)
+ ["-playdemo", demo]
+ ["-skiptic", str(-35 * parsed_args.secs_before)]
+ ["-skiptic", str(-35 * parsed_args.secs_before)],
)

View file

@ -1,12 +1,12 @@
from tkinter import messagebox
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import sys
import wand.display
import wand.image
class SS(dcc.doom_base.WadMap):
class SS(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("-g", "--gravity", default="center")
@ -22,11 +22,11 @@ class SS(dcc.doom_base.WadMap):
height = self.thumbnail_height
with wand.image.Image(width=width, height=height, pseudo="x:") as img:
img.reset_coords()
if (img.width < width or img.height < height):
if img.width < width or img.height < height:
if not messagebox.askretrycancel(
title="DCC",
message=f"Image too small ({img.width}x{img.height})." +
"Try again?"
title="Doom Command Center",
message=f"Image too small ({img.width}x{img.height})."
+ "Try again?",
):
sys.exit("Gave up trying to select an image.")
return False
@ -35,7 +35,7 @@ class SS(dcc.doom_base.WadMap):
if not yolo:
wand.display.display(img)
accepted = messagebox.askyesnocancel(
title="DCC", message="Is this image acceptable?"
title="Doom Command Center", message="Is this image acceptable?"
)
if accepted is None:
sys.exit("Gave up on image verification")

View file

@ -1,5 +1,5 @@
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import sys
import textwrap
import wand.drawing
@ -30,9 +30,7 @@ def draw_text(self, img, text, font_size=64, wrap_dist=0.75):
columns = len(wrapped_text)
while columns > 0:
columns -= 1
wrapped_text = '\n'.join(
textwrap.wrap(wrapped_text, columns)
)
wrapped_text = "\n".join(textwrap.wrap(wrapped_text, columns))
wrapped_width, _ = eval_metrics(wrapped_text)
if wrapped_width <= target_width:
break
@ -43,20 +41,20 @@ def draw_text(self, img, text, font_size=64, wrap_dist=0.75):
)
textlines[idx] = wrapped_text
wrapped_text = '\n'.join(textlines)
wrapped_text = "\n".join(textlines)
draw.text(5, int(draw.font_size) + 5, wrapped_text)
draw(img)
draw.stroke_color = wand.color.Color("none")
draw.stroke_width = 0
draw.text(5, int(draw.font_size)+5, wrapped_text)
draw.text(5, int(draw.font_size) + 5, wrapped_text)
draw(img)
dcc.config.Base.draw_text = draw_text
doomcc.config.Base.draw_text = draw_text
class Text(dcc.doom_base.WadMap):
class Text(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("--nomap", action="store_true")
@ -74,12 +72,9 @@ class Text(dcc.doom_base.WadMap):
text += "\n"
text = "{}{}".format(text, parsed_args.demotype)
with wand.image.Image(
height=self.thumbnail_height,
width=self.thumbnail_width
height=self.thumbnail_height, width=self.thumbnail_width
) as img:
self.draw_text(
img, text, wrap_dist=0.95
)
self.draw_text(img, text, wrap_dist=0.95)
img.trim()
img.reset_coords()
img.save(filename=self.text_thumb_path())
@ -91,7 +86,7 @@ class Text(dcc.doom_base.WadMap):
map_names = self._config.get("map_names")
if map_names is not None:
text = map_names.get(f"map{mapnum}")
if text != "":
if text is not None:
return text
return input("Map Name? ")

View file

@ -1,10 +1,10 @@
import dcc.config
import dcc.doom_base
import doomcc.config
import doomcc.doom_base
import wand.color
import wand.image
class Thumb(dcc.doom_base.WadMap):
class Thumb(doomcc.doom_base.WadMap):
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument("--index", action="store_true")
@ -17,7 +17,7 @@ class Thumb(dcc.doom_base.WadMap):
overlay = self.thumb_overlay_path()
with (
wand.image.Image(filename=base) as bi,
wand.color.Color("transparent") as tc
wand.color.Color("transparent") as tc,
):
with wand.image.Image(filename=text) as ti:
ti.border(tc, 5, 5)

View file

@ -1,11 +1,11 @@
[project]
name = "dcc"
name = "doomcc"
version = "0.1.0"
description = "Doom Command Center"
authors = [
{name = "yrriban",email = "yrriban@gmail.com"}
]
license = {text = "MIT"}
license = "MIT"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
@ -27,10 +27,10 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[project.scripts]
dcc = "dcc.main:main"
doomcc = "doomcc.main:main"
[tool.poetry-pyinstaller-plugin.scripts]
dcc = { source = "dcc/main.py", type = "onefile" }
doomcc = { source = "doomcc/main.py", type = "onefile" }
[tool.poetry-pyinstaller-plugin.collect]
all = ["cliff"]

28
tests/doom_base_test.py Normal file
View file

@ -0,0 +1,28 @@
import doomcc.doom_base
import os
import pathlib
import pytest
import tempfile
class Workbench:
pass
def test_demo_in_path():
w = Workbench()
w.demo_in_path = lambda: doomcc.doom_base.WadMap.demo_in_path(w)
w._file_base = lambda path: doomcc.doom_base.WadMap._file_base(w, path)
w.wad = pathlib.Path("scythe")
w.map = "01"
w.name_string = "_index"
w._name = "index"
with tempfile.TemporaryDirectory() as td:
w.demos = pathlib.Path(td)
with pytest.raises(Exception):
w.demo_in_path()
dp = pathlib.Path(td).joinpath("scythe")
os.mkdir(dp)
dp.joinpath("scythe_map01_index.lmp").touch()
assert w.demo_in_path() == dp.joinpath("scythe_map01_index.lmp")
dp.joinpath("scythe_map01_index-00028.lmp").touch()
assert w.demo_in_path() == dp.joinpath("scythe_map01_index-00028.lmp")

View file

@ -1,5 +1,5 @@
import cliff.app
import dcc.play
import doomcc.play
import logging
import os
import mockito.matchers
@ -26,26 +26,30 @@ def test_play(expect):
scp.mkdir()
scp.joinpath("config.toml").touch()
scp.joinpath("scythe.wad").touch()
dcc.play.Play.__init__ = lambda self: None
dcc.play.Play.get_epilog = lambda self: ""
rig = dcc.play.Play()
doomcc.play.Play.__init__ = lambda self: None
doomcc.play.Play.get_epilog = lambda self: ""
rig = doomcc.play.Play()
rig._hooks = []
parser = rig.get_parser("test_play")
parsed_args = parser.parse_args(args=["--doom", td, "scythe", "01"])
with expect(os, times=1).execv(
tdp.joinpath("dsda-doom").joinpath("exe"),
[
with (
expect(os, times=1)
.execv(
tdp.joinpath("dsda-doom").joinpath("exe"),
"-iwad",
tdp.joinpath("iwads").joinpath("DOOM2.WAD"),
"-file",
tdp.joinpath("pwads").joinpath("scythe").joinpath("scythe.wad"),
"-complevel",
"2",
"-skill",
"4",
"-warp",
"01",
]
).thenReturn(None):
[
tdp.joinpath("dsda-doom").joinpath("exe"),
"-iwad",
tdp.joinpath("iwads").joinpath("DOOM2.WAD"),
"-file",
tdp.joinpath("pwads").joinpath("scythe").joinpath("scythe.wad"),
"-complevel",
"2",
"-skill",
"4",
"-warp",
"01",
],
)
.thenReturn(None)
):
assert rig.run(parsed_args) is None