Let's write an e-Amusement 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.

Groundwork

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.)

Basic code framework

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:

  1. Unpack the request
  2. Identify the handler for that method
  3. Call the handler
  4. Construction and pack the response

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.

Implementing handlers

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")

Start the game!

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.

It lives!

Extra endpoints

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")

Stub cardmng implementation

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")

Stub SDVX 4 implementation

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")