diff --git a/images/eventlog_ghidra.png b/images/eventlog_ghidra.png new file mode 100644 index 0000000..ed7069c Binary files /dev/null and b/images/eventlog_ghidra.png differ diff --git a/images/eventlog_ida.png b/images/eventlog_ida.png new file mode 100644 index 0000000..031fa09 Binary files /dev/null and b/images/eventlog_ida.png differ diff --git a/images/matching_request_ghidra.png b/images/matching_request_ghidra.png new file mode 100644 index 0000000..75fa5e5 Binary files /dev/null and b/images/matching_request_ghidra.png differ diff --git a/images/matching_request_ida.png b/images/matching_request_ida.png new file mode 100644 index 0000000..ab33ab0 Binary files /dev/null and b/images/matching_request_ida.png differ diff --git a/index.html b/index.html index bb63195..e7fba18 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - eAmuse API + e-Amusement API @@ -20,17 +20,22 @@ -

Benami/Konami eAmuse API

+

Benami/Konami e-Amusement API

Why?

I was curious how these APIs work, yet could find little to nothing on Google. There are a number of closed-source projects, with presumably similarly closed-source internal documentation, and a scattering of implementations of things, yet I couldn't find a site that actually just documents how the API works. If I'm going to have to reverse engineer an open source project (or a closed source one, for that matter), I might as well just go reverse engineer an actual game (or it's stdlib, as most of my time has been spent currently).

+

For the sake of being lazy, I'll probably end up calling it eAmuse more than anything else throughout these + pages. Other names you may come across include httac and xrpc. The latter are the + suite of HTTP functions used in the Bemani stdlib, and the name of their communication protocol they implement + at the application layer, but whenever someone refers to any of them in the context of a rhythm game, they will + be referring to the things documented here.

These pages are very much a work in progress, and are being written as I reverse engineer parts of the protocol. I've been asserting all my assumptions by writing my own implementation as I go, however it currently isn't sharable quality code and, more importantly, the purpose of these pages is to make implementation of one's - own code hopefully trivial.

+ own code hopefully trivial (teach a man to fish, and all that).

Sharing annotated sources for all of the games' stdlibs would be both impractical and unwise. Where relevant however I try to include snippets to illustrate concepts, and have included their locations in the source for if you feel like taking a dive too.

diff --git a/packet.html b/packet.html index 1c6fba0..370d5f4 100644 --- a/packet.html +++ b/packet.html @@ -63,7 +63,7 @@ can be a little confusing, remembering that this is encoding an XML tree can make it easier to parse.

To start with, let's take a look at the overall structure of the packets.

- +
@@ -123,19 +123,19 @@ - + - + - + - +
0
0x420x42 Compressed data
0x430x43 Compressed, no data
0x450x45 Decompressed data
0x460x46 Decompressed, no data
@@ -145,45 +145,45 @@ - - + + - - - + + + - - - - + + + + - - - - - + + + + + - - - - - + + + + + - - - - + + + +
E~EE~E Encoding name
0x200xDFASCII0x200xDFASCII
0x400xBFISO-8859-1ISO_8859-10xBFISO-8859-1ISO_8859-1
0x600x9FEUC-JPEUCJPEUC_JP0x600x9FEUC-JPEUCJPEUC_JP
0x800x7FSHIFT-JISSHIFT_JISSJIS0x800x7FSHIFT-JISSHIFT_JISSJIS
0xA00x5FUTF-8UTF80xA00x5FUTF-8UTF8
@@ -217,7 +217,7 @@

A tag definition looks like:

- +
@@ -264,7 +264,7 @@ used later when unpacking the main data, so we need not worry about it for now, but be warned it exists and is possibly the least fun part of this format.

-
0
+
@@ -815,7 +815,7 @@ for our bucket.

For example, imagine we write the sequence byte, int, byte, short, byte, int, short. The final output should look like:

-
ID
+
diff --git a/protocol.html b/protocol.html index c52a178..ed28946 100644 --- a/protocol.html +++ b/protocol.html @@ -57,13 +57,72 @@ </response>

With "0" being a successful status. Convention is to identify a specific method as module.method, and we'll be following this convention in this document too. There are - a lot of possible methods, so the rest of this document is a big reference for them all. There are a + a lot of possible methods, so the majority of this document is a big reference for them all. There are a number of generic methods, and a number of game specific ones. If you haven't clocked yet, I've been working on an SDVX 4 build for most of these pages, and each game also comes with its own set of game-specific methods. These are namespaces under the game.%s module and, in the case of SDVX 4, are all game.sv4_method. I may or may not document the SDVX 4 specific methods, but I've listed them here anyway for completeness.

+

Paths in the XML bodies are formatted using an XPath-like syntax. That is, status@/response gets the + status attribute from response, and response/eacoin/sequence would return + that node's value. +

+

NOTE: I am using the non-standard notation of <node* ... and + <node attr*="" ... to indicate that an attribute or node is not always present! Additionally, I + am going to use the notation of <node[]> to indicate that a node repeats. +

+ +
0
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaning
0Success
109No profile
110Not allowed
112Card not found (cardmng.inquire)
116Card pin invalid (cardmng.authpass)
+
+ How to reverse engineer these calls +

Turns out bemani have been quite sensible in how they implemented their code for creating structures, so it's + rather readable. That said, if you've been using Ghidra (like me!), this is the time to switch to IDA. I'll + let the below screenshots below speak for themselves: +

+ +
+ Ghidra + + +
+
+ IDA Pro + + +
+ +

I know which of these I'd rather use for reverse engineering (sorry, Ghidra)!

+
+ +

Possible XRPC requests

@@ -228,6 +287,7 @@

Response:

@@ -333,7 +393,22 @@

Request:

<call ...>
     <matching method="request">
-        placeholder
+        <matchtyp __type="s32" />
+        <matchgrp __type="s32" />
+        <matchflg __type="s32" />
+        <waituser __type="s32" />
+        <waittime __type="s32" />
+
+        <joinip __type="str" />
+        <localip __type="str" />
+        <localport __type="s32" />
+        <dataid __type="str" />
+        <gamekind __type="str" />
+        <locationid __type="str" />
+        <lineid __type="str" />
+        <locationcountry __type="str" />
+        <locationregion __type="str" />
+
     </matching>
 </call>

Response:

@@ -464,108 +539,115 @@

cardmng.inquire

Request:

<call ...>
-    <cardmng method="inquire" cardid="" cardtype="" update="" />
+    <cardmng method="inquire" cardid="" cardtype="" update="" model*="" />
+</call>
+

Response:

+
<response>
+    <cardmng status="status" refid="" dataid="" pcode=""  newflag="" binded="" expired=" ecflag="" useridflag="" extidflag="" lastupdate="" />
+</response>
+

If the cardid cannot be found, status should be set to 112 with no other + information return. Otherwise, we return information about the found card.

+ + + + + + + + + + + + + + + + + + + + + +
refidA reference to this card to be used in other requests
dataidAppears to be set the same as refid; presumably to allow different keys for game state vs + login details.
newflag1 or 0
bindedHas a profile ever been created for this game (or an older version, requiring a migration) + (1 or 0)
expiredDid we find
+ +

cardmng.getrefid

+

Request:

+
<call ...>
+    <cardmng method="getrefid" cardtype="" cardid=" newflag="" passwd="" model*="" />
+</call>
+

Response:

+
<response>
+    <cardmng status="status" refid="" dataid="" pcode="" />
+</response>
+ +

cardmng.bindmodel

+

Request:

+
<call ...>
+    <cardmng method="bindmodel" refid="" newflag="" model*="" />
+</call>
+

Response:

+
<response>
+    <cardmng status="status" dataid="" />
+</response>
+ +

cardmng.bindcard

+

Request:

+
<call ...>
+    <cardmng method="bindcard" cardtype="" newid="" refid="" model*="" />
 </call>

Response:

<response>
     <cardmng status="status" />
 </response>
-

cardmng.getrefid

-

Request:

-
<call ...>
-    <cardmng method="getrefid">
-        placeholder
-    </cardmng>
-</call>
-

Response:

-
<response>
-    <cardmng status="status">
-        placeholder
-    </cardmng>
-</response>
- -

cardmng.bindmodel

-

Request:

-
<call ...>
-    <cardmng method="bindmodel">
-        placeholder
-    </cardmng>
-</call>
-

Response:

-
<response>
-    <cardmng status="status">
-        placeholder
-    </cardmng>
-</response>
- -

cardmng.bindcard

-

Request:

-
<call ...>
-    <cardmng method="bindcard">
-        placeholder
-    </cardmng>
-</call>
-

Response:

-
<response>
-    <cardmng status="status">
-        placeholder
-    </cardmng>
-</response>
-

cardmng.authpass

Request:

<call ...>
-    <cardmng method="authpass">
-        placeholder
-    </cardmng>
+    <cardmng method="authpass" refid="" pass="" model*="" />
 </call>

Response:

<response>
-    <cardmng status="status">
-        placeholder
-    </cardmng>
+    <cardmng status="status" />
 </response>

cardmng.getkeepspan

Request:

<call ...>
-    <cardmng method="getkeepspan">
-        placeholder
-    </cardmng>
+    <cardmng method="getkeepspan" model*="" />
 </call>

Response:

<response>
-    <cardmng status="status">
-        placeholder
-    </cardmng>
+    <cardmng status="status" keepspan="" />
 </response>

cardmng.getkeepremain

Request:

<call ...>
-    <cardmng method="getkeepremain">
-        placeholder
-    </cardmng>
+    <cardmng method="getkeepremain" refid="" model*="" />
 </call>

Response:

<response>
-    <cardmng status="status">
-        placeholder
-    </cardmng>
+    <cardmng status="status" keepremain="" />
 </response>

cardmng.getdatalist

Request:

<call ...>
-    <cardmng method="getdatalist">
-        placeholder
-    </cardmng>
+    <cardmng method="getdatalist" refid="" model*="" />
 </call>

Response:

<response>
     <cardmng status="status">
-        placeholder
+        <item[]>
+            <mcode __type="str" />
+            <dataid __type="str" />
+            <regtime __type="str" />
+            <lasttime __type="str" />
+            <exptime __type="str" />
+            <expflag __type="u8" />
+        </item>
     </cardmng>
 </response>
@@ -587,32 +669,29 @@

package

+

package.list

Request:

<call ...>
-    <package method="list">
-        placeholder
-    </package>
+    <package method="list" pkgtype="pkgtype" model*="" />
 </call>
+

all is the only currently observed value for pkgtype

Response:

<response>
     <package status="status">
-        placeholder
+        <item[] url="" />
     </package>
 </response>
+

A list of all packages available for download.

package.intend

Request:

<call ...>
-    <package method="intend">
-        placeholder
-    </package>
+    <package method="intend" url="" model*="" />
 </call>

Response:

<response>
-    <package status="status">
-        placeholder
-    </package>
+    <package status="status" />
 </response>
@@ -620,29 +699,26 @@

userdata.read

Request:

<call ...>
-    <userdata method="read">
-        placeholder
-    </userdata>
+    <userdata method="read" card*="" model*="" label="" />
 </call>

Response:

<response>
-    <userdata status="status">
-        placeholder
+    <userdata status="status" time="">
+        <b[] __type="" />
     </userdata>
 </response>
+

__type here can be either bin or str

userdata.write

Request:

<call ...>
-    <userdata method="write">
-        placeholder
+    <userdata method="write" card="" time="" model*="" label*="" >
+        <b[] __type="str" />
     </userdata>
 </call>

Response:

<response>
-    <userdata status="status">
-        placeholder
-    </userdata>
+    <userdata status="status" />
 </response>
@@ -650,7 +726,7 @@

services.get

Request:

<call ...>
-    <services method="get">
+    <services method="get" model*="" >
         <info>
             <AVS2 __type="str">AVS2 version</AVS2>
         </info>
@@ -659,7 +735,7 @@
     

Response:

<response>
     <services expire="" method="get" mode="" status="status">
-        <item name="service" url="url" />
+        <item[] name="service" url="url" />
     </services>
 </response>

Known services are:

@@ -687,19 +763,30 @@
  • sidmgr
  • globby
  • -

    Most of these will usually just return the URL to the eAmuse server (or your fake one ;D). ntp is a notable exception, unless you're planning on reimplementing NTP. keepalive will likely alsop be a custom URL with query parameters pre-baked.

    +

    Most of these will usually just return the URL to the eAmuse server (or your fake one ;D). ntp is a + notable exception, unless you're planning on reimplementing NTP. keepalive will likely alsop be a + custom URL with query parameters pre-baked.

    +

    mode is one of operation, debug, test, or + factory. +

    pcbtracker

    pcbtracker.alive

    Request:

    <call ...>
    -    <pcbtracker method="alive" accountid="pcbid" ecflag="ecflag" hardid="hardware ID" softid="software ID" />
    +    <pcbtracker method="alive" model*="" hardid="" softid="" accountid="" agree="" ecflag="" />
     </call>
    +

    ecflag here is determining if the arcade operator allows the use of paseli on this machine.

    +

    agree@ and ecflag@ appear to either be totally non present, or present with a value of + "1", but then again I may be reading the code wrong, so take that with a pinch of salt. +

    Response:

    <response>
    -    <pcbtracker status="status" ecenable="" eclimit="" expire="" limit="" time="">
    +    <pcbtracker status="" time="" limit="" ecenable="" eclimit="" >
     </response>
    +

    As you might guess, ecenable@ is therefore the flag to determine if paseli is enabled (i.e. the + arcade operator and the server both allow its use).

    pcbevent

    @@ -707,14 +794,18 @@

    Request:

    <call ...>
         <pcbevent method="put">
    -        placeholder
    +        <time __type="time" />
    +        <seq __type="u32" />
    +        <item[]>
    +            <name __type="str" />
    +            <value __type="s32" />
    +            <time __type="time" />
    +        </item>
         </pcbevent>
     </call>

    Response:

    <response>
    -    <pcbevent status="status">
    -        placeholder
    -    </pcbevent>
    +    <pcbevent status="status" />
     </response>
    @@ -722,12 +813,13 @@

    message.get

    Request:

    <call ...>
    -    <message method="get" />
    +    <message method="get" model*="" />
     </call>

    Response:

    - TODO: Investigate this response more
    <response>
    -    <message expire="" status="status" />
    +    <message expire="" status="status">
    +        <item[] name="" start="" end="" data="" />
    +    </message>
     </response>
    @@ -735,31 +827,129 @@

    facility.get

    Request:

    <call ...>
    -    <facility method="get">
    -        placeholder
    -    </facility>
    +    <facility method="get" privateip*="" encoding*="" />
     </call>

    Response:

    <response>
    -    <facility status="status">
    -        placeholder
    +    <facility expire=""\ status="status">
    +        <calendar*>
    +            <year __type="s16" />
    +            <holiday __type="s16" />
    +        </calendar>
    +        <location>
    +            <id __type="str" />
    +            <country __type="str" />
    +            <region __type="str" />
    +            <name __type="str" />
    +            <type __type="u8" />
    +            <countryname __type="str" />
    +            <countryjname __type="str" />
    +            <regionname __type="str" />
    +            <regionjname __type="str" />
    +            <customercode __type="str" />
    +            <companycode __type="str" />
    +            <latitude __type="s32" />
    +            <longitude __type="s32" />
    +            <accuracy __type="u8" />
    +        </location>
    +        <line>
    +            <id __type="str" />
    +            <class __type="u8" />
    +        </line>
    +        <portfw>
    +            <globalip __type="ip4" />
    +            <globalport __type="s16" />
    +            <privateport __type="s16" />
    +        </portfw>
    +        <public>
    +            <flag __type="u8" />1</ flag>
    +            <name __type="str" />
    +            <latitude __type="str">0<latitude>
    +            <longitude __type="str">0<longitude>
    +        </public>
    +        <share>
    +            <eapass*>
    +                <valid __type="?" />
    +            </eapass>
    +            <eacoin>
    +                <notchamount __type="s32" />
    +                <notchcount __type="s32" />
    +                <supplylimit __type="s32">100000<supplylimit>
    +            </eacoin>
    +            <url>
    +                <eapass __type="str">www.ea-pass.konami.net<eapass>
    +                <arcadefan __type="str">www.konami.jp/am<arcadefan>
    +                <konaminetdx __type="str">http://am.573.jp<konaminetdx>
    +                <konamiid __type="str">http://id.konami.jp<konamiid>
    +                <eagate __type="str">http://eagate.573.jp<eagate>
    +            </url>
    +        </share>
         </facility>
     </response>
    - +

    I'm not totally sure what type share/eapass/valid is meant to be, but it's optional, so I'd + suggest just not bothering and leaving it out :).

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CountryCode
    Hong KongHK
    TaiwanTW
    KoreaKR
    USAUS
    ThailandTH
    IndonesiaID
    SingaporeSG
    PhillipinesPH
    MacaoMO
    JapanJP
    +

    globalip (and associated ports) shold be the IP:port of the cabinet.

    +

    region is used for Japan, and has the value JP-[prefecture] where prefecture ranges + from 1 through 47.

    +

    TODO: Compile the list of regions

    apsmanager

    apsmanager.getstat

    Request:

    <call ...>
    -    <apsmanager method="getstat">
    -        placeholder
    -    </apsmanager>
    +    <apsmanager method="getstat" model*="" />
     </call>

    Response:

    <response>
    -    <apsmanager status="status">
    -        placeholder
    -    </apsmanager>
    +    <apsmanager status="status" />
     </response>
    @@ -768,70 +958,71 @@

    Request:

    <call ...>
         <sidmgr method="create">
    -        placeholder
    +        <cardtype __type="str" />
    +        <cardid __type="str" />
    +        <cardgid __type="str" />
    +        <steal __type="u8" />
         </sidmgr>
     </call>

    Response:

    <response>
         <sidmgr status="status">
    -        placeholder
    +        <state __type="u32" />
    +        <e_count __type="u8" />
    +        <last __type="time" />
    +        <locked __type="time" />
    +        <sid __type="str" />
    +        <cardid_status __type="u8" />
    +        <refid __type="str" />
         </sidmgr>
     </response>

    sidmgr.open

    Request:

    <call ...>
    -    <sidmgr method="open">
    -        placeholder
    +    <sidmgr method="open" sid="" >
    +        <pass __type="str" />
         </sidmgr>
     </call>

    Response:

    <response>
         <sidmgr status="status">
    -        placeholder
    +        <state __type="u32" />
    +        <refid __type="str" />
    +        <locked __type="time" />
         </sidmgr>
     </response>

    sidmgr.touch

    Request:

    <call ...>
    -    <sidmgr method="touch">
    -        placeholder
    -    </sidmgr>
    +    <sidmgr method="touch" sid="" />
     </call>

    Response:

    <response>
    -    <sidmgr status="status">
    -        placeholder
    -    </sidmgr>
    +    <sidmgr status="status" />
     </response>

    sidmgr.branch

    Request:

    <call ...>
    -    <sidmgr method="branch">
    -        placeholder
    -    </sidmgr>
    +    <sidmgr method="branch" sid="" />
     </call>

    Response:

    <response>
    -    <sidmgr status="status">
    -        placeholder
    -    </sidmgr>
    +    <sidmgr status="status" />
     </response>

    sidmgr.close

    Request:

    <call ...>
    -    <sidmgr method="close">
    -        placeholder
    +    <sidmgr method="close" sid="" />
    +        <cause __type="u32" />
         </sidmgr>
     </call>

    Response:

    <response>
    -    <sidmgr status="status">
    -        placeholder
    -    </sidmgr>
    +    <sidmgr status="status" />
     </response>
    @@ -840,13 +1031,18 @@

    Request:

    <call ...>
         <dlstatus method="done">
    -        placeholder
    +        <url>
    +            <param __type="str" />
    +        </url>
    +        <name __type="str" />
    +        <size __type="s32" />
         </dlstatus>
     </call>
    +

    Response:

    <response>
         <dlstatus status="status">
    -        placeholder
    +        <progress __type="s32" />
         </dlstatus>
     </response>
    @@ -870,13 +1066,21 @@

    Request:

    <call ...>
         <eacoin method="checkin">
    -        placeholder
    +        <cardtype __type="str" />
    +        <cardid __type="str" />
    +        <passwd __type="str" />
    +        <ectype __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
         <eacoin status="status">
    -        placeholder
    +        <sequence __type="s16" />
    +        <acstatus __type="u8" />
    +        <acid __type="str" />
    +        <acname __type="str" />
    +        <balance __type="s32" />
    +        <sessid __type="str" />
         </eacoin>
     </response>
    @@ -884,27 +1088,32 @@

    Request:

    <call ...>
         <eacoin method="checkout">
    -        placeholder
    +        <sessid __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
    -    <eacoin status="status">
    -        placeholder
    -    </eacoin>
    +    <eacoin status="status" />
     </response>

    eacoin.consume

    Request:

    <call ...>
    -    <eacoin method="consume">
    -        placeholder
    +    <eacoin method="consume" esid="">
    +        <sessid __type="str" />
    +        <sequence __type="s16" />
    +        <payment __type="s32" />
    +        <service __type="s16" />
    +        <itemtype __type="str" />
    +        <detail __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
         <eacoin status="status">
    -        placeholder
    +        <acstatus __type="u8" />
    +        <autocharge __type="u8" />
    +        <balance __type="s32" />
         </eacoin>
     </response>
    @@ -912,27 +1121,27 @@

    Request:

    <call ...>
         <eacoin method="getbalance">
    -        placeholder
    +      <sessid __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
         <eacoin status="status">
    -        placeholder
    +        <acstatus __type="u8" />
    +        <balance __type="s32" />
         </eacoin>
     </response>

    eacoin.getecstatus

    Request:

    <call ...>
    -    <eacoin method="getecstatus">
    -        placeholder
    -    </eacoin>
    +    <eacoin method="getecstatus" />
     </call>

    Response:

    <response>
         <eacoin status="status">
    -        placeholder
    +        <ectype __type="str" />
    +        <ecstatus __type="u8" />
         </eacoin>
     </response>
    @@ -940,41 +1149,38 @@

    Request:

    <call ...>
         <eacoin method="touch">
    -        placeholder
    +        <sessid __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
    -    <eacoin status="status">
    -        placeholder
    -    </eacoin>
    +    <eacoin status="status" />
     </response>

    eacoin.opchpass

    Request:

    <call ...>
         <eacoin method="opchpass">
    -        placeholder
    +        <passwd __type="str" />
    +        <newpasswd __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
    -    <eacoin status="status">
    -        placeholder
    -    </eacoin>
    +    <eacoin status="status" />
     </response>
    -

    eacoin.opchecking

    +

    eacoin.opcheckin

    Request:

    <call ...>
    -    <eacoin method="opchecking">
    -        placeholder
    +    <eacoin method="opcheckin">
    +        <passwd __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
         <eacoin status="status">
    -        placeholder
    +        <sessid __type="str" />
         </eacoin>
     </response>
    @@ -982,27 +1188,54 @@

    Request:

    <call ...>
         <eacoin method="opcheckout">
    -        placeholder
    +        <sessid __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
    -    <eacoin status="status">
    -        placeholder
    -    </eacoin>
    +    <eacoin status="status" />
     </response>

    eacoin.getlog

    Request:

    <call ...>
         <eacoin method="getlog">
    -        placeholder
    +        <sessid __type="str" />
    +        <logtype __type="str" />
    +        <ectype __type="str" />
    +        <target __type="str" />
    +        <perpage __type="s16" />
    +        <page __type="s16" />
    +        <sesstype __type="str" />
         </eacoin>
     </call>

    Response:

    <response>
         <eacoin status="status">
    -        placeholder
    +        <processing __type="u8" />
    +        <topic>
    +            <sumdate __type="str" />
    +            <sumfrom __type="str" />
    +            <sumto __type="str" />
    +
    +            <today __type="s32" />
    +            <average __type="s32" />
    +            <total __type="s32" />
    +        </topic>
    +        <summary>
    +            <items __type="s32" />
    +        </summary>
    +        <history>
    +            <item[]>
    +                <date __type="str" />
    +                <consume __type="s32" />
    +                <service __type="s32" />
    +                <cardtype __type="str" />
    +                <cardno __type="str" />
    +                <title __type="str" />
    +                <systemid __type="str" />
    +            </item>
    +        </history>
         </eacoin>
     </response>
    @@ -1011,10 +1244,15 @@

    traceroute.send

    Request:

    <call ...>
    -    <traceroute method="send">
    -        placeholder
    +    <traceroute proto="" method="send">
    +        <hop[]>
    +            <valid __type="bool">
    +            <addr __type="ip4">
    +            <usec __type="u64">
    +        </hop>
         </traceroute>
     </call>
    +

    hop repeats for every hop (unsurprisingly)

    Response:

    <response>
         <traceroute status="status">
    diff --git a/styles.css b/styles.css
    index b7fbc90..13908aa 100644
    --- a/styles.css
    +++ b/styles.css
    @@ -1,6 +1,6 @@
     body {
    -    /* font-family: sans-serif; */
    -    line-height: 1.25;
    +    font-family: sans-serif;
    +    line-height: 1.35;
         max-width: 1000px;
         margin: 16px auto;
         color: #222;
    @@ -9,12 +9,14 @@ body {
     
     table {
         border-collapse: collapse;
    -    font-family: monospace;
         letter-spacing: .02em;
         max-width: 100%;
         overflow-x: auto;
         display: block;
     }
    +table.code {
    +    font-family: monospace;
    +}
     
     thead {
         font-weight: bold;
    @@ -24,9 +26,14 @@ thead {
     td {
         border: 1px solid #111;
         padding: 2px;
    -    text-align: center;
         min-width: 32px;
     }
    +table:not(.code) td {
    +    padding: 2px 6px;
    +}
    +table.code td {
    +    text-align: center;
    +}
     
     td a {
         display: block;