Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mlugg committed Mar 18, 2023
0 parents commit d73a3ed
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
zig-out/
zig-cache/
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2023 Matthew Lugg

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.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# demofixup

A tool for fixing up Portal 2 demos recorded prior to game build 8873 to play on modern builds.

## Usage

Command-line usage:

./demofixup in.dem [out.dem]

If `out.dem` is not specified, it defaults to `in_fixed.dem`.

This usage means that on Windows, you can drag and drop a demo file onto the binary to convert it.

## Details

Portal 2 build 8873 removed the `point_survey` entity class. Unfortunately, this also broke all
previously recorded demos. This is because demos contain a section at the start called "data tables"
describing all entity classes and their associated networked properties. After the update, reading
this section triggers an error because it includes an entity - `point_survey` - which the client is
unaware of.

This entity was never actually used (it was an old playtesting feature). Thus, we can fix this by
simply removing the record of this entity from the datatables. There are two things that need
removing: the serverclass and the sendtable. The sendtable is easy - just omit its entry from the
list. The serverclass is harder, because every class is associated with an ID, and we want these to
be unmodified for other entity types. Moreover, the IDs must be in the range `0` to `n-1` where `n`
is the number of server classes. So, what we can do to solve this is replace the serverclass entry
for this with a duplicate of any other. We'll never use this entry, but that's not important: what
matters is that the game accepts it and plays the demo. Here, we replace `CPointSurvey` and
`DT_PointSurvey` with `CPointCamera` and `DT_PointCamera`.
23 changes: 23 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

