From 960cf73a048ff88a04abbe9bf9015c193c742672 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Tue, 30 Apr 2024 22:34:41 +0700 Subject: [PATCH 1/4] [allnet] Enable DFI-encoded responses --- core/allnet.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 5df4700..b03d168 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -132,7 +132,7 @@ class AllnetServlet: async def handle_poweron(self, request: Request): request_ip = Utils.get_ip_addr(request) pragma_header = request.headers.get('Pragma', "") - is_dfi = pragma_header is not None and pragma_header == "DFI" + is_dfi = pragma_header == "DFI" data = await request.body() try: @@ -276,20 +276,23 @@ class AllnetServlet: self.logger.info(msg) resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" self.logger.debug(f"Allnet response: {resp_dict}") - resp_str += "\n" - """if is_dfi: - request.responseHeaders.addRawHeader('Pragma', 'DFI') - return self.to_dfi(resp_str)""" + if is_dfi: + return PlainTextResponse( + content=self.to_dfi(resp_str) + b"\r\n", + headers={ + "Pragma": "DFI", + }, + ) return PlainTextResponse(resp_str) async def handle_dlorder(self, request: Request): request_ip = Utils.get_ip_addr(request) pragma_header = request.headers.get('Pragma', "") - is_dfi = pragma_header is not None and pragma_header == "DFI" + is_dfi = pragma_header == "DFI" data = await request.body() try: @@ -341,9 +344,14 @@ class AllnetServlet: await self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}") res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" - """if is_dfi: - request.responseHeaders.addRawHeader('Pragma', 'DFI') - return self.to_dfi(res_str)""" + + if is_dfi: + return PlainTextResponse( + content=self.to_dfi(res_str) + b"\r\n", + headers={ + "Pragma": "DFI", + }, + ) return PlainTextResponse(res_str) From 08891d085155004d6d1933e481fe95fc3ef40e37 Mon Sep 17 00:00:00 2001 From: zaphkito Date: Sun, 5 May 2024 05:41:14 +0000 Subject: [PATCH 2/4] mai2: some improve for DX earlier version and return game code in uri (#125) Attention: There are all talking about maimai DX and newer version, not Pre-DX dx and newer version request these but no used, they are just exist in game code, only found `oldServerUrl` used in SDEZ 1.00, this should also fix SDGA and SDGB try to visit `ServerUrl + movieServerUrl` although that just because of SEGA shit code tested work ![image](/attachments/f2c79134-4651-4976-8278-bbcf268f424a) Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/125 Co-authored-by: zaphkito Co-committed-by: zaphkito --- titles/chuni/index.py | 6 +++- titles/cm/base.py | 2 +- titles/mai2/base.py | 4 +-- titles/mai2/dx.py | 18 +++++++--- titles/mai2/index.py | 81 +++++++++++++++++++++++++------------------ 5 files changed, 69 insertions(+), 42 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index b102d5e..f781898 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -272,7 +272,11 @@ class ChuniServlet(BaseServlet): self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) - endpoint = endpoint.replace("C3Exp", "") if game_code == "SDGS" else endpoint + endpoint = ( + endpoint.replace("C3Exp", "") + if game_code == "SDGS" + else endpoint + ) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) diff --git a/titles/cm/base.py b/titles/cm/base.py index e4fd1cb..4a49832 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -51,7 +51,7 @@ class CardMakerBase: { "modelKind": 1, "type": 1, - "titleUri": f"{uri}/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/", + "titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/", }, # ONGEKI { diff --git a/titles/mai2/base.py b/titles/mai2/base.py index a75a94f..90597d1 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -26,10 +26,10 @@ class Mai2Base: self.date_time_format = "%Y-%m-%d %H:%M:%S" if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80: - self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/197/MaimaiServlet/" + self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/SDEY/197/MaimaiServlet/" else: - self.old_server = f"http://{self.core_config.server.hostname}/197/MaimaiServlet/" + self.old_server = f"http://{self.core_config.server.hostname}/SDEY/197/MaimaiServlet/" async def handle_get_game_setting_api_request(self, data: Dict): # if reboot start/end time is not defined use the default behavior of being a few hours ago diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 8d6e738..4423824 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -5,6 +5,7 @@ import json from random import randint from core.config import CoreConfig +from core.utils import Utils from titles.mai2.base import Mai2Base from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants @@ -15,6 +16,15 @@ class Mai2DX(Mai2Base): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX + # DX earlier version need a efficient old server uri to work + # game will auto add MaimaiServlet endpoint behind return uri + # so do not add "MaimaiServlet" + if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80: + self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/SDEY/197/" + + else: + self.old_server = f"http://{self.core_config.server.hostname}/SDEY/197/" + async def handle_get_game_setting_api_request(self, data: Dict): # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_config.title.reboot_start_time == "" or self.core_config.title.reboot_end_time == "": @@ -48,10 +58,10 @@ class Mai2DX(Mai2Base): "rebootEndTime": reboot_end, "movieUploadLimit": 100, "movieStatus": 1, - "movieServerUri": self.old_server + "movie/", - "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", - "oldServerUri": self.old_server + "old", - "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", + "movieServerUri": "", + "deliverServerUri": "", + "oldServerUri": self.old_server, + "usbDlServerUri": "", "rebootInterval": 0, }, "isAouAccession": False, diff --git a/titles/mai2/index.py b/titles/mai2/index.py index d194305..ad02648 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -141,31 +141,26 @@ class Mai2Servlet(BaseServlet): def get_routes(self) -> List[Route]: return [ - Route("/{version:int}/MaimaiServlet/api/movie/{endpoint:str}", self.handle_movie, methods=['GET', 'POST']), - Route("/{version:int}/MaimaiServlet/old/{endpoint:str}", self.handle_old_srv), - Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{placeid:str}/{keychip:str}/{userid:int}", self.handle_old_srv_userdata), - Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), - Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), - Route("/{version:int}/MaimaiServlet/usbdl/{endpoint:str}", self.handle_usbdl), - Route("/{version:int}/MaimaiServlet/deliver/{endpoint:str}", self.handle_deliver), - Route("/{version:int}/MaimaiServlet/{endpoint:str}", self.handle_mai, methods=['POST']), + Route("/{game:str}/{version:int}/MaimaiServlet/api/movie/{endpoint:str}", self.handle_movie, methods=['GET', 'POST']), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}", self.handle_old_srv), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{placeid:str}/{keychip:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{game:str}/{version:int}/MaimaiServlet/usbdl/{endpoint:str}", self.handle_usbdl), + Route("/{game:str}/{version:int}/MaimaiServlet/deliver/{endpoint:str}", self.handle_deliver), + Route("/{game:str}/{version:int}/MaimaiServlet/{endpoint:str}", self.handle_mai, methods=['POST']), Route("/{game:str}/{version:int}/Maimai2Servlet/{endpoint:str}", self.handle_mai2, methods=['POST']), ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if game_code in {Mai2Constants.GAME_CODE_DX, Mai2Constants.GAME_CODE_DX_INT}: - path = f"{game_code}/{game_ver}" - else: - path = game_ver - if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: return ( - f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{path}/", + f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", f"{self.core_cfg.server.hostname}", ) return ( - f"http://{self.core_cfg.server.hostname}/{path}/", + f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", f"{self.core_cfg.server.hostname}", ) @@ -289,25 +284,43 @@ class Mai2Servlet(BaseServlet): internal_ver = 0 client_ip = Utils.get_ip_addr(request) encrypted = False - - if version < 105: # 1.0 - internal_ver = Mai2Constants.VER_MAIMAI_DX - elif version >= 105 and version < 110: # PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS - elif version >= 110 and version < 115: # Splash - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH - elif version >= 115 and version < 120: # Splash PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS - elif version >= 120 and version < 125: # UNiVERSE - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125 and version < 130: # UNiVERSE PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - elif version >= 130 and version < 135: # FESTiVAL - internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL - elif version >= 135 and version < 140: # FESTiVAL PLUS - internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS - elif version >= 140: # BUDDiES - internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES + + if game_code == "SDEZ": # JP + if version < 110: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 110 and version < 114: # PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS + elif version >= 114 and version < 117: # Splash + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH + elif version >= 117 and version < 120: # Splash PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS + elif version >= 120 and version < 125: # UNiVERSE + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + elif version >= 125 and version < 130: # UNiVERSE PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130 and version < 135: # FESTiVAL + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + elif version >= 135 and version < 140: # FESTiVAL PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS + elif version >= 140: # BUDDiES + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES + elif game_code == "SDGA": # Int + if version < 105: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 105 and version < 110: # PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS + elif version >= 110 and version < 115: # Splash + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH + elif version >= 115 and version < 120: # Splash PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS + elif version >= 120 and version < 125: # UNiVERSE + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + elif version >= 125 and version < 130: # UNiVERSE PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130 and version < 135: # FESTiVAL + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + elif version >= 135 and version < 140: # FESTiVAL PLUS + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're From 3825ec8e3990839e16b33edb33cf8e5b27a0d214 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 May 2024 21:39:49 -0400 Subject: [PATCH 3/4] update contributing instructions --- contributing.md | 180 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 3 deletions(-) diff --git a/contributing.md b/contributing.md index 5397f70..d12425a 100644 --- a/contributing.md +++ b/contributing.md @@ -1,8 +1,182 @@ # Contributing to ARTEMiS -If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). Please make sure, if you're submitting a PR for a game or game version, that you're following the n-0/y-1 guidelines, or it will be rejected. +If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). This guide assume you're familiar with both git, python, and the libraries that artemis uses. + +This document is a work in progress. If you have any questions or notice any errors, please report it to the discord. ## Adding games -Guide WIP +### Step 0 ++ Follow the "n-1" rule of thumb. PRs for game versions that are currently active in arcades will be deleted. If you're unsure, ask! ++ Always PR against the `develop` branch. ++ Check to see if somebody else is already PRing the features/games you want to add. If they are, consider contributing to them rather then making an entirely new PR. ++ We don't technically have a written code style guide (TODO) but try to keep your code consistant with code that's already there where possible. + +### Step 1 (Setup) +1) Fork the gitea repo, clone your fork, and checkout the develop branch. +2) Make a new folder in the `titles` folder, name it some recogniseable shorthand for your game (Chunithm becomes chuni, maimai dx is mai2, etc) +3) In this new folder, create a file named `__init__.py`. This is the first thing that will load when your title module is loaded by the core system, and it acts as sort of a directory for where everything lives in your module. This file will contain the following required items: + + `index`: must point to a subclass of `BaseServlet` that will handle setup and dispatching of your game. + + `game_codes`: must be a list of 4 letter SEGA game codes as strings. + + It can also contain the following optional fields: + + `database`: points to a subclass of `Data` that contains one or more subclasses of `BaseData` that act as database transaction handlers. Required for the class to store and retrieve data from the database. + + `reader`: points to a subclass of `BaseReader` that handles importing static data from game files into the database. + + `frontend`: points to a subclass of `FE_Base` that handles frontend routes for your game. + + The next step will focus on `index` + +### Step 2 (Index) +1) Create another file in your game's folder. By convention, it should be called `index.py`. + +2) Inside `index.py`, add the following code, replacing {Game name here} with the name of your game, without spaces or special characters. Look at other titles for examples. +```py +from core.title import BaseServlet +from core import CoreConfig + +class {Game name here}Servlet(BaseServlet): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + pass +``` +3) The `__init__` function should acomplish the following: + + Reading your game's config + + Setting up your games logger + + Instancing your games versions + + It's usually safe to copy and paste the `__init__` functions from other games, just make sure you change everything that needs to be changed! + +4) Go back to the `__init__.py` that you created and add the following: +```py +from .index import {Game name here}Servlet + +index = {Game name here}Servlet +``` + +5) Going back to `index.py`, within the Servlet class, define the following functions from `BaseServlet` as needed (see function documentation): + + `is_game_enabled`: Returns true if the game is enabled and should be served, false otherwise. Returns false by default, so override this to allow your game to be served. + + `get_routes`: Returns a list of Starlette routes that your game will serve. + + `get_allnet_info`: Returns a tuple of strings where the first is the allnet uri and the second is the allnet host. The function takes the game ID, version and keychip ID as parameters, so you can send different responses if need be. + + `get_mucha_info`: Only used by games that use Mucha as authentication. Returns a tuple where the first is a bool that is weather or not the game is enabled, the 2nd is a list of game CDs as strings that this servlet should handle, and the 3rd is a list of netID prefixes that each game CD should use. If your game does not use mucha, do not define this function. + + `setup`: Preforms any setup your servlet requires, such as spinning up matching servers. It is run once when the server starts. If you don't need any setup, do not define. + +6) Make sure any functions you specify to handle routes in `get_routes` are defined as async, as follows: `async def handle_thing(self, request: Request) -> Response:` where Response is whatever kind of Response class you'll be returning. Make sure all paths in this function return some subclass of Response, otherwise you'll get an error when serving. + +### Step 3 (Constants) +1) In your game's folder, create a file to store static values for your game. By convention, we call this `const.py` + +2) Inside, create a class called `{Game name here}Constants`. Do not define an `__init__` function. + +3) Put constants related to your game here. A good example of something to put here is game codes. +```py +class {Game name here}Constants: + GAME_CODE = "SBXX" + CONFIG_NAME = "{game name}.yaml" +``` + +4) If you choose to put game codes in here, add this to your `__init__.py` file: +```py +from .const import {Game name here}Constants +... +game_codes = [{Game name here}Constants.GAME_CODE] +``` + +### Step 4 (Config) +1) Make a file to store your game's config. By convention, it should be called `config.py` + +2) Inside that file, add the following: +```py +from core.config import CoreConfig + +class {game name}ServerConfig: + def __init__(self, parent_config: "{game name}Config") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "{game name}", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "{game name}", "server", "loglevel", default="info" + ) + ) + +class {game name}Config(dict): + def __init__(self) -> None: + self.server = {game name}ServerConfig(self) +``` + +3) In the `example_config` folder, create a yaml file for your game. By convention, it should be called `{game folder name}.ymal`. Add the following: +```yaml +server: + enable: True + loglevel: "info" +``` + +4) Add any additional config options that you feel the game needs. Look to other games for config examples. + +5) In `index.py` import your config and instance it in `__init__` with: +```py +self.game_cfg = {game folder name}Config() +if path.exists(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}")) + ) +``` +This will attempt to load the config file you specified in your constants, and if not, go with the defaults specified in `config.py`. This game_cfg object can then be passed down to your handlers when you create them. + +At this stage your game should be loaded by allnet, and serve whatever routes you put in `get_routes`. See the next section about adding versions and handlers. + +### Step 5 (Database) +TODO + +### Step 6 (Frontend) +TODO + +### Step 7 (Reader) +TODO ## Adding game versions -Guide WIP \ No newline at end of file +See the above section about code expectations and how to PR. +1) In the game's folder, create a python file to contain the version handlers. By convention, the first version is version 0, and is stored in `base.py`. Versions following that increment the version number, and are stored in `{short version name}.py`. See Wacca's folder for an example of how to name versions. + +2) Internal version numbers should be defined in `const.py`. The version should change any time the game gets a major update (i.e. a new version or plus version.) +```py +# in const.py +VERSION_{game name} = 0 +VERSION_{game name}_PLUS = 1 +``` + +3) Inside `base.py` (or whatever your version is named) add the following: +```py +class {game name}Base: + def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None: + self.game_config = game_cfg + self.core_config = cfg + self.version = {game name}Constants.VERSION_{game name} + self.data = {game name}Data(cfg) + # Any other initialization stuff +``` + +4) Define your handlers. This will vary wildly by game, but best practice is to keep the naming consistant, so that the main dispatch function in `index.py` can use `getattr` to get the handler, rather then having a static list of what endpoint or request type goes to which handler. See Wacca's `index.py` and `base.py` for examples of how to do this. + +5) If your version is not the base version, make sure it inherits from the base version: +```py +class {game name}Plus({game name}Base): + def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None: + super().__init__(cfg, game_cfg) + self.version = {game name}Constants.VERSION_{game name}_PLUS +``` + +6) Back in `index.py` make sure to import your new class, and add it to `__init__`. Some games may opt to just a single list called `self.versions` that contains all the version classes at their internal version's index. Others may simply define them as seperate members. See Wacca for an example of `self.versions` + +7) Add your version to your game's dispatching logic. + +8) Test to make sure your game is being handled properly. + +9) Submit a PR. + +## Adding/improving core services +If you intend to submit improvements or additions to core services (allnet, mucha, billing, aimedb, database, etc) please get in touch with a maintainer. From 3ed8d9c16b0a87d950e4fbcc320126a695564bec Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 May 2024 21:40:02 -0400 Subject: [PATCH 4/4] update get_mucha_info documentation --- core/title.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/title.py b/core/title.py index 7aeb433..016e09a 100644 --- a/core/title.py +++ b/core/title.py @@ -80,7 +80,8 @@ class BaseServlet: cfg_dir (str): Config directory Returns: - Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD + Tuple[bool, List[str], List[str]]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CDs handled + by this servlette, and offset 2 is mucha netID prefixes that should be used for each game CD. """ return (False, [], [])