docs/templates/pages/server.html
2021-12-29 04:39:45 +00:00

286 lines
13 KiB
HTML

{% extends "base.html" %}
{% block body %}
<h1>Let's write an e-Amusement server!</h1>
<p>No, seriously. It's quite easy.</p>
<p>Before we start anything, let's figure out exactly what we <i>need</i> to implement in order to get games to start.
As it turns out, very little.</p>
<ul>
<li><code><a href="{{ROOT}}/proto/services.html#get">services.get</a></code></li>
<li><code><a href="{{ROOT}}/proto/pcbtracker.html#alive">pcbtracker.alive</a></code></li>
<li><code><a href="{{ROOT}}/proto/message.html#get">message.get</a></code></li>
<li><code><a href="{{ROOT}}/proto/facility.html#get">facility.get</a></code></li>
</ul>
<p>To make matters even easier, none of these endpoints require any functioning logic! It should be noted that to follow
along, however, you will need a functioning packet encoder and decoder.</p>
<h2 id="groundwork">Groundwork</h2>
<p>Before we get started, there are a few things we need to get out of the way. One potential elephant in the room is
how we tell games to use our server. You may have configured this thousands of times, or maybe this is your first
time. Head on over to <code>prop/ea3-config.xml</code>, and edit <code>ea3/network/services</code> to
<code>http://localhost:5000</code> (or whatever you want :P). If you can't find it, search for
<code>https://eamuse.konami.fun/service/services/services/</code> and swap that out (yes, they really felt the
need to repeat service 3 times).
</p>
<p>While we're in this file, we need to turn off a few services (for now). This is part of how we're able to start the
game with such a minimal server. Right at the bottom of the file there should be a <code>option</code> and
<code>service</code> block. Within these we want to turn off <code>pcbevent</code> and <code>package</code>. Totally
turning of e-Amusement will usually lead to the game refusing to start, and that's no fun anyway.
</p>
<p>We will turn these two back on later, but for now we want everything turned off. (<code>cardmng</code> and
<code>userdata</code> aren't used during statup, so don't matter.)
</p>
<h3 id="stub-code">Basic code framework</h3>
<p>I'm going to assume you already have a working packet processor. I have used an intentionally simple API for mine, so
hopefully it should be easy to follow along with code samples. In addition to that, to create a server we will need
a, well, server. I'm going to be using <code>flask</code>, because I'm using Python, but I'm going to minimise how
much flask-specific code I write, so this should really be applicable to any server. With that said, shall we
starting writing code?</p>
<pre>{% highlight "python" %}
from flask import Flask, request, make_response
app = Flask(__name__)
def handle(model, module, method):
ea_info = request.headers.get("x-eamuse-info")
compression = request.headers.get("x-compress")
compressed = compression == "lz77"
payload = b"" # TODO: This
response = make_response(payload, 200)
if ea_info:
response.headers["X-Eamuse-Info"] = ea_info
response.headers["X-Compress"] = "lz77" if compressed else "none"
return response
@app.route("//<model>/<module>/<method>", methods=["POST"])
def call(model, module, method):
return handle(model, module, method)
@app.route("/", methods=["POST"])
def index():
return handle(request.args.get("model"),request.args.get("module"), request.args.get("method"))
if __name__ == "__main__":
app.run(debug=True)
{% endhighlight %}</pre>
<p>This is all of the flask-specific code I'm going to be writing. It should be fairly simple to follow what it going on
here. From within <code>handle</code> we need to:</p>
<ol>
<li>Unpack the request</li>
<li>Identify the handler for that method</li>
<li>Call the handler</li>
<li>Construction and pack the response</li>
</ol>
<p>For me, that looks something like:</p>
<pre>{% highlight "python" %}
from utils.decoder import decode, unwrap
from utils.encoder import encode, wrap
from utils.node import create_root
methods = {}
# Populate methods
# Step 1.
call, encoding = decode(unwrap(request.data, ea_info, compressed))
# Step 2.
handler = methods[(module, method)]
# Step 3.
root = create_root("response")
handler(call, root)
# Step 4.
payload = wrap(encode(root, encoding), ea_info, compressed)
{% endhighlight %}</pre>
<p>At this point, you should be able to start the game and see a single request come in for the services method. This
endpoint is mandatory for anything else to happen, but if you're able to inspect that one request then you're on the
right track.</p>
<h2 id="handlers">Implementing handlers</h2>
<p>Now that the groundwork is in place, implementing handlers themselves should be a fairly easy task. The first handler
we need to implement is <code><a href="{{ROOT}}/proto/services.html#get">services.get</a></code>. You may have
noticed in the previous section, but this request is made <i>before</i> the network check is performed. Weird, but
okay. Referencing the spec, the response to this method should be a list of every service we support. Luckilly for
us, that's not very many right now. My code for this is as follows:</p>
<pre>{% highlight "python" %}
from utils.node import append_child
SERVICES_MODE = "operation"
SERVICE_URL = "http://localhost:5000"
SERVICES = {
"facility": SERVICE_URL,
"message": SERVICE_URL,
"pcbtracker": SERVICE_URL,
}
@handler("services", "get")
def services_get(call, resp):
services = append_child(resp, "services", expire="10800", mode=SERVICES_MODE, status="0")
for service in SERVICES:
append_child(services, "item", name=service, url=SERVICES[service])
{% endhighlight %}</pre>
<p><code>@handler</code> is a helper function I have defined that registers the function into the <code>methods</code>
dictionary.</p>
<p>Next on the menu is <code><a href="{{ROOT}}/proto/pcbtracker.html#alive">pcbtracker.alive</a></code>. If we were
implementing a full server, handling this would involve looking up the machine in our database, confirming if paseli
is allowed, and processing the request accordingly. Luckily for us, that's not what we're doing. We're going to just
echo back the enabled flag the machine operator has set.</p>
<pre>{% highlight "python" %}
@handler("pcbtracker", "alive")
def pcbtracker(call, resp):
ecflag = call[0].ecflag
append_child(
resp, "pcbtracker",
status="0", expire="1200",
ecenable=ecflag, eclimit="0", limit="0",
time=str(round(time.time()))
)
{% endhighlight %}</pre>
<p>Feel free to pause right now and implement a less trusting solution here. I just didn't particularly feel like it,
and the objective of this page is to get a bare-bones server running.</p>
<p>Our next method is <i>even</i> simpler. Again, we <i>should</i> be performing database queries to determine if there
are any new messages to send, but we don't, and there won't be!</p>
<pre>{% highlight "python" %}
@handler("message", "get")
def message(call, resp):
append_child(resp, "message", expire="300", status="0")
{% endhighlight %}</pre>
<p>Take a breather at this point. I'm really sorry, but the last endpoint we need to imeplement is
<code><a href="{{ROOT}}/proto/facility.html#get">facility.get</a></code>. This endpoint is neither simple not small.
Well... Okay. Let's cheat. Same deal as ever. We should be looking up all this information (in this instance, we
need to check the details about the physical arcade the machine is registered within) but we can hardcode it all.
Does much of this data make any sense? Nope. Does it actually get validated by the game? Not really.
</p>
<pre>{% highlight "python" %}
@handler("facility", "get")
def facility_get(call, resp):
facility = append_child(resp, "facility", status="0")
location = append_child(facility, "location")
append_child(location, "id", Type.Str, "")
append_child(location, "country", Type.Str, "UK")
append_child(location, "region", Type.Str, "")
append_child(location, "name", Type.Str, "Hello Flask")
append_child(location, "type", Type.U8, 0)
append_child(location, "countryname", Type.Str, "UK-c")
append_child(location, "countryjname", Type.Str, "")
append_child(location, "regionname", Type.Str, "UK-r")
append_child(location, "regionjname", Type.Str, "")
append_child(location, "customercode", Type.Str, "")
append_child(location, "companycode", Type.Str, "")
append_child(location, "latitude", Type.S32, 0)
append_child(location, "longitude", Type.S32, 0)
append_child(location, "accuracy", Type.U8, 0)
line = append_child(facility, "line")
append_child(line, "id", Type.Str, "")
append_child(line, "class", Type.U8, 0)
portfw = append_child(facility, "portfw")
append_child(portfw, "globalip", Type.IPv4, map(int, request.remote_addr.split(".")))
append_child(portfw, "globalport", Type.S16, request.environ.get('REMOTE_PORT'))
append_child(portfw, "privateport", Type.S16, request.environ.get('REMOTE_PORT'))
public = append_child(facility, "public")
append_child(public, "flag", Type.U8, 1)
append_child(public, "name", Type.Str, "")
append_child(public, "latitude", Type.S32, 0)
append_child(public, "longitude", Type.S32, 0)
share = append_child(facility, "share")
eacoin = append_child(share, "eacoin")
append_child(eacoin, "notchamount", Type.S32, 0)
append_child(eacoin, "notchcount", Type.S32, 0)
append_child(eacoin, "supplylimit", Type.S32, 100000)
url = append_child(share, "url")
append_child(url, "eapass", Type.Str, "www.ea-pass.konami.net")
append_child(url, "arcadefan", Type.Str, "www.konami.jp/am")
append_child(url, "konaminetdx", Type.Str, "http://am.573.jp")
append_child(url, "konamiid", Type.Str, "http://id.konami.jp")
append_child(url, "eagate", Type.Str, "http://eagate.573.jp")
{% endhighlight %}</pre>
<h2 id="start">Start the game!</h2>
<p>Go for it, you've earned it.</p>
<p>If you've done everything right, you should now be able to pass the network check during startup. If you get really
lucky, you might be able to insert coins... Yeah okay unfortunately we aren't <i>quite</i> done. It's quite
satisfying though getting to the title screen at least, right?</p>
<p>To unblock the coin mechanism we're going to want to enable the <code>pcbevent</code> option within
<code>ea3-config.xml</code>. Don't forget to also update your services endpoint to return a URL for
<code>pcbevent</code>. The handler is super simple, at least. (As ever, this should be doing database stuff--logging
in this case--but we're not bothering with that.)
</p>
<pre>{% highlight "python" %}
@handler("pcbevent", "put")
def pcbevent(call, resp):
append_child(resp, "pcbevent", status="0")
{% endhighlight %}</pre>
<p>For real, this time, we can start the game.</p>
<figure>
<img width="256" src="./images/game_started.png">
<figcaption>It lives!</figcaption>
</figure>
<h2 id="extra">Extra endpoints</h2>
<p>Remember how we also disabled <code>package</code>? We can go and enable that one too if we want. Assuming you don't
plan to offer OTA updates from your server, this endpoint ends up super simple too; just report nothing to download.
</p>
<pre>{% highlight "python" %}
@handler("package", "list")
def package_list(call, resp):
append_child(resp, "package", expire="600", status="0")
{% endhighlight %}</pre>
<h3 id="cardmng">Stub cardmng implementation</h3>
<p>As with other endpoints, we can get a "working" implementation of e-Amusement cards by returning some generic
hardcoded values. Check the reference if you want to properly implement these endpoints, because they aren't
terribly complex.</p>
<pre>{% highlight "python" %}
cardmng = handler("cardmng")
@cardmng("inquire")
def inquire(call, resp):
append_child(resp, "cardmng", binded="1", dataid="0000000000000000",
exflag="1", expired="0", newflag="0", refid="0000000000000000", status="0")
@cardmng("authpass")
def authpass(call, resp):
append_child(resp, "cardmng", status="0")
{% endhighlight %}</pre>
<h3 id="sdvx4">Stub SDVX 4 implementation</h3>
<p>Odds are implementing the <code>cardmng</code> endpoints got you past the card check, but then immediately into a
network error, as the game attempted to retrieve your game-specific profile. While I don't know the endpoints for
all games, I do know that SDVX 4's can be stubbed out quite simply (below). It should be noted that this works by
always returning "player is a new user" in the <code>sv4_load</code> handler, meaning we haven't really achieved
much here besides adding an bunch of extra steps players need to take before they can play the game.</p>
<pre>{% highlight "python" %}
game = handler("game")
@game("sv4_load")
def sv4_load(call, resp):
game = append_child(resp, "game", status="0")
append_child(game, "result", Type.U8, 1)
@game("sv4_load_m")
def sv4_load(call, resp):
game = append_child(resp, "game", status="0")
append_child(game, "music")
@game("sv4_load_r")
def sv4_load(call, resp):
append_child(resp, "game", status="0")
@game("sv4_frozen")
def sv4_load(call, resp):
append_child(resp, "game", status="0")
@game("sv4_new")
def sv4_load(call, resp):
append_child(resp, "game", status="0")
{% endhighlight %}</pre>
{% endblock %}