www
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYestin L. Harrison <yestin@ylh.io>2022-02-15 05:27:01 -0800
committerYestin L. Harrison <yestin@ylh.io>2022-02-15 05:27:01 -0800
commitf5561fbbcbc43fbe88cc879ffb5e425877368597 (patch)
tree00aefc088b6b59ab169c4aa787cda4173600625a
downloaddemoheader-f5561fbbcbc43fbe88cc879ffb5e425877368597.tar.gz
demoheader-f5561fbbcbc43fbe88cc879ffb5e425877368597.tar.xz
demoheader-f5561fbbcbc43fbe88cc879ffb5e425877368597.zip
it works
-rw-r--r--.gitignore19
-rw-r--r--LICENSE13
-rw-r--r--README.md39
-rw-r--r--rebar.config13
-rw-r--r--rebar.lock8
-rw-r--r--src/demoheader.app.src15
-rw-r--r--src/demoheader.erl130
7 files changed, 237 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f1c4554
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+.rebar3
+_*
+.eunit
+*.o
+*.beam
+*.plt
+*.swp
+*.swo
+.erlang.cookie
+ebin
+log
+erl_crash.dump
+.rebar
+logs
+_build
+.idea
+*.iml
+rebar3.crashdump
+*~
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0110b0e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2021 Yestin L. Harrison <yestin@ylh.io>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0b444bd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+demoheader
+=====
+
+Simple escript to parse Source Engine demo headers.
+
+Build
+-----
+
+ $ rebar3 escriptize
+
+Run
+---
+
+ $ _build/default/bin/demoheader
+
+Usage
+-----
+
+ $ demoheader --help
+ Usage: demoheader -j [-dnscmgStfl] demo.dem
+ demoheader -dnscmgStfl demo.dem
+
+ -j, --json Emit JSON
+ -d, --demo-protocol (version number)
+ -n, --network-protocol (version number)
+ -s, --server-name (IP:port string)
+ -c, --client-name (recorder's in-game name)
+ -m, --map-name (without .bsp extension)
+ -g, --game-directory (tf/, hl2/, etc)
+ -S, --playback-seconds (float)
+ -t, --ticks (integer)
+ -f, --frames (integer)
+ -l, --signon-length (integer)
+ -h, --help Show this help
+
+ If -j is not given, exactly one header element must be selected (-dnscmgStfl),
+ and it will be printed on standard output. If -j is given, any number of
+ header elements may be selected; specifying none is equivalent to specifying
+ all of them. Keys are the long forms of their corresponding flags. \ No newline at end of file
diff --git a/rebar.config b/rebar.config
new file mode 100644
index 0000000..738db9d
--- /dev/null
+++ b/rebar.config
@@ -0,0 +1,13 @@
+{erl_opts, [no_debug_info]}.
+{deps, [{getopt, "1.0.2"}]}.
+
+{escript_incl_apps,
+ [demoheader]}.
+{escript_main_app, demoheader}.
+{escript_name, demoheader}.
+{escript_emu_args, "%%! +sbtu +A1\n"}.
+
+%% Profiles
+{profiles, [{test,
+ [{erl_opts, [debug_info]}
+ ]}]}.
diff --git a/rebar.lock b/rebar.lock
new file mode 100644
index 0000000..761d3f4
--- /dev/null
+++ b/rebar.lock
@@ -0,0 +1,8 @@
+{"1.2.0",
+[{<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.2">>},0}]}.
+[
+{pkg_hash,[
+ {<<"getopt">>, <<"33D9B44289FE7AD08627DDFE1D798E30B2DA0033B51DA1B3A2D64E72CD581D02">>}]},
+{pkg_hash_ext,[
+ {<<"getopt">>, <<"A0029AEA4322FB82A61F6876A6D9C66DC9878B6CB61FAA13DF3187384FD4EA26">>}]}
+].
diff --git a/src/demoheader.app.src b/src/demoheader.app.src
new file mode 100644
index 0000000..17784df
--- /dev/null
+++ b/src/demoheader.app.src
@@ -0,0 +1,15 @@
+{application, demoheader,
+ [{description, "escript to parse source engine demo headers"},
+ {vsn, "0.1.0"},
+ {registered, []},
+ {applications,
+ [kernel,
+ stdlib,
+ getopt
+ ]},
+ {env,[]},
+ {modules, []},
+
+ {licenses, ["ISC"]},
+ {links, []}
+ ]}.
diff --git a/src/demoheader.erl b/src/demoheader.erl
new file mode 100644
index 0000000..821dd43
--- /dev/null
+++ b/src/demoheader.erl
@@ -0,0 +1,130 @@
+-module(demoheader).
+
+-export([main/1]).
+
+% bless epp
+-define(HEADER,
+ ?X('demo-protocol', $d, "(version number)", 32, signed-little),
+ ?X('network-protocol', $n, "(version number)", 32, signed-little),
+ ?X('server-name', $s, "(IP:port string)", 260, bytes),
+ ?X('client-name', $c, "(recorder's in-game name)", 260, bytes),
+ ?X('map-name', $m, "(without .bsp extension)", 260, bytes),
+ ?X('game-directory', $g, "(tf/, hl2/, etc)", 260, bytes),
+ ?X('playback-seconds', $S, "(float)", 32, float-little),
+ ?X('ticks', $t, "(integer)", 32, signed-little),
+ ?X('frames', $f, "(integer)", 32, signed-little),
+ ?X('signon-length', $l, "(integer)", 32, signed-little)
+).
+
+-define(X(ATOM, CHAR, DESC, _W, _T), F(ATOM, CHAR, DESC)).
+main(Args) ->
+ F = fun(A, C, D) -> {A, C, atom_to_list(A), undefined, D} end,
+ Opts = [
+ F(json, $j, "Emit JSON"),
+ ?HEADER,
+ F(help, $h, "Show this help")
+ ],
+ case Args of
+ [_|_] ->
+ args(Opts, Args);
+ _ ->
+ usage(Opts),
+ halt(1)
+ end.
+-undef(X).
+
+-define(X(ATOM, _C, _D, _W, _T), ATOM).
+is_header(Atom) -> lists:member(Atom, [?HEADER]).
+args(Opts, Args) ->
+ DU = fun(F, A) ->
+ err("error: " ++ F, A),
+ usage(Opts),
+ halt(1)
+ end,
+ {Options, NonOptionArgs} = case getopt:parse(Opts, Args) of
+ {error, E} -> DU("~s", [getopt:format_error(Opts, E)]);
+ {ok, Res} -> Res
+ end,
+ lists:member(help, Options) andalso begin usage(Opts), halt(0) end,
+ Headers = lists:filter(fun is_header/1, Options),
+ Retrieval = case lists:member(json, Options) of
+ true -> json_of(case Headers of
+ [] -> [?HEADER];
+ Else -> Else
+ end);
+ _ -> case Headers of
+ [One] -> fun(#{One := V}) ->
+ Fmt = case is_list(V) of true -> "~s"; _ -> "~p" end,
+ io_lib:format(Fmt, [V])
+ end;
+ _ -> DU("without -j, exactly one field must be requested", [])
+ end
+ end,
+ Parsed = case NonOptionArgs of
+ [Single] -> parse(Single);
+ _ -> DU("exactly one file must be specified", [])
+ end,
+ io:format("~s\n", [Retrieval(Parsed)]).
+-undef(X).
+
+json_of(Keys) ->
+ fun(Map) ->
+ F = fun(Key, Acc) ->
+ #{Key := Value} = Map,
+ [io_lib:format("\"~s\": ~p", [atom_to_list(Key), Value])|Acc]
+ end,
+ "{\n\t" ++
+ lists:join(",\n\t", lists:foldl(F, [], lists:reverse(Keys))) ++
+ "\n}"
+ end.
+
+-define(X(ATOM, _C, _D, WIDTH, TYPE), fun
+ ({<<Var:WIDTH/TYPE,Rest/binary>>, Map}) -> {Rest, Map#{ATOM => Var}}
+end).
+parse(Arg) ->
+ In = case file:open(Arg, [read, binary]) of
+ {ok, I} -> I;
+ {error, EE} -> die("~s: ~s", [Arg, file:format_error(EE)])
+ end,
+ Bin = case file:read(In, 1072) of
+ {ok, B} when byte_size(B) =:= 1072 -> B;
+ {ok, _} -> die("could not read 1072 bytes (is ~s a demo?)", [Arg]);
+ {error, E} -> die("error reading ~s: ~s", [Arg, file:format_error(E)])
+ end,
+ file:close(In),
+ Rem = case Bin of
+ <<"HL2DEMO\0", R/binary>> -> R;
+ _ -> die("did not see \"HL2DEMO\" at start (is ~s a demo?)", [Arg])
+ end,
+ {<<>>, Map} = lists:foldl(fun(F, A) -> F(A) end, {Rem, #{}}, [?HEADER]),
+ maps:map(fun
+ (_, S) when is_binary(S) ->
+ % emitting nulls breaks a lot of things, it's kind of funny
+ unicode:characters_to_list(case binary:match(S, <<0>>) of
+ {Pos, _} -> binary:part(S, 0, Pos);
+ _ -> S
+ end);
+ (_, V) ->
+ V
+ end, Map).
+-undef(X).
+
+-define(X(_A, CHAR, _D, _W, _T), CHAR).
+usage(Opts) ->
+ err("Usage: ~w -j [-~s] demo.dem", [?MODULE, [?HEADER]]),
+ err(" ~w -~s demo.dem\n", [?MODULE, [?HEADER]]),
+ err("~ts", [unicode:characters_to_list(getopt:usage_options(Opts))]),
+ err("If -j is not given, exactly one header element must be selected "
+"(-" ++ [?HEADER] ++ "),\n"
+"and it will be printed on standard output. If -j is given, any number of\n"
+"header elements may be selected; specifying none is equivalent to specifying\n"
+"all of them. Keys are the long forms of their corresponding flags.").
+-undef(X).
+
+die(Fmt, Args) ->
+ err(Fmt, Args),
+ halt(1).
+err(Str) ->
+ err("~s", [Str]).
+err(Fmt, Args) ->
+ io:format(standard_error, Fmt ++ "\n", Args).