mirror of
https://gitea.tendokyu.moe/eamuse/docs.git
synced 2024-11-24 06:50:11 +01:00
289 lines
13 KiB
HTML
289 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Write a server{% endblock %}
|
|
{% 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>
|
|
<p><small>Quick tangent: If the words "Smart E-Amusement" ring a bell and have you curious, you may be interested in
|
|
<a href="{{ROOT}}/smartea.html">how that works</a>.</small></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 %} |