const exe = b.addExecutable(.{
.name = "demofixup",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
exe.install();

const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}

const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
219 changes: 219 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
const std = @import("std");

pub fn main() !u8 {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const ally = arena.allocator();

const args = try std.process.argsAlloc(ally);
if (args.len != 2 and args.len != 3) {
std.io.getStdErr().writer().print(
"usage: {s} <in name> [out name]\n",
.{if (args.len > 0) args[0] else "demofixup"},
) catch {};
return 1;
}

const in_name = args[1];
const out_name = if (args.len == 3) args[2] else blk: {
const base = if (std.mem.endsWith(u8, in_name, ".dem") or std.mem.endsWith(u8, in_name, ".DEM"))
in_name[0 .. in_name.len - 4]
else
in_name;
break :blk try std.fmt.allocPrint(ally, "{s}_fixed.dem", .{base});
};

var in_file = try std.fs.cwd().openFile(in_name, .{});
defer in_file.close();
var out_file = try std.fs.cwd().createFile(out_name, .{});
defer out_file.close();

var buf_rd = std.io.bufferedReader(in_file.reader());
var buf_wr = std.io.bufferedWriter(out_file.writer());

const in_rd = buf_rd.reader();
const out_wr = buf_wr.writer();

try clone(in_rd, out_wr, 1072); // demo header

while (true) {
const kind = try in_rd.readByte();
try out_wr.writeByte(kind);
try clone(in_rd, out_wr, 5);
switch (kind) {
1, 2 => { // signon, packet
try clone(in_rd, out_wr, 76 * 2 + 8);
const size = try in_rd.readIntLittle(u32);
try out_wr.writeIntLittle(u32, size);
try clone(in_rd, out_wr, size);
},
3 => {}, // synctick
4 => { // consolecmd
const size = try in_rd.readIntLittle(u32);
try out_wr.writeIntLittle(u32, size);
try clone(in_rd, out_wr, size);
},
5 => { // usercmd
try clone(in_rd, out_wr, 4); // cmd
const size = try in_rd.readIntLittle(u32);
try out_wr.writeIntLittle(u32, size);
try clone(in_rd, out_wr, size);
},
6 => { // datatables
// oh boy 3am!!!
const size = try in_rd.readIntLittle(u32);
try out_wr.writeIntLittle(u32, size);
var count_rd = std.io.countingReader(in_rd);
var count_wr = std.io.countingWriter(out_wr);
try doDataTableStuff(count_rd.reader(), count_wr.writer(), ally);
try in_rd.skipBytes(size - count_rd.bytes_read, .{});
const to_pad = size - count_wr.bytes_written;
for (0..to_pad) |_| {
try out_wr.writeByte(0);
}
},
7 => { // stop
break;
},
8 => { // customdata
try clone(in_rd, out_wr, 4); // type
const size = try in_rd.readIntLittle(u32);
try out_wr.writeIntLittle(u32, size);
try clone(in_rd, out_wr, size);
},
9 => { // stringtables
const size = try in_rd.readIntLittle(u32);
try out_wr.writeIntLittle(u32, size);
try clone(in_rd, out_wr, size);
},
else => @panic("silly demo"),
}
}

// write all remaining data
var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
try fifo.pump(in_rd, out_wr);

try buf_wr.flush();

std.io.getStdOut().writer().print("Successfully wrote {s}!\n", .{out_name}) catch {};
return 0;
}

fn clone(in: anytype, out: anytype, n: usize) !void {
var buf: [64]u8 = undefined;
var rem = n;
while (rem > 64) {
try in.readNoEof(&buf);
try out.writeAll(&buf);
rem -= 64;
}
try in.readNoEof(buf[0..rem]);
try out.writeAll(buf[0..rem]);
}

fn cloneBits(br: anytype, bw: anytype, n: usize) !void {
var buf: [8]u8 = undefined;
var rem = n;
while (rem > 64) {
try br.reader().readNoEof(&buf);
try bw.writer().writeAll(&buf);
rem -= 64;
}
const x = try br.readBitsNoEof(u64, rem);
try bw.writeBits(x, rem);
}

fn doDataTableStuff(in_rd: anytype, out_wr: anytype, ally: std.mem.Allocator) !void {
var br_l = std.io.bitReader(.Little, in_rd);
var bw_l = std.io.bitWriter(.Little, out_wr);
const br = &br_l;
const bw = &bw_l;

// write out sendtables but remove bad one
while (try readBool(br)) {
const needs_dec = try readBool(br);
const table_name = try readStr(br, ally);
const num_props = try br.readBitsNoEof(u10, 10);

const skip = std.mem.eql(u8, table_name, "DT_PointSurvey\x00");

if (!skip) {
try bw.writeBits(@as(u1, 1), 1); // presence bit
try bw.writeBits(@boolToInt(needs_dec), 1);
try bw.writer().writeAll(table_name);
try bw.writeBits(num_props, 10);
}

for (0..num_props) |_| {
const prop_ty = try br.readBitsNoEof(u5, 5);
const prop_name = try readStr(br, ally);
const prop_flags = try br.readBitsNoEof(u19, 19);
const prop_priority = try br.readBitsNoEof(u8, 8);

if (!skip) {
try bw.writeBits(prop_ty, 5);
try bw.writer().writeAll(prop_name);
try bw.writeBits(prop_flags, 19);
try bw.writeBits(prop_priority, 8);
}

if (prop_ty == 6 or prop_flags & (1 << 6) != 0) {
const exclude_name = try readStr(br, ally);
if (!skip) {
try bw.writer().writeAll(exclude_name);
}
} else {
const extra_len: usize = switch (prop_ty) {
0...4 => 64 + 7,
5 => 10, // array
else => @panic("bad sendtable prop type"),
};

if (!skip) {
try cloneBits(br, bw, extra_len);
} else {
_ = try br.readBitsNoEof(u128, extra_len);
}
}
}
}

try bw.writeBits(@as(u1, 0), 1);

// write out the classes but replace the bad one with a dummy entry
const num_classes = try br.readBitsNoEof(u16, 16);
try bw.writeBits(num_classes, 16);
for (0..num_classes) |_| {
const class_id = try br.readBitsNoEof(u16, 16);
const class_name = try readStr(br, ally);
const dt_name = try readStr(br, ally);
if (std.mem.eql(u8, dt_name, "DT_PointSurvey\x00")) {
if (!std.mem.eql(u8, class_name, "CPointSurvey\x00")) {
@panic("bad class name using DT_PointSurvey");
}
try bw.writeBits(class_id, 16);
try bw.writer().writeAll("CPointCamera\x00");
try bw.writer().writeAll("DT_PointCamera\x00");
} else {
try bw.writeBits(class_id, 16);
try bw.writer().writeAll(class_name);
try bw.writer().writeAll(dt_name);
}
}
}

fn readBool(br: anytype) !bool {
const x = try br.readBitsNoEof(u1, 1);
return x == 1;
}

fn readStr(br: anytype, ally: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(ally);
while (true) {
const c = try br.reader().readByte();
try buf.append(c);
if (c == 0) break;
}
return buf.toOwnedSlice();
}

0 comments on commit d73a3ed

Please sign in to comment.