#!/usr/bin/perl -w ###################################################################### # Artemis packet parser, (C) 2015-2018 Nosey Nick Waterman, # https://noseynick.org/artemis/ # All wrong righted, all rights reserved. Licensed under thev GNU # Affero General Public License v3.0 https://www.gnu.org/licenses/agpl.txt # with Commons Clause https://commonsclause.com/ v1.0 ###################################################################### # Based heavily on https://artemis-nerds.github.io/protocol-docs/ # with help from / many thanks to chrivers and the artemis-nerds, # AwesomeAiden, @Slate, @Starry, @rjwut, and probably others # Thanks to Thom Robertson of course, creator of our favourite game! ###################################################################### # ++++++++++ WIP Support for 1.65 - thanks @Donovan! # ++++++++++ Test with multiple ships, particularly: # Connection that does nothing (logger --quiet?) # Ship 2 Main Ship 2 Helm Ship 2 Weap Ship 2 Eng # Ship 2 Sci Ship 2 Comm Ship 2 Fighter Ship 2 Data # Ship 2 Observer Ship 2 Captain (Ship 2) GM # Ship 2 Main (a 2nd one, to see if all non-ship1 mains get DMX events) # Ship 1 MULTI ("solo", incl Main) # Make one of them a jump ship (pirate or ximni jump ship?) # RUN ./tailer.sg ArtemisLog/*[cs] ... which includes parser.pl --test # Remember to test warp, jump, and emergency jump # Remember to test MANUAL beams # Remember to fly a fighter for a bit, shoot a bit, and take ship+fighter damage # Remember to collect + activate at least one upgrade? # Remember to drop a variety of attract+repel beacons # Remember to tag a creature, an NPCShip, maybe other player ship (mesh?) # Remember to detonate some mines # Try some pirate looting? # +++ Make a proper perl package + module # +++ Base on /usr/share/perl5/Class/Accessor/Faster.pm (array-based) # so object/packet fields maintained in order, and easier bitfield stuff? # +++ Base on Storable for storing? Overload '""' for printable version? # +++ Pluggable readers/writers for deadbeef, xz, gz, hex, pcap(?) # +++ and ACTUAL TCP (both connect() and listen()) # +++ Plugable per-packet hooks, where "printing out as Perl" is just ONE way to do it, # Damcon Grids are just another (optional) hook, etc # +++ CPAN it? # +++ consider -T for taint-checks # +++ consider Perl::Critic # +++ podchecker use v5.10.0; use strict; # use bytes; # BREAKS UTF8<->UTF16 conversion :-D use warnings; # use diagnostics; use Time::HiRes qw(time sleep); use Data::Dumper; # only really for quoting strings properly $Data::Dumper::Sortkeys = 1; our $VERSION = 1.026; ###################################################################### # Version History: # v1.026 2021-08-18 minor tweaks, mostly comments # v1.025 2020-12-31 PING support with --times (see mainscreen.pl for sender) # v1.024 Tweaked NaN handling further # v1.023 ShipType / VesselID to hullID https://github.com/artemis-nerds/protocol-docs/pull/201/files # v1.022 Minor tweaks # v1.021 ClientHeartbeat for Artemis v2.7.5 # v1.020 NPCShip.InNebula - thanks @Starry! # v1.019 Support for 1.70 - thanks @Donovan! # v1.018 Avoid inf loop on 0-len DEADBEEF # v1.017 Stringify $PROTOVERSION, --protoversion, remove the messy "25.106" stuff # v1.016 Quote, and ensure printability of, strings in --objstats # v1.015 licensed under AGPLv3 # v1.014 --nosss / --noshipsystemsync option for @Starry # v1.013 Creature.Unknown21=SingleScan, 23..26 floats, 31=Age, 32=ShowOnMaps # v1.012 re-worked some --objstats, to handle (+count) NaNs better. # v1.011 PlayerShip.Unknown57 = ShowOnMaps - thanks @Starry # v1.010 Base.AftShields = MaxShields, Unknown26 = Side - thanks @Starry # v1.009 better handling of empty args to many subs for packet-types-js.pl # v1.008 introduction of --hook option # v1.007 slightly cleaner NaN handling inside objectBitStreams # v1.006 simpleEvent 0x1c = fighterText # v1.005 Streamlined ShipSettings, included discovery of $has_name # v1.004 ObjGenericMesh.Scanned # v1.003 Full, neat, tidy parsing of the many flavours of gmText # v1.002 Research into startGame() and beam arcs for @Starry # v1.001 Support the great 2.1.1 object type renumbering, WITH AUTODETECT. # With support for all versions from 2.000000 to 2.7, I decided # this marks the first feature-complete full release - v1.0! ###################################################################### # Much less accurate beta version history: ###################################################################### # v0.80 SmokePuff() # v0.79 Tag() # v0.78 Preliminary support for the great 2.1.1 object type renumbering, # including ObjWhale before 2.1.1 # v0.77 Documenting the great 2.1.1 object type renumbering # v0.76 _PROTOVERSION() to neaten protocol version switching # v0.75 new_valueInt() factory / pseudo-supertype # v0.74 breaking up valueInt() to allow easier renumbering in future # v0.71 Create %untested from symbol-table not reading own source code # v0.70 Ripped the (unused) OO version out into OOparser.pl for later # v0.69 SingleScan/DoubleScan/ShowOnMaps and others from @Starry # v0.68 Creature Health # v0.66 InNebula changes for v2.7.0 # v0.64 Kaboom() # v0.62 Klaxon() # v0.60 Introduction of --objstats and "# full" # v0.58 Various other 2.6.3 / 2.7.0 updates # v0.56 Attempting some early support for versions < 2.2.0 # v0.54 Nicer decoding of commsMessage() # v0.52 objectText() type decoding # v0.50 ... and below ... undocumented ###################################################################### autoflush STDOUT 1; =head1 NAME parser.pl - parse Artemis SBS protocol =head1 SYNOPSIS parser.pl [ --help | --man | --version ] parser.pl [ OPTIONS ] [file[.gz|.xz]] ... =head1 DESCRIPTION B will take artemis SBS TCP streams, on stdin, in files listed on the cmdline, or in gzipped / xz files listed on the cmdline, and will decode the contesnts of every packet into somewhat human-readable Perl code. The Perl can itself be run to re-create the exact same TCP stream. When run like this, B is used as a Perl module with a big bunch of functions for recreating Artemis SBS protocol packets. =head1 OPTIONS =over =item B<--help> Print brief help and exits. =item B<--man> Prints the manual page and exits. =cut my (%opt, @opt); opt('help|?' => sub { pod2usage(-verbose => 1); }); opt('man' => sub { pod2usage(-verbose => 2); }); =item B<--version> Print some version information =item B<--test> Periodically output info useful for converage testing - specifically, show which functions have NOT been tested (called) yet. =cut opt('version|v' => sub { die "This is $0 version $VERSION.\n"; }); opt('test|t!' => 0); =item B<--protoversion> I Decode Protocol version: =over =item B<--protoversion> I<2.1.5> is protocol v2.1.5 =item B<--protoversion> I<2.5.106> is protocol v2.5.106 - it is considered greater than 2.5.1 but less than 2.5.2 =item B<--protoversion> I<2.7> is protocol v2.7.0 =back Environment variable $PROTOVERSION overrides the default, but can still be overridden by this cmdline option. =cut our $PROTOVERSION; opt('protoversion|p=s' => $ENV{PROTOVERSION} // '2.7.5'); sub _PROTOVERSION { $PROTOVERSION = shift; PopObjTypes(); PopConsoleType(); PopValueInt(); } ###################################################################### # +++ copy every OPTION into SYNOPSIS use Getopt::Long qw(:config gnu_getopt); use Pod::Usage; sub opt { my ($name, $def) = @_; push @opt, lc($name); if (defined ($def) && $name =~ /^(\w+|<>)/) { $opt{lc $1} = $def } } ###################################################################### our %parsers; # will be populated below my %untested; # for --test my %stats; # for --objstats my $verbose = 0; ###################################################################### # Utilities for creating / dumping (bits of) Artemis packets sub SelfInvert { # duplicate all key=>value in a hash as value=>key my ($hash) = @_; for (keys %$hash) { $hash->{$hash->{$_}} = $_; } } # Cheeky way to note BAD stuff, ignore 1st arg but keep any remaining: sub BAD { shift ; return @_ } # NOTE: UTF16 strings CAN sometimes (wrongly?) contain 0x0000 # mid-string. Need to decode properly, print properly, and re-encode # properly (or wrongly but identically :-/ ) sub UTF16 { my @chars = unpack('U*', shift); return pack('V v*', 1+scalar(@chars), @chars, 0); } sub ToUTF16 { # when we parse VXXXXV/v we will have an array of UTF16 chars returned # and will need to create some UTF16('blah') code for it: my ($ret, $len); while (defined ($len = shift)) { $ret .= '.' if $ret; unless ($len) { $ret .= '"\0\0\0\0"'; next; } my $str = pack('U*', splice(@_,0,$len-1,)); my $null = shift; $ret .= "UTF16(" . Data::Dumper::qquote($str) . ")" . ((($null//999) == 0) ? '' : ".BAD('UTF16_term " . ($null//'undef') . "!=NULL')"); } return $ret } sub UTF16_strs { my (@ret, $len); while (defined ($len = shift)) { unless ($len) { push @ret, undef; next; } push @ret, pack('U*', splice(@_,0,$len-1,)); return if shift; # remove the (assumed) NULL, return undef if it wasn't } for (@ret) { next unless defined $_; s/\0.*//; s/ +/ /g; s/^ //; s/ $//; } return @ret; } =item B<--noeof> Suppress the "# EOF" comment after each file/stream =item B<--header> Print the "# header" comments =item B<--hex> Expect Hex format files, specifically: !metadata_key=value # comment C01 efbeadde2000000002000000000000000c0000003c1d824c0f00000000000000 @1234 # client 1 S04 0123456789abcdef @2345 # server 4 # See https://github.com/chrivers/tricorder/ @timestamps, #comments, and [CS]nn Client/Server indicators are all optional. If specified TWICE (B<--hex --hex> or B<--hex>=I<2>), the files have had the "deadbeef" header removed, and are expected to contain exactly 1 packet per line, EG variations of: C01 3c1d824c0f00000000000000 @1234 # Ready() 3c1d824c0f00000000000000 # comment 3c1d824c0f00000000000000 =item B<--comments> Preserve any comments (and timestamps, Client/Server identifiers, metadata) that come in via the --hex files. If specified TWICE (B<--comments --comments> or B<--comments>=I<2>), the entire hex line will be provided as a comment before the parsed perl output. =item B<--times> Can ADD timestamp comments to heartbeat() packets if parsed from binary in real-time =cut opt('eof!' => 1); opt('header!' => 0); opt('hex:+' => 0); opt('comments:+' => 0); opt('times!' => 0); =item B<--maxpacket> I<5000> Maximum packet size. DEADBEEF packets bigger than this are assumed to be a fatal protocol error. Default 5000 (packets this big have never been observed) =cut # @Starry: At a guess artemis can't cope with packets about 4000 # bytes of payload long - 4018 bytes no hang 4036 bytes hanging - not # sure but I'm guessing its a hang with 4000 payload bytes + 20 # header bytes long opt('maxpacket=i' => 5000); sub ReadPackets { my $in = shift; my ($buf, $skip, $deadbeef, $totlen); # Binary files: # GotBytes will handle DEADBEEF de-framing BUT it's going to be useful # to read() a complete header and then a complete payload, so if we are in # a pipeline like nc 127.1 2010 | ./parser.pl # we can parse realtime, but NOT read 1 byte at a time. binmode($in); while (read($in, $buf, 8)) { # hopefully DEADBEEF + TOTLEN ($deadbeef, $totlen) = unpack('VV', $buf); while ($deadbeef && $deadbeef != 0xDEADBEEF) { GotBytes(substr($buf, 0, 1, '')); # process just 1 byte read($in, $buf, 1, 7) # ... replace with 1-byte read or return GotBytes($buf); ($deadbeef, $totlen) = unpack('VV', $buf); } if ($totlen && $totlen > 8 && $totlen <= $opt{maxpacket}) { read($in, $buf, $totlen-8, 8); } GotBytes($buf); } } sub ReadHexFile { my $in = shift; while (<$in>) { chomp; # remove EOL my $hex = $_; my ($comment, $timestamp, $cs); if (s/\@(\d+\.?\d*)//) { $timestamp = $1; } if (s/\#\s*(.*)//) { $comment = $1; } if (s/^([CS]\d*|-|\+)+//) { $cs = $1; } if ($opt{comments} > 1) { print "# $hex\n"; } elsif ($opt{comments} && ($comment || $timestamp || $cs)) { print "#", defined($cs) ? " $cs" : "", defined($comment) ? " $comment" : "", defined($timestamp) ? " \@$timestamp" : "", "\n"; } if (/^!(\S+)=(.+)/ || /^!(\S+)/) { print "# metadata $1", defined($2) ? " = $2" : "", "\n" if $opt{comments}; next; # no hex to parse here } s/\s+//g; # strip whitespace next unless $_; # skip blank lines die "Unparseable hex line: $hex" unless /^[0-9A-Fa-f]+$/; my $payload = pack('H*', $_); if ($opt{hex} > 1) { # --hex --hex means we have stripped DEADBEEF and expect 1 packet per line: my $name = ParsePacket(\%parsers, $payload); print ToTODO($name, $payload), "\n" if $name; } else { GotBytes($payload, $cs); } } } # so we can maintain different buffers for multiple C/S and only # parse once we have got complete DEADBEEF packets... my %bufs; sub GotBytes { my ($bytes, $chan) = @_; $chan //= ''; my $buf = $bufs{$chan} .= $bytes; # append to THIS CHAN'S buffer my $skip = index($buf, "\xEF\xBE\xAD\xDE"); if (($skip < 0) && (length($buf) > 140)) { # "get out of jail free" card if we have too much rubbish to SKIP: print 'print ', ToTODO('BAD:SKIPPED', substr($buf,0,128,'')), ";\n"; } while ($skip >= 0) { # found DEADBEEF if ($skip) { print 'print ', ToTODO('BAD:SKIPPED', substr($buf,0,$skip,'')), ";\n"; } last unless length($buf) >= 8; # found DEADBEEF + totlen my ($deadbeef, $totlen) = unpack('VV', $buf); # This should never happen, we have skipped to a DEADBEEF?!? die ToTODO('NOT DEADBEEF', $buf) unless $deadbeef == 0xDEADBEEF; die ToTODO("HUGE DEADBEEF $totlen?", $buf) if $totlen > $opt{maxpacket}; if ($totlen <= 8) { print 'print ', ToTODO('BAD:SHORT', substr($buf,0,8,'')), ";\n"; last; } last unless length($buf) >= $totlen; # We seem to have a COMPLETE deadbeef packet - remove + parse it! my $payload = substr($buf,0,$totlen,''); my $name = ParsePacket(\%parsers, $payload); print ToTODO($name, $payload), "\n" if $name; # There may be MORE THAN ONE deadbeef packet in $buf? $skip = index($buf, "\xEF\xBE\xAD\xDE"); } $bufs{$chan} = $buf; # we may have removed some bytes } sub _deadbeef { # Complete (except maybe the "zero") # NOTE: There is no PacketType 0xdeadbeef AS SUCH, but it seems # really unlikely Thom would put a deadbeef PacketType inside a # deadbeef wrapper, and this hack makes it easier to parse # HexDumps WITH or WITHOUT deadbeef wrappers. my ($totlen, $from, $zero, $len, $payload) = unpack('V V V V a*', shift); my $dir = { 1 => 's2c', 2 => 'c2s' } -> {$from}; $dir //= $from; print "BAD('from=$from')" unless $dir; print "BAD('deadbeef_0 ", $zero//'undef', "!=0');\n" unless (($zero//999) == 0); unless (defined ($totlen) && defined ($len) && $totlen == $len + 20) { print "BAD('deadbeef_len ", $len//'undef', "+20!=", $totlen//'undef', "');\n"; # ... but try anyway? } if ($len && $len > $opt{maxpacket}) { print "BAD('payload_len $len TOO BIG?');\n"; # ... but try anyway? } my $actual = length($payload); if (($len//0) != ($actual//0)) { print "BAD('payload_len ", $len//'undef', " != ACTUAL ", $actual//'undef', "');\n"; # ... but try anyway? } print "# Header $dir len=$len\n" if $opt{head}; # ... and print the payload: my $name = ParsePacket(\%parsers, $payload); print "$dir(", ToTODO($name, $payload), ");\n" if $name; } $parsers{0xdeadbeef} = \&_deadbeef; sub s2c { SendPacket(1, @_); } # server to client sub c2s { SendPacket(2, @_); } # client to server sub SendPacket { my $from = shift; my $payload = join('', @_); my $len = length($payload); print pack( 'V V V V V', 0xDEADBEEF, $len+20, $from, 0, $len) . $payload; } sub ParsePacket { my ($parsers, $payload, $name) = @_; return 'undef' unless defined $payload; my $type = unpack('V', $payload); my $parser = $parsers->{$type // 'undef'}; return "BADUnknownPacket:" . ($name ? "${name}:" : '') . (defined($type) ? sprintf('%8x', $type) : 'undef') unless $parser; $type = sprintf('%s0x%x', $name ? "${name}:" : '', $type); if (ref($parser) eq 'HASH') { return ParsePacket($parser, substr($payload, 4), $type); } elsif (ref($parser) eq 'CODE') { $parser->(substr($payload, 4)); return unless $opt{test}; delete $untested{$type}; # we HAVE just tested this packet type PrintUntested() if time() > $opt{test}; return undef; } else { return $parser; } } sub ToTODO { my ($name, $rest) = @_; return unless defined $rest; my @stuff = unpack('V*a*', $rest); $rest = pop @stuff; return "TODO('" . join(' ', $name, map(sprintf('%08x', $_), @stuff), # 32-bit ints / floats? map(sprintf('%02x', $_), unpack('C*', $rest)) # stray bytes? ) . "')"; } sub TODO { my (undef, @words) = split (' ', shift); # ignore first "name" argument my $ret = ''; while (@words) { my $word = shift @words; # Holy ####: Unpack the hex, then reverse the bytes $ret .= pack('C*', reverse unpack('C*', pack('H*', $word))); } return $ret; } ###################################################################### # Create or Display other non-packet stuff: my %grid; my $gridmax = 0; sub AddToGrid { my ($tab, $gr, $a, $b) = @_; my $val = ++$grid{"$tab$gr"}{$a}{$b}; if ($val > $gridmax) { $gridmax = $val; } ++$grid{$tab.'Tot'}{$a}{$b}; } sub ShowGrids { my $name = shift; my $gr = shift || 'Tot'; return unless $grid{"XY$gr"}; my ($xmin, $xmax) = range(keys %{$grid{"XY$gr"}}); my ($ymin, $ymax) = range(keys %{$grid{"YZ$gr"}}); my ($zmin, $zmax) = range(keys %{$grid{"ZX$gr"}}); print "### $name:\n"; for my $y ($ymin .. $ymax) { print '# '; for my $x ($xmin .. $xmax) { ShowGridSq('XY', $gr, $x, $y); } print ' | '; for my $z ($zmin .. $zmax) { ShowGridSq('YZ', $gr, $y, $z); } print "\n"; } print '# '; for my $x ($xmin .. $xmax) { print '--'; } print '-+-'; for my $z ($zmin .. $zmax) { print '--'; } print "\n"; for my $x ($xmin .. $xmax) { print '# '; for my $x2 ($xmin .. $xmax) { print $x==$x2 ?' \\':' '} print ' | '; for my $z ($zmin .. $zmax) { ShowGridSq('ZX', $gr, $z, $x); } print "\n"; } print "\n"; } sub range { return (0,0) unless @_; my @range = sort {$a <=> $b} @_; return ($range[0] - 1, $range[-1] + 1); } sub ShowGridSq { my ($tab, $gr, $a, $b) = @_; my $val = $grid{"$tab$gr"}{$a}{$b}; if ($val && $val > $gridmax) { print ' #'; } elsif ($val) { printf '%2d', int(9.9 * $val / $gridmax); } else { print ' '; } } =item B<--nogrids> After parsing each file/stream, B will usually output some comments showing which DamageControl grid-squares have been seen in various packets. B<--nogrids> suppresses these. =cut opt('grids!' => 1); sub ShowAllGrids { ShowGrids('Seen Damage Grid', 'D'); ShowGrids('Seen DamCon Grid', 'C'); ShowGrids('Seen DamCon Goal Grid', 'G'); ShowGrids('Total Grid'); %grid = (); $gridmax = 0; } ###################################################################### # Subs to dump particular Artemis packet types: $parsers{0x0351a5ac}{_name} = 'valueFloat'; # c2s sub _HelmSetImpulse { # Complete my ($throttle) = unpack('f<', shift); print "HelmSetImpulse($throttle);\n"; } $parsers{0x0351a5ac}{0x00} = \&_HelmSetImpulse; sub HelmSetImpulse { c2s pack('VV f<', 0x0351a5ac,0x00, @_); } sub _HelmSetSteering { # Complete my ($rudder) = unpack('f<', shift); print "HelmSetSteering($rudder);\n"; } $parsers{0x0351a5ac}{0x01} = \&_HelmSetSteering; sub HelmSetSteering { c2s pack('VV f<', 0x0351a5ac,0x01, @_); } sub _HelmSetPitch { # Complete my ($pitch) = unpack('f<', shift); print "HelmSetPitch($pitch);\n"; } $parsers{0x0351a5ac}{0x02} = \&_HelmSetPitch; sub HelmSetPitch { c2s pack('VV f<', 0x0351a5ac,0x02, @_); } my %ShipSystems = ( # Always Int (V) 0x00 => 'Beams', 0x01 => 'Torps', 0x02 => 'Sense', 0x03 => 'Manuv', 0x04 => 'Impul', 0x05 => 'Warp', 0x06 => 'FShld', 0x07 => 'RShld', ); SelfInvert(\%ShipSystems); sub _EngSetEnergyV1 { # seen in v1.70 my ($energy, $system) = unpack('f< V', shift); print "EngSetEnergyV1($energy, $system); # ", $ShipSystems{$system} // 'BADShipSys'.$system, "\n"; } $parsers{0x0351a5ac}{0x03} = \&_EngSetEnergyV1; sub EngSetEnergyV1 { # ++++ WIP c2s pack('VV f< V', 0x0351a5ac,0x03, @_); } sub _EngSetEnergy { # Complete my ($dat) = @_; my ($energy, $system) = unpack('f< V', $dat); my $name = $ShipSystems{$system}; if ($name) { print "EngSetEnergy($energy, $system); # $name\n"; } else { my ($bearing, $dist) = unpack('f< f<', $dat); print "HelmSetJumpV1($bearing, $dist);\n"; } } $parsers{0x0351a5ac}{0x04} = \&_EngSetEnergy; sub EngSetEnergy { c2s pack('VV f< V', 0x0351a5ac,0x04, @_); } sub HelmSetJumpV1 { c2s pack('VV f< f<', 0x0351a5ac,0x04, @_); } sub _HelmSetJump { # Complete? +++ Decode bearing+dist? my ($bearing, $dist, $Z) = unpack('f< f< f<', shift); # $bearing 0->1 is 0-360deg # $dist 0->1 is 0-50k? if (defined $Z) { print "gmSelectV1($bearing,$dist,$Z);\n"; } else { print "HelmSetJump($bearing,$dist);\n"; } } $parsers{0x0351a5ac}{0x05} = \&_HelmSetJump; sub HelmSetJump { c2s pack('VV f< f<', 0x0351a5ac,0x05, @_); } sub gmSelectV1 { c2s pack('VV f< f< f<', 0x0351a5ac,0x05, @_); } sub _gmSelect { # Complete? my ($x,$y,$z) = unpack('f< f< f<', shift); # ++++ MAYBE $z,$y,$x ? $y always 0.0? print "gmSelect($x,$y,$z);\n"; } $parsers{0x0351a5ac}{0x06} = \&_gmSelect; sub gmSelect { c2s pack('VV f< f< f<', 0x0351a5ac,0x06, @_); } sub _FighterPilotPacket { # Good? my ( $rudder, $id, $unk1, $X, $Y, $Z, $OriX, $OriY, $OriZ, $OriW) = unpack('f< V f< f< f< f< f< f< f< f<', shift); # rudder 0.0 .. 1.0 # $unk1 always 0? Even for multiple fighters from multiple capitalShips # See also https://github.com/artemis-nerds/protocol-docs/issues/63 print "FighterPilotPacket($rudder, $id, $unk1, $X, $Y, $Z, $OriX, $OriY, $OriZ, $OriW);\n"; } $parsers{0x0351a5ac}{0x07} = \&_FighterPilotPacket; sub FighterPilotPacket { # my ($rudder, $id, $unk1, $X,$Y,$Z, $OX,$OY,$OZ,$OW) = @_; c2s pack('VV f< V f< f< f< f< f< f< f< f<', 0x0351a5ac,0x07, @_); } =item B<--nosss> | B<--noshipsystemsync> Suppress shipSystemSync packets =cut opt('sss|shipsystemsync!' => 1); sub _shipSystemSync { # AKA EngGridUpdate. Complete? return unless $opt{sss}; my ($full, $rest) = unpack('C a*', shift); # Seems to mean 1="Full Update" if requested with RequestFullUpdate() # vs 0="just a partial/delta update" print "shipSystemSync($full,"; my $b; # 0xFF-terminated LIST (size not known in advance) # ... of Artemis::Damage while ((($b = unpack('C', $rest)) // 0xFF) != 0xFF ) { # 0xFF marks EOL my ( $x,$y,$z, $dam, $next) = unpack('c c c f< a*', $rest); $rest = $next; print "\n Damage($x,$y,$z => $dam),"; # Store in our grid: AddToGrid('XY', 'D', $x, $y); AddToGrid('YZ', 'D', $y, $z); AddToGrid('ZX', 'D', $z, $x); } print "BAD('EODamage $b')," unless ($b // 0) == 255; print 'EODamage(),'; $rest = substr($rest, 1); # print + skip the 0xFF # 0xFE-terminated LIST (size not known in advance) # ... of Artemis::DamConTeam while ((($b = unpack('C', $rest)) // 0xFE) != 0xFE ) { # 0xFE marks EOL my ( $id, $gx,$x,$gy,$y,$gz,$z, $prog, $ppl, $next) = unpack('c V V V V V V f< V a*', $rest); $rest = $next; print "\n DamConTeam($id => $x,$y,$z => $gx,$gy,$gz => $prog, $ppl),"; # Store in our grid: AddToGrid('XY', 'C', $x, $y); AddToGrid('YZ', 'C', $y, $z); AddToGrid('ZX', 'C', $z, $x); AddToGrid('XY', 'G', $gx, $gy); AddToGrid('YZ', 'G', $gy, $gz); AddToGrid('ZX', 'G', $gz, $gx); } print "BAD('EODamConTeam $b')," unless ($b // 0) == 254; print "EODamConTeam());\n"; } $parsers{0x077e9f3c} = \&_shipSystemSync; sub EngGridUpdate { shipSystemSync(@_); } # old name sub shipSystemSync { my ($a, @rest) = @_; s2c pack('V C', 0x077e9f3c, $a//0).join('', @rest); } sub Damage { my ($x, $y, $z, $dam) = @_; return pack('ccc f<', $x,$y,$z, $dam); # _s2c } sub EODamage { return "\xFF" } sub DamConTeam {my ($id, $x,$y,$z, $gx,$gy,$gz, $prog, $ppl) = @_; return pack('c VVVVVV f< V',$id, $gx,$x,$gy,$y,$gz,$z, $prog, $ppl); # _s2c } sub EODamConTeam { return "\xFE" } my %ConsoleType; sub PopConsoleType { %ConsoleType = (); my $n = -1; for ( 'CommsMsg', # -1 Dummy for gmText 'Main', 'Helm', 'Weap', 'Eng', 'Sci', 'Comm', # 0..5 "MHWESCO" $PROTOVERSION ge '2.3' ? ('Fighter') : (), # 6 if present $PROTOVERSION ge '2.1' ? ('Data') : (), # 7 if present 'Observer', 'Captain', 'GM' # 6..8 | 7..9 | 8..10 ) { $ConsoleType{$n} = $_; $ConsoleType{$_} = $n++; } $ConsoleType{2999} = 'idleTextFreeform'; # actually gmText 3000 } sub _clientConsoles { # prev ConsoleStatus. Complete print 'clientConsoles('; my ( $ship, @status) = unpack('V C*', shift); print "ship=>$ship"; for (0 .. (scalar(@status)-1)) { my $sta = $status[$_]; print ', ', $ConsoleType{$_} // "BAD('CONSOLE', $_)", "=>$sta"; # +++ enumerate? 0 => 'free', 1 => 'YOU', 2=> 'TAKEN' } print ");\n"; } $parsers{0x19c6e2d4} = \&_clientConsoles; sub ConsoleStatus { clientConsoles(@_); } # old name sub clientConsoles { my %arg = @_; my @consoles; for (0 .. 20) { # probably only 10, but... last unless $ConsoleType{$_}; push @consoles, $arg{$ConsoleType{$_}} // 0; } s2c pack('V V C*', 0x19c6e2d4, $arg{ship}//0, @consoles); } sub _gmButton { # Complete? my ($subtype, $rest) = unpack('C a*', shift); if ($subtype == 0) { my @name = unpack('VXXXXV/v', $rest); print "gmButton($subtype, ", ToUTF16(@name), "); # Destroy\n"; } elsif ($subtype == 1) { my @name = unpack('VXXXXV/v', $rest); print "gmButton($subtype, ", ToUTF16(@name), "); # Create\n"; } elsif ($subtype == 2) { my @name = unpack('VXXXXV/v VV VV', $rest); my ($x,$y, $w,$h) = splice @name, -4, 4, (); # pop 'em all off print "gmButton($subtype, ", ToUTF16(@name), ", $x,$y, $w,$h); # CreatePlace\n"; } elsif ($subtype == 3) { # MenuWidth???? my $w = unpack('V', $rest); print "gmButton($subtype, $w); # MenuWidth????\n"; } elsif ($subtype == 99) { # Instructions my @titleContent = unpack('(VXXXXV/v)*', $rest); # probably only TWO strings? print "gmButton($subtype, ", ToUTF16(@titleContent), "); # Instructions\n"; } elsif ($subtype == 100) { # Reset print "gmButton($subtype); # Reset\n"; } else { print "gmButton($subtype"; print ", ", ToTODO("BAD:gmButton$subtype", $rest) if defined $rest; print "); # BAD:gmButtonSubType?\n"; } } $parsers{0x26faacb9} = \&_gmButton; sub gmButton { my ($subtype, @rest) = @_; my $rest = ''; for (@rest) { if (/^\d+$/) { $rest .= pack('V', $_); } # everything except... else { $rest .= $_; } # strings } s2c pack('V C', 0x26faacb9, $subtype//0) . $rest; } =item B<-d> | B<--discord> Send startGame, GameOverReason and GameOverStats to discord via B<./discord.sh> =cut opt('discord!' => 0); my @discord = (); sub discord { return unless $opt{discord}; open(OUT, "| ./discord.sh --send") or return; print OUT @_; # +++++ BLOCKING close OUT; } my %GameType = ( 0 => 'Siege', 1 => 'Single Front', 2 => 'Double Front', 3 => 'Deep Strike', 4 => 'Peacetime', 5 => 'Border War', 6 => 'Infestation', ); SelfInvert(\%GameType); sub _startGame { # AKA Difficulty, Complete # https://github.com/artemis-nerds/protocol-docs/issues/126 # Without this packet, your helm/weapons screens will have no beam arcs! my ($difficulty, $gametype) = unpack('VV', shift); my $nicename = $GameType{$gametype//''}//''; if ($nicename) { print "startGame($difficulty,'$nicename');\n"; } elsif (defined $gametype) { print "startGame($difficulty,BAD('GAMETYPE',$gametype));\n"; } else { # version 2.0.0 print "startGame($difficulty);\n"; } @discord = ("Level ** $difficulty ** $nicename game"); } $parsers{0x3de66711} = \&_startGame; sub Difficulty { startGame(@_); } # old name sub startGame { my ($difficulty,$gametype) = @_; if (defined $gametype) { # version > 2.0 if ($gametype =~ /^[a-z]/i) { $gametype = $GameType{$gametype}; } s2c pack('V VV', 0x3de66711, $difficulty,$gametype); } else { s2c pack('V V', 0x3de66711, $difficulty // 1); } } ###################################################################### # 0x4c821d3c = valueInt c2s # see https://github.com/artemis-nerds/protocol-docs/pull/87/files ####### TYPE:SUBT 1.X? # 0x4c821d3c:0x00 HelmSetWarp # 0x4c821d3c:0x01 SetMainScreen # 0x4c821d3c:0x02 SetWeaponsTarget # 0x4c821d3c:0x03 ToggleAutoBeams # 0x4c821d3c:0x04 ToggleShields # 0x4c821d3c:0x05 ShieldsUp # 0x4c821d3c:0x06 ShieldsDown # 0x4c821d3c:0x07 RequestDock ####### TYPE:SUBT 1.X? # 0x4c821d3c:0x08 FireTube # 0x4c821d3c:0x09 UnloadTube # 0x4c821d3c:0x0a ToggleRedAlert # 0x4c821d3c:0x0b SetBeamFreq # 0x4c821d3c:0x0c EngSetAutoDamcon # 0x4c821d3c:0x0d SetShip # 0x4c821d3c:0x0e SetConsole # 0x4c821d3c:0x0f Ready ####### TYPE:SUBT 1.X? # 0x4c821d3c:0x10 SciSelect # 0x4c821d3c:0x11 CaptainSelect # 0x4c821d3c:0x12 GameMasterSelect # 0x4c821d3c:0x13 SciScan # 0x4c821d3c:0x14 Keystroke # 0x4c821d3c:0x15 ButtonClick ####### TYPE:SUBT 1.X? 2.1.5 2.3.0 2.3.109/2.4.0 # 0x4c821d3c:0x16 SetShipSettings* SomeKindaNewKeyStroke? # 0x4c821d3c:0x17 EngResetCoolant \ SetShipSettingsV24* # 0x4c821d3c:0x18 HelmToggleReverse \ EngResetCoolant # 0x4c821d3c:0x19 RequestFullUpdate \ HelmToggleReverse # 0x4c821d3c:0x1a TogglePerspective \ RequestFullUpdate # 0x4c821d3c:0x1b ClimbDive *OldClimbDive \ TogglePerspective # 0x4c821d3c:0x1c \ ClimbDive \ *OldClimbDive # 0x4c821d3c:0x1d FighterLaunch \ ClimbDive # 0x4c821d3c:0x1e FighterShoot \ FighterLaunch # 0x4c821d3c:0x1f \ FighterShoot ####### TYPE:SUBT 1.X? 2.1.5 2.3.0 2.3.109 2.5.106/2.6 2.6.204/2.7 2.7.5 # 0x4c821d3c:0x20 EmergencyJump # 0x4c821d3c:0x21 SingleSeatSelect* # 0x4c821d3c:0x22 BioBeaconControl # 0x4c821d3c:0x23 ??? # 0x4c821d3c:0x24 ClientHeartbeat my (%valueInt, %valueIntVer); $parsers{0x4c821d3c} = \%valueInt; $parsers{0x4c821d3c}{_name} = 'valueInt'; sub new_valueInt { my $name = shift; my %opts; if ($_[0] =~ /^v\d+/) { %opts = @_} else { %opts = ('v1' => @_); } $main::{"_$name"} = $opts{unpacker} // sub { my (@args) = unpack($opts{fmt} // 'V*', shift); # not uber-efficient, but valueInt is quite an infrequent c2s packet my $comment; for (sort keys %opts) { next unless /^comment(\d+)/; $comment .= ' ' if $comment; $comment .= $opts{$_}->{$args[$1]} // $opts{$_}->{_def} // 'UNKNOWN'; } print "$name(", join(',', @args), ");", $comment ? " # $comment\n" : "\n"; }; $main::{$name} = $opts{packer} // sub { c2s pack('VV'.($opts{fmt}//'V*'), 0x4c821d3c, $valueInt{$name}, @_); }; for (keys %opts) { next unless /^v(\d+.*)/; $valueIntVer{$1}{$opts{$_}} = $name; } } new_valueInt(HelmSetWarp => 0x00); new_valueInt(SetMainScreen => 0x01, comment0 => {qw( 0 front 1 left 2 right 3 rear 4 TAC 5 LRS 6 INFO )}); new_valueInt(SetWeaponsTarget => 0x02); new_valueInt(ToggleAutoBeams => 0x03); new_valueInt(ToggleShields => 0x04); new_valueInt(ShieldsUp => 0x05); new_valueInt(ShieldsDown => 0x06); new_valueInt(RequestDock => 0x07, comment0 => { 0 => 'from helm', _def => 'from fighter', }, ); new_valueInt(FireTube => 0x08); new_valueInt(UnloadTube => 0x09); new_valueInt(ToggleRedAlert => 0x0a); new_valueInt(SetBeamFreq => 0x0b, comment0 => {qw( 0 A 1 B 2 C 3 D 4 E )}, ); new_valueInt(EngSetAutoDamcon => 0x0c); new_valueInt(SetShip => 0x0d, comment0 => { 0 => 'Ship 1: Artemis', 1 => 'Ship 2: Intrepid', 2 => 'Ship 3: Aegis', 3 => 'Ship 4: Horatio', 4 => 'Ship 5: Excalibur', 5 => 'Ship 6: Hera', 6 => 'Ship 7: Ceres', 7 => 'Ship 8: Diana', }, ); new_valueInt(SetConsole => 0x0e, comment0 => \%ConsoleType, comment1 => { qw( 0 REMOVE 1 ADD ) }, ); new_valueInt(Ready => 0x0f); # https://github.com/artemis-nerds/protocol-docs/issues/149 new_valueInt(SciSelect => 0x10); new_valueInt(CaptainSelect => 0x11); new_valueInt(GameMasterSelect => 0x12); new_valueInt(SciScan => 0x13); new_valueInt(Keystroke => 0x14, # See https://msdn.microsoft.com/en-us/library/aa243025.aspx comment0 => { qw( 0 NONE 1 LMouse 2 RMouse 3 Cancel 4 MMouse 8 BackSp 9 TAB 12 Clear 13 Enter 16 Shift 17 Ctrl 18 Alt 19 Pause 20 Caps 27 ESC 32 SPACE 33 PgUp 34 PgDn 35 End 36 Home 37 Left 38 Up 39 Right 40 Down 41 Select 42 PrtScr 43 Exec 44 Snapsh 45 INS 46 DEL 47 Help 91 Win 96 KP0 97 KP1 98 KP2 99 KP3 100 KP4 101 KP5 102 KP6 103 KP7 104 KP8 105 KP9 106 * 107 + 108 KPEnter 109 - 110 . 111 / 112 F1 113 F2 114 F3 115 F4 116 F5 117 F6 118 F7 119 F8 120 F9 121 F10 122 F11 123 F12 124 F13 125 F14 126 F15 127 F16 144 NumLock ), map {(ord($_)=>$_)} "A".."Z", "0".."9" }, ); new_valueInt(ButtonClick => 0x15); # unk, JamCRC(button_name) our %DriveType = ( 0 => 'Warp', 1 => 'Jump'); SelfInvert(\%DriveType); # ++++++++++ Rename HullIDs or something? https://github.com/artemis-nerds/protocol-docs/issues/165 our %ShipType = ( # See dat/vesselData.xml - borrowed from 2.6.0 0 => 'LightCruiser', 1 => 'Scout', 2 => 'Battleship', 3 => 'MissileCruiser', 4 => 'Dreadnought', 5 => 'Carrier', 6 => 'MineLayer', 7 => 'Juggernaut', 8 => 'XimniCruiser', 9 => 'XimniScout', 10 => 'XimniMissileCruiser', 11 => 'XimniBattleship', 12 => 'XimniCarrier', 13 => 'XimniDreadnought', 14 => 'Strongbow', 15 => 'Longbow', 16 => 'Brigantine', 100 => 'TSNMediumFighter', 101 => 'TSNBomber', 102 => 'TSNShuttle', 103 => 'TSNLRShuttle', 120 => 'ZimFighter', 121 => 'ZimBomber', 122 => 'ZimShuttle', 123 => 'ZimLRShuttle', 130 => 'Avenger', 131 => 'Adventure', # ++++++++++++++++++ parse some provided/default vesselData.xml for other $ShipType{}s? ); SelfInvert(\%ShipType); # Couple of util function used by TSN-CA-Ships.pl TSN-RP-Ships.pl EF-Ships.pl etc our ($tsn_ship, $baynum); sub ShipConf { my ($drive, $type, $col, $name, @bays) = @_; $| = 1; # autoflush and... sleep 0.2; # ... delay seem to help SetShip($tsn_ship++); sleep 0.2; # SetConsole(1,1); # Helm ADD SetShipSettingsV24( ShipSetting($drive, $type, $col, UTF16($name)) ); sleep 0.2; $baynum = 0; # for the next ship return unless @bays; SingleSeatSelect(0, @bays); } sub SSConf { # Return a FUNCTION that creates a SingleSeat record for fighers/bombers/shuttles: # Provide me with qw(uniqueID Race broadType) - values from vesselData.xml: # The fighter/bomber/shuttle vessel "uniqueID" (usually start around 100) # The hullRace "name" corresponding to ID=side # the fighter/bomber/shuttle vessel "broadType" my ($id, $race, @broadType) = @_; return sub { SingleSeatRec($id, $baynum++, UTF16(shift).UTF16($race).UTF16("@broadType")) }; } new_valueInt(SetShipSettings => 0x16, # 'v2.3.109' => -1, unpacker => sub { # Complete if ($PROTOVERSION gt '2.3.0') { # ACTUALLY this looks like SomeKindaNewKeyStroke? ++++++++++++++++++++ # Get server to send KeyCaptureToggle(1); and check :-( # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/147 print "_PROTOVERSION('2.3.0'); # AUTO-DOWNGRADE\n"; _PROTOVERSION('2.3.0'); } print "SetShipSettings(\n"; ToShipSetting(shift); print ");\n"; }, packer => sub { c2s pack('VV', 0x4c821d3c,0x16).join('', @_); }, ); new_valueInt(SetShipSettingsV24 => 'v2.3.109' => 0x17, unpacker => sub { # Complete my $dat = shift; if (length($dat)) { print "SetShipSettingsV24(\n"; ToShipSetting($dat); print ");\n"; } else { print "_PROTOVERSION('2.3.0'); # AUTO-DOWNGRADE\n"; _PROTOVERSION('2.3.0'); $valueInt{0x17}->($dat); } }, packer => sub { c2s pack('VV', 0x4c821d3c,0x17).join('', @_); }, ); # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/125 new_valueInt(EngResetCoolant => 0x17, 'v2.3.109' => 0x18); new_valueInt(HelmToggleReverse => 0x18, 'v2.3.109' => 0x19); new_valueInt(RequestFullUpdate => 0x19, 'v2.3.109' => 0x1a); new_valueInt(TogglePerspective => 0x1a, 'v2.3.109' => 0x1b); new_valueInt(OldClimbDive => 'v2.1.5' => 0x1b, 'v2.3.109' => 0x1c, fmt => 'l<', comment0 => {qw(-1 climb 1 dive)}, ); # ++++++++++++++++++++ auto-downgrade? new_valueInt(ClimbDive => 0x1b, 'v2.1.5' => 0x1c, 'v2.3.109' => 0x1d, fmt => 'l<', comment0 => {qw(-1 climb 1 dive)}, ); new_valueInt(FighterLaunch => 'v2.3' => 0x1d, 'v2.3.109' => 0x1e); new_valueInt(FighterShoot => 'v2.3' => 0x1e, 'v2.3.109' => 0x1f); new_valueInt(EmergencyJump => 'v2.3.109' => 0x20, comment0 => {qw( 0 forward 1 backward )}, ); new_valueInt(SingleSeatSelect => 'v2.5.106' => 0x21, # AKA SetFighterSettingsPacket unpacker => sub { # Complete # For selecting shuttle / bomber / fighter / etc when customising the ship. my ($unk1,$hullID,$rest) = unpack('VV a*', shift); print "SingleSeatSelect($unk1,\n"; while ($hullID) { my ($bay, @strings) = unpack('V VXXXXV/v VXXXXV/v VXXXXV/v a*', $rest); # strings: Name (provided by player), hullRace, broadType (as seen in vesselData.xml) $rest = pop @strings; print " SingleSeatRec($hullID, $bay, ", ToUTF16(@strings), "), # ", ($ShipType{$hullID} // 'UNKNOWN_SHIP'), "\n"; ($hullID, $rest) = unpack('V a*', $rest); } print ");\n"; }, packer => sub { # Complete my $unk1 = shift // 0; c2s pack('VV V', 0x4c821d3c,0x21, $unk1).join('', @_)."\0\0\0\0"; }, ); sub SingleSeatRec { # _c2s return pack('VV a*', @_); } new_valueInt(BioBeaconControl => 'v2.6.204' => 0x22, fmt => 'V CC', comment1 => { 0 => 'Typhon', # GMMONSTER_CLASSIC 1 => 'Whale', # GMMONSTER_WHALE 2 => 'Shark', # GMMONSTER_SHARK 3 => 'Dragon', # GMMONSTER_DRAGON 4 => 'Pira', # GMMONSTER_PIRANHA 5 => 'Charybdis', # GMMONSTER_TUBE 6 => 'Nsect', # GMMONSTER_BUG 7 => 'Jelly', # GMMONSTER_JELLY # 8 => 'Wreck', # GMMONSTER_DERELICT - can't be beaconed though }, comment2 => {qw( 0 attract 1 repel )}, ); new_valueInt(ClientHeartbeat => 'v2.7.5' => 0x24); sub PopValueInt { %valueInt = ( _name => 'valueInt' ); for my $ver (sort {$a cmp $b} keys %valueIntVer) { next unless $PROTOVERSION ge $ver; for my $subt (sort {$b <=> $a} keys %{$valueIntVer{$ver}}) { my $name = $valueIntVer{$ver}{$subt}; delete $valueInt{$valueInt{$name}//-1}; next unless $subt >= 0; $valueInt{$name} = $subt; $valueInt{$subt} = $main::{"_$name"}; } } if ($ENV{DEBUG}) { for (sort {$valueInt{$a} <=> $valueInt{$b}} grep {! /^\d|^_/} keys %valueInt) { print "# valueInt{$_} = $valueInt{$_}\n"; } } } sub _commsMessage { # AKA CommsOutgoingPacket +++ Unknown my ($type, $to, $msg, $target, $unk5) = unpack('V V V V V', shift); # unk5 believed to be junk. $target believed to be junk when it's not a target. # either junk often looks UTF16-ish (vvvv instead of VV) my $typec = qw(player enemy station other)[$type] // 'unknown'; my $msgc = { # to player: '0:0' => 'Yes', '0:1' => 'No', '0:2' => 'Help!', '0:3' => 'Greetings', '0:4' => 'Die!', '0:5' => 'We\'re leaving the sector. Bye', '0:6' => 'Ready to go', '0:7' => 'Please follow us', '0:8' => 'We\'ll follow you', '0:9' => 'We\'re badly damaged', '0:10' => 'We\'re headed back to the station', '0:11' => 'Sorry, please disregard', # to enemy: '1:0' => 'Will you surrender?', '1:1' => 'Taunt #1', '1:2' => 'Taunt #2', '1:3' => 'Taunt #3', # to station: '2:0' => 'Stand by for docking (friend) OR Cease operation (foe)', '2:1' => 'Please report status.', '2:2' => 'Build homing missiles', '2:3' => 'Build nukes', '2:4' => 'Build mines', '2:5' => 'Build EMPs', '2:6' => 'Build PShocks', '2:7' => 'Build beacons', # 2.6.3 '2:8' => 'Build probes', # 2.6.3 '2:9' => 'Build tags', # 2.6.3 # to other: '3:0' => 'Hail', '3:1' => 'Turn to heading 0', '3:2' => 'Turn to heading 90', '3:3' => 'Turn to heading 180', '3:4' => 'Turn to heading 270', '3:5' => 'Turn L 10 degrees', '3:6' => 'Turn R 10 degrees', '3:7' => 'Attack nearest enemy', '3:8' => 'Proceed to your destination', '3:9' => "Go defend [$target]", '3:15' => 'Turn L 25 degrees', '3:16' => 'Turn R 25 degrees', }->{"$type:$msg"} // '???'; print "commsMessage($type,$to,$msg, $target,$unk5); # $typec [$to] $msgc\n"; } $parsers{0x574c4c4b} = \&_commsMessage; sub CommsOutgoingPacket { commsMessage(@_); } # old name sub commsMessage { c2s pack('V VVVVV', 0x574c4c4b, @_); } ###################################################################### $parsers{0x69cc01d9}{_name} = 'valueFourInts'; # c2s sub _EngSetCoolant { # ++++ WIP, Somme Unknowns :-/ my ($system, $value, $unk1, $unk2) = unpack('VVVV', shift); $system = $ShipSystems{$system} // "BAD('SYSTEM',$system)"; print "EngSetCoolant($system=>$value"; if ($unk1) { print ",$unk1"; } if ($unk2) { print $unk1 ? ",$unk2" : ",,$unk2"; } print ");\n"; } $parsers{0x69cc01d9}{0x00} = \&_EngSetCoolant; sub EngSetCoolant { my ($system, $value, $unk1, $unk2) = @_; $system //= 0; if ($system =~ /^[a-z]/i) { $system = $ShipSystems{$system} } c2s pack('VV VVVV', 0x69cc01d9,0x00, $system, $value//0, $unk1//0, $unk2//0); } # $parsers{0x69cc01d9}{0x01} ? my %OrdnanceType = ( Homing => 0, Nuke => 1, Mine => 2, EMP => 3, PShock => 4, Beacon => 5, Probe => 6, Tag => 7, ); SelfInvert(\%OrdnanceType); sub _LoadTube { # Complete +++ except UNKnowns my ($tube, $ordnance, $unk1, $unk2) = unpack('VVVV', shift); my $ordname = $OrdnanceType{$ordnance}; if ($ordname) { print "LoadTube($tube,'$ordname'"; } else { print "LoadTube($tube,BAD('ORDNANCE',$ordnance)"; } if ($unk1) { print ",$unk1"; } if ($unk2) { print $unk1 ? ",$unk1,$unk2" : ",,$unk2"; } print ");\n"; } $parsers{0x69cc01d9}{0x02} = \&_LoadTube; sub LoadTube { my ($tube, $ordnance, $unk1, $unk2) = @_; $ordnance //= 0; if ($ordnance =~ /^[a-z]/i) { $ordnance = $OrdnanceType{$ordnance} // 0 } c2s pack('VV VVVV', 0x69cc01d9,0x02, $tube//0, $ordnance, $unk1//0, $unk2//0); } sub _ConvertTorpedo { # Unknowns my ($dir, $unk1, $unk2, $unk3) = unpack('f< VVV', shift); # +++ parse FLOAT (!?!) $dir into something more meaningful? # 0 = Torp to Ene, 1 = Ene to Torp if ($unk1 || $unk2 || $unk3) { print "ConvertTorpedo($dir, $unk1,$unk2,$unk3);\n"; } else { print "ConvertTorpedo($dir); # ", $dir ? "Ene to Torp\n" : "Torp to Ene\n"; } } $parsers{0x69cc01d9}{0x03} = \&_ConvertTorpedo; sub ConvertTorpedo { c2s pack('VV f< VVV', 0x69cc01d9,0x03, @_); } sub _EngSendDamcon { # Complete my ($team, $x,$y,$z) = unpack('V VVV', shift); print "EngSendDamcon($team=>$x,$y,$z);\n"; } $parsers{0x69cc01d9}{0x04} = \&_EngSendDamcon; sub EngSendDamcon { c2s pack('VV V*', 0x69cc01d9,0x04, @_); } # $parsers{0x6aadc57f} = controlMessage ? sub _plainTextGreeting { # AKA Welcome. Complete my ($len, $str) = unpack('V a*', shift); # Oddly str NOT UTF16! my $strl = length($str); print 'plainTextGreeting(', Data::Dumper::qquote($str), ')'; print ".BAD('plainTextGreeting_len $len!=$strl')" unless ($strl == $len); print ";\n"; } $parsers{0x6d04b3da} = \&_plainTextGreeting; sub Welcome { plainTextGreeting(@_); } # old name sub plainTextGreeting { my $str = shift // 'You have connected to Thom Robertson\'s Artemis Bridge Simulator. Please connect with an authorized game client.'; s2c pack('V V A*', 0x6d04b3da, length($str), $str); # Oddly NOT UTF16! } # As seen on "Upgrades" screen, and found in strings in Artemis.exe our @Upgrades = qw( InfusionPCoils HydrogenRam TauronFocusers CarpactionCoils PolyphasicCapacitors CoolantReserves LateralArray ECMStarpulse DoubleAgent WartimeProduction InfusionPCoilsPERM ProtonicVerniers TauronFocusersPERM RegenerativePauGrids VeteranDamConTeams CetrociteHeatsinks TachyonScanners GridscanOverload OverrideAuthorization ResupplyImperatives PatrolGroup FastSupply VanguardRefitHelm VanguardRefitWeap VanguardRefitSci VanguardRefitBase VanguardRefitEng VanguardRefitAll ); our %ObjTypes; sub PopObjTypes { %ObjTypes = ( # ./deadbeef2hex.pl $FILE | grep ^f93d8080xx | ./marfez.pl 0x00 => ['EOL' ], ObjPlayerShip() => ['PlayerShip', # byte 1: Target=>'V', Impulse=>'f<', # -1 to 1 Rudder=>'f<', # 0 to 1 MaxImpulse=>'f<', # 0.001 to 1.4 TurnRate=>'f<', # 0.0006 to 0.048 AutoBeams=>'C', # 0 or 1 WarpFactor=>'C', # 0 to 4 Energy=>'f<', # byte 2: ShieldsUp=>'v', # 0 or 1. WHY 2 bytes? # See https://github.com/artemis-nerds/protocol-docs/issues/94 $PROTOVERSION lt '2.3' ? ( ShipNum=>'V' ) # 1=Artemis 2=Intrepid 3=Aegis SEE ShipNum BELOW : ( Unknown22=>'V'), # Almost always 1, except when it's 0? hullID=>'V', # +++ probably enumerate via %ShipType? 0 .. 141 - uniqueID in vesselData.xml X=>'f<', Y=>'f<', Z=>'f<', Pitch=>'f<', # -pi to +pi Roll=>'f<', # -pi to +pi # byte 3: Heading=>'f<', # -6.1 to +6.1, but usually -pi to +pi? Velocity=>'f<', # -3 to +60 $PROTOVERSION lt '2.7' ? ( InNebula=>'v' ) : ( InNebula=>'C' ), # 0, 1, (2, 3, others) set non-0 to limit to warp 1 Name=>'V/v', ForeShields=>'f<', ForeShieldsMax=>'f<', AftShields=>'f<', AftShieldsMax=>'f<', # byte 4: LastDockedBase=>'V', RedAlert=>'C', # 0 or 1 Unknown43=>'f<', # always 200000? MainScreenView=>'C', # 0..6 +++ Enumerate as seen in SetMainScreen BeamFreq=>'C', # 0..4 AvailableCoolant=>'C', # and/or fighter missiles? 0..20 ScienceTarget=>'V', CaptainTarget=>'V', # byte 5: DriveType=>'C', # 0 or 1 +++ enumerate ScanningID=>'V', ScanningProgress=>'f<', Reverse=>'C', # 0 or 1 $PROTOVERSION ge '2' ? ( ClimbDive=>'f<', # -1 to +1 ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/146 Side=>'C', # 2 or 3 - Thanks @Starry! # ++++++++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/163 : ShowOnMaps=>'V', # 0, -1, 4, 6, 0x4000000[46] etc... for Player's ship to show on Sci/CapMap ) : (), $PROTOVERSION ge '2.3' ? ( ShipNum=>'C', # ShipNum 0=Artemis 1=Intrepid 2=Aegis etc. 255=fighter? ++++++++++ # byte 6: CapShipID=>'V' ) : (), $PROTOVERSION ge '2.3.109' ? ( AccentHue=>'f<', # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/118 EmergencyJumpTime=>'f<' # 0..1 ) : (), $PROTOVERSION ge '2.6.204' ? ( # seen in 2,6,204. See BioBeaconControl beaconMonsterType=>'C', beaconEffect=>'C' ) : (), ], ObjWeapCons() => ['WeapCons', # +++ ASSUMING VERSION 2.1.5 or greater: # 2.1.5: f93d8080 02 ea030000 ffff7f 08 03 05 00 7b00000000000000000000000041655aff3f6456ff3f6355ff00000000000002020365656300000000 Homings=>'C', Nukes=>'C', Mines=>'C', EMPs=>'C', # Quantity, 0 to N $PROTOVERSION ge '2.2' ? ( PShocks=>'C') : (), $PROTOVERSION ge '2.6.204' ? ( Beacons=>'C', Probes=>'C', Tags=>'C' ) # seen in 2,6,204 and 2,3,0 : ( Unknown16=>'C' ), # was previously always 0? 200? OR IGNORABLE? # Can be ignored unless loading/unloading... LoadTime1=>'f<', LoadTime2=>'f<', LoadTime3=>'f<', LoadTime4=>'f<', LoadTime5=>'f<', LoadTime6=>'f<', # 0=unloaded 1=loaded 2=loading 3=unloading +++ enumerate TubeUsed1=>'C', TubeUsed2=>'C', TubeUsed3=>'C', TubeUsed4=>'C', TubeUsed5=>'C', TubeUsed6=>'C', # Ordnance Type - can be ignored if unloaded? ++++ ENUM TubeCont1=>'C', TubeCont2=>'C', TubeCont3=>'C', TubeCont4=>'C', TubeCont5=>'C', TubeCont6=>'C', ], ObjEngCons() => ['EngCons', # 0.0 to 1.0: BeamsHeat=>'f<', TorpsHeat=>'f<', SenseHeat=>'f<', ManuvHeat=>'f<', ImpulHeat=>'f<', DriveHeat=>'f<', FShldHeat=>'f<', AShldHeat=>'f<', # 0.0 to 1.0: BeamsEner=>'f<', TorpsEner=>'f<', SenseEner=>'f<', ManuvEner=>'f<', ImpulEner=>'f<', DriveEner=>'f<', FShldEner=>'f<', AShldEner=>'f<', # Coolants, 0..8 BeamsCool=>'C', TorpsCool=>'C', SenseCool=>'C', ManuvCool=>'C', ImpulCool=>'C', DriveCool=>'C', FShldCool=>'C', AShldCool=>'C', # Some docs have a bunch of "Unknown" at this point, # assume someone was trying to explain the empty byte in the bitfield # (8N-bit fields take N+1 bytes with an empty byte) ], ObjUpgrades() ? ( ObjUpgrades() => ['Upgrades', # 28 bits for 28 BYTES for "Upgrade In Use right now" (0 or 1) map({ $_ . 'InUse' => 'C' } @Upgrades), # 28 bits for 28 BYTES for "Upgrade Quantity In Stock" (0 to 5?) map({ $_ . 'Stock' => 'C' } @Upgrades), # 28 bits for 28 SHORTS for "Upgrade Countdown Timer" whilst in use (0 to 300?) map({ $_ . 'Timer' => 'v' } @Upgrades), # total 84 of 88 bits used (11 byte bitfield!) ]) : (), ObjEnemy() ? (ObjEnemy() => ['Enemy', # v1.x, similar to (not quite same as) NPCShip # bit 1.X: Name=>'V/v', Throttle=>'f<', Rudder=>'f<', # 0..1 MaxImpulse=>'f<', MaxTurnRate=>'f<', # tiny to huge vals seen ++++++++ check IsEnemy=>'V', # sometimes 0, 1, but usually 5??? hullID=>'V', # vesselData.xml vessel uniqueID. 1001..7002 seen X=>'f<', # bit 2.X: Y=>'f<', Z=>'f<', Pitch=>'f<', # usually 0, seen as high as 1.57 Roll=>'f<', # -1 to +1 Heading=>'f<', # -pi to +pi Velocity=>'f<', # 0..21 seen Surrendered=>'C', # 0 or 1 Unknown28=>'v', # ++++++++++++++++++++ InNebula? 0, 1, (2, 3), others. # bit 3.X: ForeShields=>'f<', ForeShieldsMax=>'f<', AftShields=>'f<', AftShieldsMax=>'f<', Unknown35=>'v', # usually 1, occasionally 0 FleetNum=>'C', # These next 3 are 2,4,6(,8),0x4000000[02468] for different SIDES who have scanned / can see # Use 1<'V', DoubleScan=>'V', # ++++++++++++++++++++ TBC Unknown41 => 'C', Unknown42 => 'v', BeamsDamg=>'f<', TorpsDamg=>'f<', SenseDamg=>'f<', ManuvDamg=>'f<', ImpulDamg=>'f<', DriveDamg=>'f<', FShldDamg=>'f<', AShldDamg=>'f<', # 0..1 ShFreqA=>'f<', ShFreqB=>'f<', ShFreqC=>'f<', ShFreqD=>'f<', ShFreqE=>'f<', # 0..1 ]) : (), ObjNPCShip() => ['NPCShip', # (enemy or neutral/civilian) # bit 1.X: Name=>'V/v', Throttle=>'f<', Rudder=>'f<', # 0..1 MaxImpulse=>'f<', MaxTurnRate=>'f<', # tiny to huge vals seen ++++++++ check IsEnemy=>'V', # sometimes 0, 1, but usually 5??? hullID=>'V', # vesselData.xml vessel uniqueID. 1001..7002 seen X=>'f<', # bit 2.X: Y=>'f<', Z=>'f<', Pitch=>'f<', # usually 0, seen as high as 1.57 Roll=>'f<', # -1 to +1, often really tiny Heading=>'f<', # -pi to +pi Velocity=>'f<', # 0..21 seen $PROTOVERSION ge '2' ? ( Surrendered=>'C') : (), # 0 or 1 $PROTOVERSION ge '2.7' ? ( Unknown28=>'C' ) # 0 or 1, occasionally others. : ( InNebula=>'v' ), # 0, 1, 2, 3, others assumed to be corruption # bit 3.X: ForeShields=>'f<', ForeShieldsMax=>'f<', AftShields=>'f<', AftShieldsMax=>'f<', Unknown35=>'v', # usually 1, occasionally 0 FleetNum=>'C', # +++ EliteAbility actually a bitfield - see mission-file-docs.txt $PROTOVERSION ge '2' ? ( EliteAbility=>'V', EliteInUse=>'V' ) : (), # bit 4.X: $PROTOVERSION lt '2' ? ( # ++++++++++++++++++++ TBC Unknown41 => 'C', ) : ( # These next 3 are 2,4,6(,8),0x4000000[02468] for different SIDES who have scanned / can see # Use 1<'V', DoubleScan=>'V', ShowOnMaps=>'V', # science captains lrs etc - thanks @Starry! Side=>'C', # 1, 2, sometimes 3, 30? Use 1<'C', Unknown46=>'C', # ++++++++++++++++++++ might be signed, -6 .. +5? Unknown47=>'C', # usually 1, occasionally 0 # ++++++++ mentioned in https://github.com/artemis-nerds/protocol-docs/issues/93 targetPointX=>'f<', # Usually -100000,0,0. RARELY becomes an X/Y/Z triplet # bit 5.X: targetPointY=>'f<', targetPointZ=>'f<', ), # ++++++++ mentionned in https://github.com/artemis-nerds/protocol-docs/issues/93 $PROTOVERSION ge '2.6.204' ? ( Tagged=>'C' ) : (), # seen in 2,6,204. 0..1, "Has been Tagged"? # ++++++++ mentionned in https://github.com/artemis-nerds/protocol-docs/issues/93 $PROTOVERSION ge '2.6.3' ? ( Unknown54=>'C' ) : (), # Always 0? BeamsDamg=>'f<', TorpsDamg=>'f<', SenseDamg=>'f<', ManuvDamg=>'f<', ImpulDamg=>'f<', DriveDamg=>'f<', FShldDamg=>'f<', AShldDamg=>'f<', # 0..1 ShFreqA=>'f<', ShFreqB=>'f<', ShFreqC=>'f<', ShFreqD=>'f<', ShFreqE=>'f<', # 0..1 ], ObjBase() => ['Base', Name=>'V/v', # +++++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/164 Shields=>'f<', MaxShields=>'f<', Index=>'V', hullID=>'V', # +++ enumerate? 1000..1008 X=>'f<', Y=>'f<', Z=>'f<', # Y always 0? Pitch=>'f<', Roll=>'f<', # ALWAYS 0? Heading=>'f<', Unknown24=>'V', # ALWAYS 0? Unknown25=>'C', # ALWAYS 0? Side=>'C', # 1, 2, (3, 4) +++++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/164 ], ObjMine() => ['Mine', # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/120 X=>'f<', Y=>'f<', Z=>'f<', # Y typ -500 .. +500 ], ObjAnomaly() => ['Anomaly', X=>'f<', Y=>'f<', Z=>'f<', $PROTOVERSION lt '2.1.5' ? ( Name => 'V/v', # ANOM? ) : ( # >= 21.5 pickupType=>'V', # +++ enumerate? see mission-file-docs.txt ++++++++++ BUT Anomaly.pickupType=9 (Space Debris) seen! ), # ++++++++ mentioned in https://github.com/artemis-nerds/protocol-docs/issues/93 $PROTOVERSION ge '2.3' ? ( Scanned=>'V', # 0 or 4? Mask like SingleScanned/DoubleScanned - reveal the anomaly type Unknown16=>'V' # always 0? ) : (), # ++++++++ mentionned in https://github.com/artemis-nerds/protocol-docs/issues/93 $PROTOVERSION ge '2.6.3' ? ( beaconMonsterType=>'C', beaconEffect=>'C' ) : (), # seen in 2,6,204. See BioBeaconControl ], ObjNebula() => ['Nebula', X=>'f<', Y=>'f<', Z=>'f<', # Y=0? R=>'f<', G=>'f<', B=>'f<', # Really RGB, used on 3D sccreens $PROTOVERSION ge '2.7' ? (nebType => 'C') : (), # AKA "Flavor", 1=purple 2=blue-ish 3=yellow ], ObjTorpedo() => ['Torpedo', # f93d8080 # 0a 27070000 3f 41fc6747 00000000 2d6a1147 8d25bf3d cdcccc3e 00000000 # 0a 39070000 3f b4221f47 00000000 e0df0f47 cdcccc3e cdcccc3e 00000000 # 0a 4b070000 3f 33365b47 00000000 2c140047 cdcccc3e 00000000 fa62c93e # 0a 5d070000 3f 089c5947 00000000 a4772447 00000000 cdcccc3e 8d25bf3d # 00000000 # 1.70000004768372 payload X =>'f<', Y =>'f<', Z =>'f<', dX=>'f<', dY=>'f<', dZ=>'f<', # -1 to 1 # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/121 $PROTOVERSION lt '2.3' ? ( OrdnanceType =>'V', # 0 to 4 +++ enumerate Unknown18=>'V', # Always 0(?) ) : ( # ge 2.3 Unknown17=>'V', # Always 11(?) OrdnanceType =>'V', # 0 to 7 +++ enumerate ), ], ObjBlackHole() => ['BlackHole', # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/120 X=>'f<', Y=>'f<', Z=>'f<', # Y always 0? ], ObjAsteroid() => ['Asteroid', # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/120 X=>'f<', Y=>'f<', Z=>'f<', ], ObjGenericMesh() => ['GenericMesh', # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/8 # ++++++++++ re-test GenericMesh in 2.7.0 - they are now scannable # ++++++++++ and likely to include set_ship_text X=>'f<', Y=>'f<', Z=>'f<', Unknown14=>'V', Unknown15=>'V', Unknown16=>'V', # All 0? roll=>'f<', # changes a LOT? pitch=>'f<', angle=>'f<', rollDelta=>'f<', pitchDelta=>'f<', angleDelta=>'f<', Name=>'V/v', meshFileName=>'V/v', textureFileName=>'V/v', pushRadius=>'f<', # often 200.0? blocksShotFlag=>'C', # Various values seen - NOT just boolean? artScale=>'f<', # 0.1 to 0.15 seen ColorRed=>'f<', ColorGreen=>'f<', ColorBlue=>'f<', # 0.0 .. 1.0 fakeShieldsFront=>'f<', fakeShieldsRear=>'f<', # often -1? Unknown38=>'C', # 0, 31, 32, 64, 181 seen Unknown41=>'V/v', Unknown42=>'V/v', # spaces? # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/118 $PROTOVERSION ge '2.7' ? ( Scanned=>'V' ) : (), # changes 0->4 after a scan # hullRace? hullType? hasFakeShldFreq? ], ObjXtal() ? ( ObjXtal() => [ 'Xtal', # v1.x only. X=>'f<', Y=>'f<', Z=>'f<', Name=>'V/v', # Always '???'? ]) : (), ObjCreature() ? ( ObjCreature() => ['Creature', X=>'f<', Y=>'f<', Z=>'f<', Name=>'V/v', # Always "ZZ" in < v2.1.5? $PROTOVERSION ge '2.1.5' ? ( Heading=>'f<', # -pi to pi Pitch=>'f<', # -0.5 .. +0.5 Roll=>'f<', # 0 to 1000? CreatureType=>'V', # +++ 0..8 - should enumerate :-/ # +++++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/142 # ++++++++++++++++++++ Confirm Creature Unknown21..26 by scanning? SingleScan=>'V', # -1, 0, 2, 4, 6 seen. 0x4000000[026]? Unknown22=>'V', # 0, sometimes 4 (Side?) DoubleScan? # ALL these look wrong - floats? ints? bitmasks? : Unknown23=>'V', Unknown24=>'V', Unknown25=>'V', Unknown26=>'V', # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/56 ) : (), $PROTOVERSION ge '2.3' ? ( Health=>'f<', MaxHealth=>'f<', # yeah honestly no health before 2.3.0? ) : (), # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/93 and 56 # +++++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/142 $PROTOVERSION ge '2.6.3' ? ( Age=>'C' ) : (), # 1=Young 2=Mature, 3=Ancient # +++++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/200 $PROTOVERSION ge '2.7' ? ( ShowOnMaps=>'V' ) : (), # -1, 0, 4, or other values # +++++++++++++++ Needs work - mission-file-docs.txt ? # +++++++++++++++ Whale pod number somewhere? # https://github.com/artemis-nerds/protocol-docs/issues/56 ]) : (), ObjWhale() ? ( ObjWhale() => [ 'Whale', Name=>'V/v', Unknown12 => 'V', Unknown13 => 'V', X=>'f<', Y=>'f<', Z=>'f<', Pitch=>'f<', Roll=>'f<', Heading=>'f<', Unknown22=>'f<', Unknown23=>'f<', Unknown24=>'f<', Unknown25=>'f<', ]) : (), ObjDrone() ? (ObjDrone() => ['Drone', # +++ ASSUMING VERSION 2.1.5 or greater: Unknown11=>'V', # 15, 30, sometimes 60, occasionally 20 X=>'f<', Y=>'f<', # ++++++++++ UPDATE PROTO-DOCS Z=>'f<', Unknown15=>'f<', # Always 0? Unknown16=>'f<', # 0.01 to 745.3 Heading=>'f<', # -pi to +pi. Possibly ignored by client? Side=>'V', # Usually 1, sometimes 2 # NEVER SEEN - FIX PROTODOCS? Unknown21=>'f<', # ++++++++++++++++++ Confirm ObjDrone fields - look dodgy ]) : (), );} =item B<--noobject> | B<--noemptyobject> Suppress ALL objectBitStream (AKA ObjectUpdate) packet decodes, or suppress EMPTY objectBitStream packet decodes. =item B<--switchobj> Allow objectBitStreams to switch object type partway through a multi-update packet. Official B never seem to do this, they often send multiple objects in one objectBitStream but they will be all the same TYPE... However, official B seem to accept it, so this option is here for B, @Starry! :-) =cut opt('object|obj!' => 2); opt('noemptyobject' => sub { $opt{object} = 1; }); opt('switchobj|objswitch!' => 0); =item B<--objstats> After all is said and done, spew a big bunch of stats about each object and its fields / values. NOTE: Will not work well with B<--noobject>! Example: # STAT: 804826 object packets parsed # STAT: 953 Anomaly (16 fully-pop) # STAT: 944 Anomaly.X (min 14289.529296875 max 85925.171875) # STAT: 937 Anomaly.Y (min -7.55591678619385 max 438.670043945312) # STAT: 944 Anomaly.Z (min 17280.49609375 max 86166.875) # STAT: 23 Anomaly.Upgrade (min 1 max 8) # STAT: 5 Anomaly.Upgrade = 1 (0x1) # STAT: 4 Anomaly.Upgrade = 2 (0x2) # STAT: 3 Anomaly.Upgrade = 3 (0x3) # STAT: 2 Anomaly.Upgrade = 4 (0x4) # STAT: 4 Anomaly.Upgrade = 7 (0x7) # STAT: 5 Anomaly.Upgrade = 8 (0x8) # STAT: 25 Anomaly.Unknown15 (min 0 max 4) # STAT: 11 Anomaly.Unknown15 = 0 (0x0) # STAT: 14 Anomaly.Unknown15 = 4 (0x4) # STAT: 16 Anomaly.Unknown16 (min 0 max 0) # STAT: 16 Anomaly.BeaconType (min 0 max 3) # STAT: 14 Anomaly.BeaconType = 0 (0x0) # STAT: 2 Anomaly.BeaconType = 3 (0x3) # STAT: 16 Anomaly.BeaconDir (min 0 max 0) =cut opt('objstats!' => 0); =item B<--objdocs> Dump a sort of abbreviated version of L and quit. Usually you'll want something like: --protover 2.7.0 --objdocs =cut opt('objdocs' => sub { _PROTOVERSION( $opt{protoversion} ); print "OBJECT DOCS FOR PROTOCOL VERSION $opt{protoversion}:\n"; for my $type (sort {$a <=> $b} keys %ObjTypes) { my ($objName, @fields) = @{$ObjTypes{$type}}; printf "\n### Object Type 0x%02x %s (%d bit field = %d bytes)\n", $type, $objName, @fields/2, @fields ? (@fields + 16)/16 : 0; my ($byte, $bit, $name, $type) = (1,1); while (($name, $type, @fields) = @fields) { $name =~ s/^Unknown\d+$/Unknown/; $type = { 'C' => 'byte', 'V' => 'int', 'v' => 'short', 'f<' => 'float', 'V/v' => 'string', }->{$type} // "UNKNOWN TYPE $type"; print " $name (bit $byte.$bit, $type)\n"; $bit++; if ($bit>8) { $byte++; $bit=1; } } } exit 0; } ); sub _objectBitStream { # AKA ObjectUpdate ++++ Special floats are a bit hacky # skip all objects? return unless $opt{object}; my $rest = shift; # skip empty objects? return if ($opt{object}==1) && (unpack('C', $rest) == 0); # --noemptyobj print "objectBitStream(\n"; my ($prevtype, $type, $id, $as_int, @vals, $this_obj); # 0x000000-terminated LIST (size not known in advance) # COULD be treated as 0x00-terminated with extra 0x00 0x00 0x00? :-/ # ... of Artemis::Object, exact type TBD? $stats{_count}++ if $opt{objstats}; while (defined($rest)) { ($type, $rest) = unpack('C a*', $rest); if (! $type) { if ($rest && $rest eq "\0\0\0") { print " ObjEOL()\n"; } else { print ' ', ToTODO('BAD:Obj_EOL', "\0".($rest // "")); } last; } unless ($opt{switchobj}) { $prevtype //= $type; if ($prevtype != $type) { # This shouldn't ever happen AFAICT? # Usually server sends all the same ObjectType (or just ObjEOL) # ... but CLIENTS seem to accept switching, so @Starry wanted... print ' ', ToTODO('ObjTypeSwitched', pack('C', $type) . $rest), ");\n"; return; } } my $fields = $ObjTypes{$type}; unless ($fields) { print ' ', ToTODO("UnknownObject$type", pack('C', $type)), ",", ToTODO('UnknownObjectDATA', $rest), "\n);\n"; return; } my ($objName, @fields) = @{$fields}; my $bits = scalar(@fields) / 2; # swallow up "spare" bits to nearest byte BUT 24 bits fill 4 bytes not 3: # https://github.com/artemis-nerds/protocol-docs/issues/75 $bits = 1 + ($bits | 7) if $bits; ($id, $bits, $rest) = unpack("V b$bits a*", $rest); print " Obj$objName($id"; my $dat; if ($opt{objstats}) { $stats{$objName}{_count}++; $dat = $stats{$objName}{ID}; $dat->{_max} //= $id; $dat->{_max} = $id if $id > $dat->{_max}; $dat->{_min} //= $id; $dat->{_min} = $id if $id < $dat->{_min}; } # BITFIELD-controlled LIST of some fields # (size not known in advance) my $gotone = 0; my $fullypop = 1; # until shown otherwise: while (@fields) { my $fname = shift @fields; my $ftype = shift @fields; my $want = substr($bits,0,1,''); unless ($want) { $fullypop = 0; next; } last unless length($rest) > 4; $gotone = 1; ($as_int, @vals) = unpack("V XXXX $ftype a*", $rest); $rest = pop @vals; if ($opt{objstats}) { $dat = $stats{$objName}{$fname} //= {}; $dat->{_count}++; } if ($ftype =~ m|^V/v|) { # special handling for strings if ($opt{objstats}) { my $val = Data::Dumper::qquote ( join(' ', UTF16_strs($as_int,@vals)) // 'undef' ); $dat->{$val}++; } @vals = ToUTF16($as_int, @vals); } elsif ($ftype eq 'f<') { my $val = $vals[0]; # some "special NaN" floats don't convert well to Perl if ($as_int == 0xffc00000) { @vals = "'NaN:0xffc00000'"; $dat->{NaN}++ if $opt{objstats}; } elsif (pack ('f<', "$val") eq pack('V', $as_int)) { # this one re-encodes properly. Keep it! if ($opt{objstats}) { $dat->{_max} //= $val; $dat->{_max} = $val if $val > $dat->{_max}; $dat->{_min} //= $val; $dat->{_min} = $val if $val < $dat->{_min}; } } elsif ($as_int == 0x80000000) { # special float "negative zero" re-packs OK as '-0' @vals = "'-0'"; if ($opt{objstats}) { $dat->{_max} //= 0; $dat->{_max} = 0 if 0 > $dat->{_max}; $dat->{_min} //= 0; $dat->{_min} = 0 if 0 < $dat->{_min}; } } else { # encode as a 'NaN:0x12345678' instead :-( @vals = sprintf "'NaN:0x%08x'", $as_int; $dat->{NaN}++ if $opt{objstats}; } } elsif ($opt{objstats}) { # other numeric types: my $val = $vals[0]; $dat->{_max} //= $val; $dat->{_max} = $val if $val > $dat->{_max}; $dat->{_min} //= $val; $dat->{_min} = $val if $val < $dat->{_min}; $dat->{$val}++; } print ",$fname=>$vals[0]"; $this_obj->{$fname} = $vals[0] if $this_obj; } print ',_EMPTY=>1' unless $gotone; print '),'; print "BAD('Obj_spare_bits $bits')," unless $bits =~ /^0*$/; if ($fullypop) { $stats{$objName}{_fullypop}++ if $opt{objstats}; $this_obj->{_fullypop}++ if $this_obj; print "# full\n"; } else { print "\n"; } } print ");\n"; # print ToTODO('SPARE', $rest) unless $rest eq ''; } $parsers{0x80803df9} = \&_objectBitStream; sub ObjectUpdate { objectBitStream(@_); } # old name sub objectBitStream { s2c pack('V', 0x80803df9).join('', @_); }; sub ObjGeneric { my ($type, $id, %fields) = @_; # Cheeky way to get ObjPlayerShip() (ObjectType=PlayerShip) etc: return $type unless defined $id; return ($type, $id) unless %fields; delete $fields{_EMPTY}; # Still here? We are encoding some fields: my ($name, @fields) = (@{ $ObjTypes{$type} // [ "UnknownObject$type" ]; }); my $want = ""; # will be the bitfield my $dat = ""; # will contain all the packed data # BITFIELD-controlled LIST of some fields (size not known in advance) while (@fields) { my $fname = shift @fields; my $ftype = shift @fields; my $val = delete $fields{$fname}; if (defined $val) { $want .= "1"; if ($ftype =~ m|^V/v| ) { $dat .= $val; # strings have already been packed by UTF16() } elsif (($ftype eq 'f<') && $val =~ /^NaN:0x(..)(..)(..)(..)/) { # special NaN, re-pack the hex in little-endian order: $dat .= pack('H2H2H2H2', $4, $3, $2, $1); } else { $dat .= pack($ftype, $val); } } else { $want .= "0"; } } if (%fields) { print STDERR "# Ignored invalid params in Obj$name($id", map(",$_=>$fields{$_}", sort keys %fields), ")\n"; } return pack('C V b*', $type, $id, $want.'0').$dat; } sub ObjMultiVer { my ($one, $two, $two15, @rest) = @_; return ObjGeneric( $PROTOVERSION lt '2' ? $one : $PROTOVERSION lt '2.1.5' ? $two : $two15, @rest); } sub ObjEOL { return "\0\0\0\0"; } # ObjMultiVer v1.70,v2.0, v2.1.5+ sub ObjPlayerShip { return ObjGeneric( 0x01, @_); } sub ObjWeapCons { return ObjGeneric( 0x02, @_); } sub ObjEngCons { return ObjGeneric( 0x03, @_); } sub ObjUpgrades { return ObjMultiVer(undef,undef,0x04, @_); } sub ObjEnemy { return ObjMultiVer(0x04, undef,undef,@_); } sub ObjNPCShip { return ObjMultiVer(0x05, 0x04, 0x05, @_); } sub ObjBase { return ObjMultiVer(0x06, 0x05, 0x06, @_); } sub ObjMine { return ObjMultiVer(0x07, 0x06, 0x07, @_); } sub ObjAnomaly { return ObjMultiVer(0x08, 0x07, 0x08, @_); } # ObjUnused { return ObjMultiVer(0x09, 0x08, 0x09, @_); } sub ObjNebula { return ObjMultiVer(0x0a, 0x09, 0x0a, @_); } sub ObjTorpedo { return ObjMultiVer(0x0b, 0x0a, 0x0b, @_); } sub ObjBlackHole { return ObjMultiVer(0x0c, 0x0b, 0x0c, @_); } sub ObjAsteroid { return ObjMultiVer(0x0d, 0x0c, 0x0d, @_); } sub ObjGenericMesh { return ObjMultiVer(0x0e, 0x0d, 0x0e, @_); } # ++++++++++++++++++++ v1.x GenericMesh TBC! sub ObjXtal { return ObjMultiVer(0x0f,undef,undef, @_); } # xtalline entity? v1.x only? sub ObjCreature { return ObjMultiVer(undef,0x0e, 0x0f, @_); } sub ObjWhale { return ObjMultiVer(0x10, 0x0f,undef, @_); } # v1.x actually "Xeno" sub ObjDrone { return ObjMultiVer(undef,0x10, 0x10, @_); } sub Stat { my ($num, $what, $comment) = @_; return unless $num; printf "# STAT: %7d %s%s\n", $num//0, $what, $comment ? " ($comment)" : ''; } sub ShowObjStats { return unless %stats; Stat(delete($stats{_count}), 'object packets parsed'); for my $type (sort keys %ObjTypes) { next unless $type; # skip ObjEOL my ($objName, @fields) = @{$ObjTypes{$type}}; my $fp = delete( $stats{$objName}{_fullypop} ); Stat( delete( $stats{$objName}{_count} ), $objName, $fp ? "$fp fully-pop": ''); while (@fields) { my $fname = shift @fields; my $ftype = shift @fields; my $dat = delete $stats{$objName}{$fname}; my @comm; my $count = delete $dat->{_count}; my $min = delete $dat->{_min}; push @comm, "min $min" if defined $min; my $max = delete $dat->{_max}; push @comm, "max $max" if defined $max; Stat( $count, " $objName.$fname", "@comm" ); my @keys = keys %$dat; if (@keys > 10) { # ++++++++++ Other values of "top 10"? if ($ftype eq 'V/v') { @keys = sort {($dat->{$b} cmp $dat->{$a}) || ($a cmp $b)} @keys; } else { @keys = sort {($dat->{$b} <=> $dat->{$a}) || ($a cmp $b)} @keys; } @keys = splice(@keys, 0, 10); # just show top 10 } else { @keys = sort @keys; # show all, sorted by VAL not FREQ } for (@keys) { my $val = $dat->{$_}; Stat( $val, " $objName.$fname = $_", ($ftype =~ /^[vCl]/ or $ftype eq 'V') ? sprintf("0x%x", $_) : '' ); } } } %stats = (); } sub _gmText { # AKA GameMasterMessage. Complete. my $dat = shift; # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/129 : # There are 3.5 variants, and it's ALMOST impossible to tell them apart # by examining first few bytes, so let's just try each, in approx order # of parseability / confidence, and see which parses it first :-( _gmText24($dat) || _gmText26($dat) || _gmText20($dat) || print 'gmText(', ToTODO('BAD:gmText', $dat), ");\n"; } sub _gmTextTSMCX { # all gmText have Type, Sender, Message. # Check + return these (+Comment +eXtra) or UNDEF: my $dat = shift; my ($type, @Sender) = unpack('V V v', $dat); my $comment = $ConsoleType{$type-1} or return; # check type return unless $Sender[0] && $Sender[0]<2000; # check sender length return unless ($Sender[0]==1) || (($Sender[1] >= 32) && ($Sender[1] <= 127)); # check it looks like ASCII ($type, @Sender) = unpack('V VXXXXV/v a*', $dat); $dat = pop(@Sender); my @Message = unpack('V v', $dat); return unless $Message[0] && $Message[0]<4000; # check message length return unless ($Message[0]==1) || (($Message[1] >= 32) && ($Message[1] <= 127)); # check it looks like ASCII @Message = unpack('VXXXXV/v a*', $dat); $dat = pop(@Message); return ($type, ToUTF16(@Sender), ToUTF16(@Message), $comment, $dat); } sub _gmText20 { # v2.0 (possibly before): # int type (0..5), string sender, string message my ($type, $sender, $message, $comment, $xtra) = _gmTextTSMCX(shift) or return; return if $xtra ne ''; return if $type > 10; # 3000 didn't exist in 2.0 print "gmText($type, $sender => $message); # v2.0 $comment\n"; return 1; # YAY! } sub _gmText24 { # v2.4: # byte recip[8] (0|1), # int type (0..5), string sender, string message my (@recip) = unpack('C8 a*', shift); my $dat = $recip[8] // return; # check recipients are 0 (no) or 1 (yes): @recip = grep { $_ < 2 } @recip[0..7]; return unless @recip == 8; # check + parse rest: my ($type, $sender, $message, $comment, $xtra) = _gmTextTSMCX($dat) or return; return if $xtra ne ''; return if $type > 10; # 3000 didn't exist in 2.4 # still here? Cool, print it! printf "gmText(0x0%d0%d0%d0%d,0x0%d0%d0%d0%d, %d, %s => %s); # v2.4 %s\n", @recip[3,2,1,0,7,6,5,4], $type, $sender, $message, $comment; return 1; # YAY! } sub _gmText26 { # v2.6: # int recip[0..n] (1000+), int EOL (0), # int type (0..5|3000), string sender, string message # OPTIONAL int priority my ($a, $dat) = unpack('V a*', shift); my $recip = $a; while ($a && defined($dat)) { return if $a < 1000; # this is ALMOST certainly NOT a shipID ($a, $dat) = unpack('V a*', $dat); $recip .= ",$a"; } my ($type, $sender, $message, $comment, $xtra) = _gmTextTSMCX($dat) or return; if ($xtra eq '') { print "gmText($recip, $type, $sender => $message); # v2.6 $comment\n"; return 1; # YAY! } ($a, $xtra) = unpack('V a*', $xtra); return if $xtra ne ''; # check no extra stuff my $pri = { 0x00 => ' General', 0x01 => ' Distress', # AKA Alert 0x02 => ' SideMission', # AKA Side 0x04 => ' StationStatus', # AKA Status 0x08 => ' Player', 0x10 => ' Station', 0x20 => ' Enemy', 0x40 => ' Friend', }->{$a} // ' UNKNOWN'; print "gmText($recip, $type, $sender => $message, $a); # v2.7 $comment$pri\n"; return 1; # YAY! } $parsers{0x809305a7} = \&_gmText; sub GameMasterMessage { gmText(@_); } # old name sub gmText { my $packet = ''; for (@_) { if (/^\d+$/) { $packet .= pack('V', $_); } else { $packet .= $_; } } c2s pack('V a*', 0x809305a7, $packet); } sub _bigMess { # Complete # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/119 # ++++ maybe https://github.com/artemis-nerds/protocol-docs/issues/130 my (@strs) = unpack('(VXXXXV/v)*', shift); print "bigMess(", ToUTF16(@strs), ");\n"; } $parsers{0x902f0b1a} = \&_bigMess; sub bigMess { s2c pack('V a*', 0x902f0b1a, @_); } sub _carrierRecord { # AKA FighterBayStatusPacket. Complete? my $dat = shift; my ($id, $bay, @nameclass, $refit); print "carrierRecord("; my $comma = ''; while (unpack('V', $dat)) { if ($PROTOVERSION ge '2.6') { ($id, $bay, @nameclass) = unpack('V V VXXXXV/v VXXXXV/v V a*', $dat); $dat = pop @nameclass; $refit = pop @nameclass; print $comma, "\n singleseat($id,$bay, ", ToUTF16(@nameclass), ", $refit)"; } else { ($id, @nameclass) = unpack('V VXXXXV/v VXXXXV/v V a*', $dat); $dat = pop @nameclass; $refit = pop @nameclass; print $comma, "\n singleseat($id, ", ToUTF16(@nameclass), ", $refit)"; } $comma = ','; } print $comma, ToTODO('BAD:carrierRecordEOL', $dat) if $dat ne "\0\0\0\0"; print ");\n"; } $parsers{0x9ad1f23b} = \&_carrierRecord; sub carrierRecord { s2c pack('V', 0x9ad1f23b).join('', @_)."\0\0\0\0"; } sub singleseat { # _s2c if (@_ > 3) { # version >= 2.6.0 my ($id, $bay, $nameclass, $refit) = @_; return pack('VV', $id, $bay) . $nameclass . pack('V', $refit); } else { # version < 2.6.0 my ($id, $nameclass, $refit) = @_; return pack('V', $id) . $nameclass . pack('V', $refit); } } # 0xae88e058 = incomingMessage # 63040000 00000000 64000000 0000 0000 0500 0000 0100 0000 f2030000 eb030000 00000000 00000000 00000000 00000000 # bf080000 08000000 04000000 4f00 5f00 5200 5900 5f00 5400 00000000 00000000 34703a47 109f6d41 4ed24047 00000000 sub _attack { # AKA BeamFired my $packet = shift; # +++++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/83 my ($id, $type, $rest) = unpack('VV a*', $packet); # $id - yes, beams have their own objectIDs # $type 0=enemy, 1=player, but also sometimes 4, 8, 9? if (($type==0) or ($type==1) or ($type==4)) { # version <= 2.2.0 my ( $dam, $port, $stype,$ttype, $sid,$tid, $x,$y,$z, $manual) = unpack('V V V V V V f< f< f< V', $rest); # $id 10XX++ # $type 0,1, 4 # $dam "nice" base10 ints like 100, 400, 1200, 100, 2800, 520, 200, 4500... # $port vesselData.xml entry, 0-indexed # $stype shooter type (1,5,6,15) # $ttype target type (1, 5,6,7, 11, 15,16) # $sid, $tid shooter and target Object IDs (1000++) # $x,$y,$z,$manual=1 if manually fired (x=-46..55, y=-13..16, z=-76..150) # 0,0,0,0 otherwise print "attack($id, $type, $dam, $port, $stype,$ttype, $sid,$tid, $x,$y,$z, $manual);\n"; } elsif ($type == 8) { # version <= 2.2.0 ++++++++ Many unknowns! my ( $un1, $un2, $un3, $un4, $un5, $un6, $un7, $un8, $un9, $x,$y,$z, $manual) = unpack('V v v v v v v V V f< f< f< V', $rest); # $id 10XX++ # $type 8 # $un1 2,3,4, 6 # $un2 (short) 45, 78, 79, 110 # $un3 (short) 45, 66, 84, 95, 101 # $un4 (short) 32, 45, 79, 82, 111 # $un5 (short) 32, 73, 77, 89, 110, 116 # $un6 (short) 46, 77,78,79, 95, 117 # $un7 (short) 78, 79, 84, 94, 114 # $un8, $un9 always 0? # $x,$y,$z, never 0? 16154..58291, -15..139, 25810..58736 # $manual=0 always? print "attack($id, $type, $un1, $un2, $un3, $un4, $un5, $un6, $un7, $un8, $un9, $x,$y,$z, $manual);\n"; } elsif ($type == 9) { # version 2.3.0+ my ( $sub, $port, $stype,$ttype, $un1, $sid,$tid, $x,$y,$z, $manual) = unpack('V V V V V V V f< f< f< V', $rest); # $id 10XX++ (.. 18234, wow!) # $type 9 # $sub 0 | 1 | 2 | 5 | 8 | 9 - SUBTYPE?? # $port 0..9 | 0..f | 0 | 0 | 0 | 0 # $stype 5,6 | 1 | 1 | 15 | 15 | 0 - shooter type # $ttype target type (0, 1, 5,6,7, 11,15,16) # $un1 0 | 0 | 0 | 0 | 0 | 2,3,4,6 # $sid, $tid shooter and target IDs | 0,0 # $x,$y,$z, $manual=1 if manually fired (x=-48..53, y=-18..48, z=-83..150) # 0,0,0,0 otherwise | x=17641..88735, y=-107..393, z=33353..82553, man=0 print "attack($id, $type,$sub, $port, $stype,$ttype, $un1, $sid,$tid, $x,$y,$z, $manual);\n"; } else { print ToTODO("Bad_attack_$type", $packet), "\n"; } } $parsers{0xb83fd2c4} = \&_attack; sub BeamFired { attack(@_); } # old name sub attack { my ($id, $type, @rest) = @_; $id //= 0; $type //= 0; if (($type==0) or ($type==1) or ($type==4)) { # version <= 2.2.0 s2c pack('V VV VV VV VV f= 2.3.0 s2c pack('V VV VV VV V VV f[0], "($id));\n"; } else { print "objectDelete(", ToTODO('BAD:objectDeleteType',pack('C',$type)), ",$id);\n"; print "# ObjTypes ", join(' ', sort keys %ObjTypes), "\n"; } } $parsers{0xcc5a3e30} = \&_objectDelete; sub DestroyObject { objectDelete(@_); } # old name sub objectDelete { s2c pack('V C V', 0xcc5a3e30, @_); } sub _commText { # AKA CommsIncoming. Complete # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/113 if ($PROTOVERSION ge '2.6') { # V became v? WAT?!? Sounds like this also became a BITMASK: my ($priority, @strings) = unpack('v VXXXXV/v VXXXXV/v', shift); print "commText($priority,", ToUTF16(@strings), ");\n"; } else { my ($priority, @strings) = unpack('V VXXXXV/v VXXXXV/v', shift); print "commText($priority,", ToUTF16(@strings), ");\n"; } } $parsers{0xd672c35f} = \&_commText; sub CommsIncoming { commText(@_); } # old name sub commText { if ($PROTOVERSION ge '2.6') { s2c pack('V v a*', 0xd672c35f, @_); } else { s2c pack('V V a*', 0xd672c35f, @_); } } sub _Version { # Officially "connected". I prefer "Version" :-p $unk? my ( $unk, $ver, $major, $minor, $patch) = unpack('V f< V V V', shift); if (defined $patch) { _PROTOVERSION( "$major.$minor" . ( $patch ? ".$patch" : '' ) ); print "Version($unk, $ver, $major,$minor,$patch); # PROTOVERSION=$PROTOVERSION\n"; } elsif ($ver ge '1.70') { _PROTOVERSION($ver); print "Version($unk, $ver); # PROTOVERSION=$PROTOVERSION\n"; } else { _PROTOVERSION($ver); print "Version($unk, $ver); # UNSUPPORTED PROTOVERSION=$PROTOVERSION\n"; } } $parsers{0xe548e74a} = \&_Version; sub connected { Version(@_); } # NEW NAME ... but I prefer "Version" :-p sub Version { my ($unk, $ver, $major,$minor,$patch) = @_; if (defined($patch)) { s2c pack('V V f< VVV', 0xe548e74a, $unk, $ver, $major,$minor,$patch); _PROTOVERSION( "$major.$minor" . ($patch ? ".$patch" : '')); } else { s2c pack('V V f<', 0xe548e74a, $unk//0, $ver//=0); _PROTOVERSION( $ver ); # Probably 2.0 OR WORSE. } } =item B<--nointel> Suppress objectText packet decodes (formerly known as "intel" packets) =cut opt('intel!' => 1); sub _objectText { # AKA Intel. return unless $opt{intel}; my ( $id, $type, @text) = unpack('V C VXXXXV/v', shift); my $desc = { # see mission-file-docs.txt#set_ship_text 0 => 'race', 1 => 'class', 2 => 'desc', 3 => 'scan_desc', # OR non-scripted missions? }->{$type} || 'UNKNOWN!'; print "objectText($id,$type,", ToUTF16(@text), "); # $type=$desc\n"; } $parsers{0xee665279} = \&_objectText; sub Intel { objectText(@_); } # old name sub objectText { my ($id, $type, $str) = @_; s2c pack('V V C', 0xee665279, $id//0, $type//0) . ($str//''); } =item B<--noheartbeat> | B<--nohb> Suppress heartbeat packet decodes =cut opt('heartbeat|hb!' => 1); sub _heartbeat { # Sent every 3sec during simulation return unless $opt{heartbeat}; print "heartbeat();"; if ($opt{times}) { my $timestamp = int(1000 * (time() - $^T)); # $^T is time program started print ' # @', $timestamp; } print "\n"; } $parsers{0xf5821226} = \&_heartbeat; sub heartbeat { s2c pack('V', 0xf5821226); } ###################################################################### $parsers{0xf754c8fe}{_name} = 'simpleEvent'; # s2c sub _Kaboom { # AKA DestroyObject2, UnknownPreDestroy, ExplosionPacket +++ Unknowns # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/167 # Mine: big deathstar-style spherical firework explosion with ring # Torpedo: nuke = big circular firework. emp = big blue puffy thing, others = big fireball # Creature: small blue sphere / puffy thing # Base: giant fireball # ++++++++++ Drone? NPCShip? my ($type, $id) = unpack('V V', shift); if (my $typedat = $ObjTypes{$type}) { print "Kaboom(Obj", $typedat->[0], "($id));\n"; } else { print "Kaboom(BAD('KaboomType',$type),$id);\n"; } } $parsers{0xf754c8fe}{0x00} = \&_Kaboom; sub UnknownPreDestroy { Kaboom(@_); } # older name sub DestroyObject2 { Kaboom(@_); } # old name sub Kaboom { s2c pack('VV VV', 0xf754c8fe,0x00, @_); } sub _Klaxon { # AKA RedAlertNoise. Complete print "Klaxon();\n"; } $parsers{0xf754c8fe}{0x01} = \&_Klaxon; sub RedAlertNoise { Klaxon(@_); } # old name sub Klaxon { s2c pack('VV', 0xf754c8fe,0x01); } sub _OldGameOverStats { # ++++ WIP my (@strs) = unpack('(VXXXXV/v)*', shift); print "OldGameOverStats(", ToUTF16(@strs), ");\n"; } $parsers{0xf754c8fe}{0x02} = \&_OldGameOverStats; sub OldGameOverStats { s2c pack('VV', 0xf754c8fe,0x02).join('', @_); } sub _SoundEffect { # Complete my (@filename) = unpack('VXXXXV/v', shift); print 'SoundEffect(', ToUTF16(@filename), ");\n"; } $parsers{0xf754c8fe}{0x03} = \&_SoundEffect; sub SoundEffect { s2c pack('VV a*', 0xf754c8fe,0x03, @_); } sub _Pause { # Complete my ($pause) = unpack('V', shift) // ''; print "Pause($pause);\n"; } $parsers{0xf754c8fe}{0x04} = \&_Pause; sub Pause { s2c pack('VV V*', 0xf754c8fe,0x04, @_); } sub _DamageShake { # Unknown my ($index, $duration) = unpack('V f<', shift); # $index is the index of the damaged ship (not ID, but 0=Artemis 1=Aegis etc) # $duration 0.529, 1.596, 1.764, 2.293, but usually 3 print "DamageShake($index,$duration);\n"; } $parsers{0xf754c8fe}{0x05} = \&_DamageShake; sub DamageShake { s2c pack('VV V f<', 0xf754c8fe,0x05, @_); } =item B<--quit>=I B<--end>=I Once an EndGame packet has been received, delete the I. This can signal to a B to quit. =cut opt('quit|end=s' => ''); sub _EndGame { # Complete +++++ BUT PREVIOUSLY KNOWN AS GameOver() print "EndGame();\n"; return unless $opt{quit}; print "# Deleting $opt{quit} to trigger ...\n"; unlink $opt{quit}; } $parsers{0xf754c8fe}{0x06} = \&_EndGame; sub EndGame { s2c pack('VV', 0xf754c8fe,0x06); } sub _Poof { # Complete my ($xi,$x, $yi,$y, $zi,$z) = unpack('VXXXXf< VXXXXf< VXXXXf<', shift); # render a green "poof" thing at x,y,z, possibly de-cloaking # special floats" again :-/ $x = "'-0'" if $xi == 0x80000000; $y = "'-0'" if $yi == 0x80000000; $z = "'-0'" if $zi == 0x80000000; print "Poof($x,$y,$z);\n"; } $parsers{0xf754c8fe}{0x07} = \&_Poof; sub Poof { s2c pack('VV f< f< f<', 0xf754c8fe,0x07, @_); } sub _Tension { # Complete? # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/150 my ($level) = unpack('f<', shift); # values of 0.0, 50.0, 100.0 my $comment = { 0 => ' # low', 50 => ' # med', 100 => ' # high', }->{$level} || ''; print "Tension($level);$comment\n"; } $parsers{0xf754c8fe}{0x08} = \&_Tension; sub Unknown_f754c8fe_08 { Tension(@_); } # old name sub UnknownDamageRelated { Tension(@_); } # older name sub Tension { s2c pack('VV f<', 0xf754c8fe,0x08, @_); } sub _SkyBox { # Complete my ($num) = unpack('V', shift) // ''; print "SkyBox($num);\n"; } $parsers{0xf754c8fe}{0x09} = \&_SkyBox; sub SkyBox { s2c pack('VV V*', 0xf754c8fe,0x09, @_); } sub _GameMessage { # Complete. AKA warning_popup_message in mission scripts my (@msg) = unpack('VXXXXV/v', shift); print 'GameMessage(', ToUTF16(@msg), ");\n"; } $parsers{0xf754c8fe}{0x0a} = \&_GameMessage; sub GameMessage { s2c pack('VV a*', 0xf754c8fe,0x0a, @_); } sub _EngAutoDamconUpdate { # Complete my ($auto) = unpack('V', shift) // ''; print "EngAutoDamconUpdate($auto);\n"; } $parsers{0xf754c8fe}{0x0b} = \&_EngAutoDamconUpdate; sub EngAutoDamconUpdate { s2c pack('VV V*', 0xf754c8fe,0x0b, @_); } # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/130 renaming? sub _JumpStart { print "JumpStart();\n"; } # Complete? $parsers{0xf754c8fe}{0x0c} = \&_JumpStart; sub JumpStart { s2c pack('VV', 0xf754c8fe,0x0c); } sub _JumpComplete { print "JumpComplete();\n"; } # Complete? $parsers{0xf754c8fe}{0x0d} = \&_JumpComplete; sub JumpComplete { s2c pack('VV', 0xf754c8fe,0x0d); } sub _Unknown_f754c8fe_0e { # ++++ WIP # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/109 CLOSED # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/77 my ($unk1,$unk2) = unpack('V f<', shift); # unk1 usually 0, unk2 0 to 0.887 print "Unknown_f754c8fe_0e($unk1,$unk2);\n"; } $parsers{0xf754c8fe}{0x0e} = \&_Unknown_f754c8fe_0e; sub Unknown_f754c8fe_0e { # ++++ WIP s2c pack('VV V f<', 0xf754c8fe,0x0e, @_); } sub _AllShipSettings { print "AllShipSettings(\n"; # Complete? my $rest = shift; # Fixed size LIST of (8) ship settings, or terminated by end of data? # COULD be regarded as an undef-terminated list? while (length $rest) { $rest = ToShipSetting($rest); } print ");\n"; } $parsers{0xf754c8fe}{0x0f} = \&_AllShipSettings; sub AllShipSettings { s2c pack('VV', 0xf754c8fe,0x0f).join('', @_) } sub ToShipSetting { my ($drive, $hullID, $col, $has_name, $rest, @name); if ($PROTOVERSION ge '2.3.1') { ($drive, $hullID, $col, $has_name, $rest) = unpack('V V f< V a*', shift); } elsif ($PROTOVERSION ge '2') { ($drive, $hullID, $has_name, $rest) = unpack('V V V a*', shift); } else { # $PROTOVERSION 1.X: ($drive, $hullID, $rest) = unpack('V V a*', shift); $has_name = 1; } if ($has_name) { # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/133 @name = unpack('VXXXXV/v a*', $rest); $rest = pop @name; } # $name has already been fed through ToUTF16() print " ShipSetting($drive,$hullID," . ($col // 'undef') . ', ' . ($has_name ? ToUTF16(@name) : 'undef') . '), # ' . ($DriveType{$drive} // 'BAD_DRIVE') # ++++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/165 . ' ' . ($ShipType{$hullID} // 'UNKNOWN_SHIP') . "\n"; return $rest; } sub ShipSetting { # Occasional c2s but mostly _s2c my ($drive, $hullID, $col, $name) = @_; if ($drive =~ /^[a-z]/i) { $drive = $DriveType{$drive} // 0 } if ($hullID =~ /^[a-z]/i) { $hullID = $ShipType{$hullID} // 0 } my $ret = pack('V V', $drive, $hullID); $ret .= pack('f<', $col) if defined $col; # v2.4.0 or later if (defined ($name) and $name ne '') { return $ret . ($PROTOVERSION ge '2' ? "\1\0\0\0" : '') . $name; } else { return $ret . ($PROTOVERSION ge '2' ? "\0\0\0\0" : ''); # missing a name? } } =item B<--nodmx> Suppress DMX message packets =cut opt('dmx!' => 1); sub _DmxMessage { # Complete return unless $opt{dmx}; my $dat = shift; my $len = unpack('V', $dat); return print 'print ', ToTODO('BAD:Broken_DmxMessage', $dat), ";\n" if $len > 100; my (@name) = ($len, unpack('V/v V', $dat)); my $bool = pop @name; # 'cos @name swallowed all print 'DmxMessage(', ToUTF16(@name), "=>$bool);\n"; } $parsers{0xf754c8fe}{0x10} = \&_DmxMessage; sub DmxMessage { my ($name, $bool) = @_; s2c pack('VV a* V', 0xf754c8fe,0x10, $name//'', $bool//0); } sub _KeyCaptureToggle { # ++++ Untested for $capture != 0 my ($capture) = unpack('C', shift); print "KeyCaptureToggle($capture);\n"; # +++++++++++++++ See 0x4c821d3c:0x16 } $parsers{0xf754c8fe}{0x11} = \&_KeyCaptureToggle; sub KeyCaptureToggle { s2c pack('VV C', 0xf754c8fe,0x11, @_); } sub _Perspective { # Complete? my ($perspective) = unpack('V', shift) // ''; print "Perspective($perspective); # Unknown if this is mislabled?\n"; } $parsers{0xf754c8fe}{0x12} = \&_Perspective; sub Perspective { s2c pack('VV V*', 0xf754c8fe,0x12, @_); } sub _Detonate { # Complete? my ($type,$id) = unpack('VV', shift); # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/111 # Small fireball if (my $typedat = $ObjTypes{$type}) { print "Detonate(Obj", $typedat->[0], "($id));\n"; } else { print "Detonate(", ToTODO('BAD:DetonateType',$type,$id), ");\n"; } } $parsers{0xf754c8fe}{0x13} = \&_Detonate; sub UnknownMaybeDetonate { Detonate(@_); } # old name sub Detonate { s2c pack('VV VV', 0xf754c8fe,0x13, @_); } sub _GameOverReason { # Complete my (@reasons) = unpack('(VXXXXV/v)*', shift); print 'GameOverReason(', ToUTF16(@reasons), ");\n"; my ($title, $result, $result2) = UTF16_strs(@reasons); $discord[1] = "**$title**" . ($result ? ": $result" : '') # sometimes NULL (see fighter results) . ($result2 ? ": $result2" : '') # usually not there . "\n```"; } $parsers{0xf754c8fe}{0x14} = \&_GameOverReason; sub GameOverReason { s2c pack('VV a*', 0xf754c8fe,0x14, @_); } sub _GameOverStats { # Complete my ($col, $rest) = unpack('C a*', shift); print "GameOverStats($col"; # bug with "no left column": @discord = ($discord[0], $discord[1]) unless $col; # +++++ 0xCE-terminated LIST (size SORTA not known in advance (?)) my $n = 2; my $fighters = 0; while (length $rest) { last if (unpack('C', $rest) == 0xCE); my ($unk1, $val, @name) = unpack('C l< VXXXXV/v a*', $rest); $rest = pop @name; print ",\n GameOverStat($unk1, $val, ", ToUTF16(@name), ')'; my $name = join ' ', UTF16_strs(@name); $name =~ s/ minutes /minutes/; if ($val < 0) { # magical score that means this is a sub-heading $discord[$n] ||= ' '; $discord[$n++] .= join(' ', ' ', $name); $fighters = 1; } elsif ($fighters) { # format fighters, better than Thom ;-) $discord[$n++] .= sprintf('%13s%13s', $name, $val); } elsif ($col) { # second column. right-justify $discord[$n++] = sprintf('%31s %27s %3s', $discord[$n]//'', $name, $val); } else { # first column. Just format. $discord[$n++] = sprintf('%27s %3s', $name, $val); } } print ");\n"; return unless $col; return if (@discord < 4) && $fighters; discord(join "\n", map($_//'', @discord, '```')); @discord = (); } $parsers{0xf754c8fe}{0x15} = \&_GameOverStats; sub GameOverStats { my $col = shift; s2c pack('VV C', 0xf754c8fe,0x15, $col//0) . join('', @_) . "\xCE"; } sub GameOverStat { return pack('C l< a*', @_); } # _s2c # $parsers{0xf754c8fe}{0x16} ? sub _FighterLaunched { # Complete my ($id) = unpack('V', shift); print "FighterLaunched($id);\n"; } $parsers{0xf754c8fe}{0x17} = \&_FighterLaunched; sub FighterLaunched { s2c pack('VV V', 0xf754c8fe,0x17, @_); } sub _SingleSeatDamageShake { # Complete! my ($id,$duration) = unpack('Vf<', shift); print "SingleSeatDamageShake($id,$duration);\n"; } $parsers{0xf754c8fe}{0x18} = \&_SingleSeatDamageShake; sub SingleSeatDamageShake { s2c pack('VV Vf<', 0xf754c8fe,0x18, @_); } sub _BiomechRage { # Complete? my ($a) = unpack('V', shift) // ''; # See https://github.com/artemis-nerds/protocol-docs/issues/105 # and https://github.com/artemis-nerds/protocol-docs/issues/198 # values of 0 (not hostile) (2? 3?) 4 (hostile) my $comment = { 0 => ' # Passive', 1 => ' # Nearly calm', 2 => ' # Calming down', 3 => ' # Still angry', 4 => ' # Angered', }->{$a} // ''; print "BiomechRage($a);$comment\n"; } $parsers{0xf754c8fe}{0x19} = \&_BiomechRage; sub Unknown_f754c8fe_19 { BiomechRage(@_); } # old name sub BiomechRage { s2c pack('VV V*', 0xf754c8fe,0x19, @_); } sub _ShipDocked { # Complete? my ($id) = unpack('V', shift) // ''; # id of the main player ship? print "ShipDocked($id);\n"; } $parsers{0xf754c8fe}{0x1a} = \&_ShipDocked; sub ShipDocked { # ++++ WIP s2c pack('VV V*', 0xf754c8fe,0x1a, @_); } sub _SmokePuff { # New in 2.6.0? my ($id, $a, $x, $y, $z) = unpack('V V f< f< f<', shift); # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/107 # $id of a player ship, # $a=0..7 (puff type? Ship System damaged?) ++++++++++ # $x,$y,$z Ship X,Y,Z print "SmokePuff($id, $a, $x, $y, $z);\n"; } $parsers{0xf754c8fe}{0x1b} = \&_SmokePuff; sub Unknown_f754c8fe_1b { SmokePuff(@_); } # old name sub SmokePuff { s2c pack('VV V V f< f< f<', 0xf754c8fe,0x1b, @_); } sub _fighterText { # ++++ WIP # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/115 my ($id, @msg) = unpack('V VXXXXV/v', shift); print "fighterText($id, ", ToUTF16(@msg), ");\n"; } $parsers{0xf754c8fe}{0x1c} = \&_fighterText; sub fighterText { # ++++ WIP my $id = shift//0; s2c pack('VV V', 0xf754c8fe,0x1c, $id).join('', @_); } sub _Tag { # ++++++++ https://github.com/artemis-nerds/protocol-docs/issues/108 # $unk1=0? 3?, @rest "Artemis" and a DATE "2017-11-19" :-O my ($id,$unk1,@rest) = unpack('V V VXXXXV/v VXXXXV/v', shift); print "Tag($id,$unk1,", ToUTF16(@rest), ");\n"; } $parsers{0xf754c8fe}{0x1d} = \&_Tag; sub Unknown_f754c8fe_1d { Tag(@_) } # old name sub Tag { s2c pack('VV V V a*', 0xf754c8fe,0x1d, @_); } sub _GameOver { # Complete BUT see EndGame() # ++++++++++ https://github.com/artemis-nerds/protocol-docs/issues/122 print "GameOver();\n"; } $parsers{0xf754c8fe}{0x1e} = \&_GameOver; sub GameOver { s2c pack('VV', 0xf754c8fe,0x1e); } ###################################################################### sub PopUntested { my ($prefix, $from) = @_; # populate the %untested hash - a list of packet types we have # NOT YET SEEN in this particular run. $from //= \%parsers; for my $type (sort keys %$from) { next unless $type =~ /^\d/; my $name = sprintf('%s0x%x', $prefix//'', $type); my $sub = $from->{$type}; if (ref($sub) eq 'HASH') { PopUntested($name.':', $sub); } elsif (ref($sub) eq 'CODE') { for my $subname (keys %main::) { next unless $sub eq $main::{$subname}; $subname =~ s/^_//; $untested{$name} = $subname; last; } die "Couldn't find name for $sub to PopUntested $name\n" unless $untested{$name}; } else { die "Don't know how to PopUntested $name $sub\n"; } } return if $prefix; PrintUntested(); } sub PrintUntested { # ... and then dump a list of what packets we have NOT YET seen. print join(' ', '# UNTESTED:', sort {lc($a) cmp lc($b)} values %untested), "\n"; # next dump in 5 secs unless otherwise specified: $opt{test} = time() + (shift // 5); } =item B<--hook> I Instead of parser.pl, the perl program we output should call I, which should presumably implement the same API, for any new functionality. =cut opt('hook=s' => 'parser.pl'); ###################################################################### # ACTUALLY DO STUFF: if (caller) { _PROTOVERSION( $opt{protoversion} ); # ... unless otherwise specified } else { GetOptions(\%opt, @opt) or pod2usage(-verbose=>0); # print usage if opt error pod2usage("$0: No files given.") if ((@ARGV == 0) && (-t STDIN)); my $in; for my $file (@ARGV ? @ARGV : '-') { if ($file =~ /\.gz$/) { open($in, "zcat '$file' |") or die "Unable to open $file: $!\n"; } elsif ($file =~ /\.xz$/) { open($in, "xzcat '$file' |") or die "Unable to open $file: $!\n"; } else { open($in, $file) or die "Unable to open $file : $!\n"; } print join "\n", '#!/usr/bin/perl -w', 'use strict;', 'use lib ".";', "require '$opt{hook}';", ($file eq '-') ? () : "# parsed from $file", '', ''; _PROTOVERSION( $opt{protoversion} ); PopUntested() if $opt{test}; if ($opt{hex} or $file =~ /\.hex(\.gz|\.xz|)/) { ReadHexFile($in); } else { ReadPackets($in); } # Leftovers at the end? for (keys %bufs) { next if $bufs{$_} eq ''; print 'print ', ToTODO('BAD:LEFTOVER', $bufs{$_}), ";\n"; } print "\n", '#'x70, "\n### EOF\n\n" if $opt{eof}; close($in); PrintUntested() if $opt{test}; ShowAllGrids() if $opt{grids}; ShowObjStats() if $opt{objstats}; } # MAY have already unlinked if we saw GameOver(), but if we saw EOF first... unlink $opt{quit} if $opt{quit} } 1; # If we are use()d =back =head1 RETURN VALUE returns 0 on success, 1 for warnings, 2 for errors =head1 ERRORS Filesystem errors will cause parser.pl errors, for example attempting tp parse a file that does not exist, or uncompress invalir gz/xz files. DEADBEEF packets bigger than B<--maxpacket> I<5000> have not been observed in the wild, and are considered a fatal protocol error (mostly as a safety-net). Other errors should be treated as bugs (see above), or (more likely) misunderstandings of new versions of the Artemis wire protocol. =head1 EXAMPLES perl -e 'require parser.pl; c2s(SetShip(2)); sleep 300;' \ | nc 127.0.0.1 2010 | ./parser.pl =head1 ENVIRONMENT $PROTOVERSION can override the default protocol version (as can B<--protoversion>) =head1 FILES At this time, the only files read by B are those provided on the command-line (which can include gz/xz compressed files and/or hexdumps) Future versions of parser.pl may expect to find Artemis's B and some of the files referenced therein, in particular, etc. =head1 BUGS Extensive tests are performed on each version of B to ensure it can correctly decode, and re-create, a number of captures from many versions of Artemis (2.2.0, 2.3.0, 2.4.0, 2.5.1, 2.6.0, 2.6.204 - let me know if you need support for others). Generally speaking, any inability to completely reproduce client-to-server or server-to-client streams, byte-for-byte, is considered to be a bug. Plenty of packets are unnkown. Plenty of parts of "known" packets are of unknown types and lengths. Fields once assumed to be integers have turned out to be floats upon further investigation. Objects in objectBitStream have a different number of fields in different versions. Object fields may be of unknown types or lengths. On the whole, most bugs are likely to be the result of (almost) undocumented protocol changes. That said, I'll usually gladly fix bugs, especially if you can provide example files which demonstrate / reproduce the problem. =head1 NOTES Parents of young, organic life forms are warned that towels can be harmful if swallowed in large quantities -- DNA, H2G2 =head1 SEE ALSO Based heavily on https://artemis-nerds.github.io/protocol-docs/ with many thanks to chrivers and the artemis-nerds. Help from AwesomeAiden, @Slate, @Starry, and probably others =head1 AUTHOR "Nosey" Nick Waterman of Nilex Eperl@noseynick.orgE L =head1 COPYRIGHT (C) Copyright 2015-2018 Nosey Nick Waterman. All wrongs righted, all rights reserved. =cut __END__ +++++++++++++++ MISC FROM @Starry: Interestingly mine explosion seems to be a client side thing Only discovered due a bug in my code There is a comment in the protocol docs as to what the maximum damage % is - the stock client starts glitching in terms of ui both at 100000% (when damcons start to repair it they drop to 99999%) Int32 max (it clearly is converted to a int for displaying) +++++++++++++++ @NoseyNick your parser incorrectly assumes that weapons console ordinance counts are unsigned they are in fact signed (the protocol documents say "byte" which in guess we figure is signed - it is signed)