Contents | Transport layer | Packet format | Application Protocol | Let's write a server |
No, seriously. It's quite easy.
Before we start anything, let's figure out exactly what we need to implement in order to get games to start. As it turns out, very little.
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.
Quick tangent: If the words "Smart E-Amusement" ring a bell and have you curious, you may be interested in how that works.
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 prop/ea3-config.xml
, and edit ea3/network/services
to
http://localhost:5000
(or whatever you want :P). If you can't find it, search for
https://eamuse.konami.fun/service/services/services/
and swap that out (yes, they really felt the
need to repeat service 3 times).
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 option
and
service
block. Within these we want to turn off pcbevent
and package
. Totally
turning of e-Amusement will usually lead to the game refusing to start, and that's no fun anyway.
We will turn these two back on later, but for now we want everything turned off. (cardmng
and
userdata
aren't used during statup, so don't matter.)
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 flask
, 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?
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)
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 handle
we need to:
For me, that looks something like:
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)
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.
Now that the groundwork is in place, implementing handlers themselves should be a fairly easy task. The first handler
we need to implement is services.get
. You may have
noticed in the previous section, but this request is made before 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:
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])
@handler
is a helper function I have defined that registers the function into the methods
dictionary.
Next on the menu is pcbtracker.alive
. 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.
@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())) )
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.
Our next method is even simpler. Again, we should be performing database queries to determine if there are any new messages to send, but we don't, and there won't be!
@handler("message", "get") def message(call, resp): append_child(resp, "message", expire="300", status="0")
Take a breather at this point. I'm really sorry, but the last endpoint we need to imeplement is
facility.get
. 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.
@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")
Go for it, you've earned it.
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 quite done. It's quite satisfying though getting to the title screen at least, right?
To unblock the coin mechanism we're going to want to enable the pcbevent
option within
ea3-config.xml
. Don't forget to also update your services endpoint to return a URL for
pcbevent
. 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.)
@handler("pcbevent", "put") def pcbevent(call, resp): append_child(resp, "pcbevent", status="0")
For real, this time, we can start the game.
Remember how we also disabled package
? 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.
@handler("package", "list") def package_list(call, resp): append_child(resp, "package", expire="600", status="0")
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.
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")
Odds are implementing the cardmng
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 sv4_load
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.
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")