private IActionResult PostGame()
        {
            using (var reader = new StreamReader(Request.Body))
            {
                JObject postedObject;
                try
                {
                    // read posted data
                    postedObject = JObject.Parse(reader.ReadToEnd());
                }
                catch
                {
                    return(StatusCode(400)); // Bad Request
                }

                IGameListModule Plugin = null;
                Plugin = _gameListModuleManager.GetLikelyPlugins(postedObject, Request.Path.Value, Request.Method).FirstOrDefault();

                Plugin?.TransformPostParamaters(ref postedObject);

                /**
                 * __gameId
                 * Optional: Depends on server setting. Not optional on public server.
                 * Default: If optional, defaults to an unnamed game.
                 * This is a unique identifier for your game, of your choosing.If __gameId is
                 * unknown, the server will either create it or fail, depending on the server
                 * setting.On the public server, the server will create it.You may specify
                 * passwords for this game on creation with the control fields __updatePW and __readPW.
                 **/
                string inputGameId = postedObject["__gameId"]?.Value <string>();
                if (string.IsNullOrWhiteSpace(inputGameId))
                {
                    if (!s_AllowEmptyGameID)
                    {
                        return(StatusCode(400)); // Bad Request
                    }
                    inputGameId = string.Empty;
                }

                /**
                 * __clientReqId
                 * Optional: Yes
                 * Default: NIL.
                 * The intent of __clientReqId is if you have multiple games on the same computer,
                 * you can choose which game to update or delete on a subsequent request.On
                 * success, the value passed to __clientReqId is returned to you, along with
                 * __gameId and __rowId of the row and game added or updated. While optional, if
                 * you do not pass __clientReqId there is no way to know what __rowId was assigned
                 * to your game, so no way to later update or delete the row.
                 **/
                long inputClientReqId = -1;
                if (postedObject["__clientReqId"] != null)
                {
                    inputClientReqId = postedObject["__clientReqId"].Value <long>();
                }


                /**
                 * __timeoutSec
                 * Optional: Yes
                 * Default: 60 seconds
                 * Minimum: 15 seconds
                 * Maximum: 300 seconds on the public test server. 900 seconds on private servers.
                 * This parameter controls how long your game will be listed until it is deleted by
                 * the server.You must execute POST or PUT at least this often for your server to
                 * maintain continuous visibility. If your server crashes, then for the remainder
                 * of the timeout the server will be listed but unconnectable.
                 **/
                int inputTimeoutSec = s_TimeoutDefault; // default
                if (postedObject["__timeoutSec"] != null)
                {
                    if (!int.TryParse(postedObject["__timeoutSec"].Value <string>(), out inputTimeoutSec))
                    {
                        inputTimeoutSec = s_TimeoutDefault; // reinforce default
                    }
                }
                //if (inputTimeoutSec > s_TimeoutMax) inputTimeoutSec = s_TimeoutMax; // 900 on private list servers
                //if (inputTimeoutSec < s_TimeoutMin) inputTimeoutSec = s_TimeoutMin;

                if ((inputTimeoutSec > s_TimeoutMax) || (inputTimeoutSec < s_TimeoutMin))
                {
                    return(StatusCode(400)); // Bad Request
                }

                /**
                 * __geoIP
                 * Optional: Yes
                 * Default: Whatever IP you connected to the server with (See __addr)
                 * This parameter allows you to override what IP address is used for Geographic
                 * lookup.You will get more accurate results if you do a traceroute to your ISP,
                 * and pass that IP address with __geoIP, rather than letting the system determine
                 * your IP automatically.
                 **/
                string geoIP = Request.Query["__geoIP"];
                //string geoIP = __geoIP;
                if (geoIP != null && !IsValidIP(geoIP))
                {
                    geoIP = null;
                }


                /**
                 * __rowPW
                 * Optional: Yes
                 * Default: NIL.
                 * If __rowPW was specified when the row was created, you must also specify this
                 * value to update the row when using __rowId. The purpose of this value is to
                 * prevent players of other games from updating your own row. If a row required a
                 * password but it was not specified, or the password was wrong, error code 401
                 * will be returned.
                 **/
                string inputRowPW = postedObject["__rowPW"]?.Value <string>();
                if (string.IsNullOrWhiteSpace(inputRowPW))
                {
                    inputRowPW = null;
                }


                // Game level password for reading.
                // Not much point as it can just be wiresharked.
                // Seems to never change for a given game.

                /**
                 * __readPW
                 * Optional: Yes
                 * Default: Empty string / no password.
                 * This password is used for the GET operation. If specified when the a new game is
                 * created, this field specifies what password to set for future requests.
                 **/
                //string inputReadPW = postedObject["__readPW"].Value<string>();
                //if (inputReadPW == null || inputReadPW.Length == 0)
                //{
                //    inputReadPW = string.Empty;
                //}


                // Probably another game level password but for writing.
                // Not much point as it can just be wiresharked.
                // Seems to never change for a given game.

                /**
                 * __updatePW
                 * Optional: Yes
                 * Default: Empty string / no password.
                 * This password is used for POST, PUT, and DELETE operations. If specified when
                 * the a new game is created, this field specifies what password to set for future
                 * requests.
                 **/
                //string inputUpdatePW = postedObject["__updatePW"].Value<string>();
                //if (inputUpdatePW == null || inputUpdatePW.Length == 0)
                //{
                //    inputUpdatePW = string.Empty;
                //}


                // process input variables
                string inputAddr = Request.HttpContext.Connection.RemoteIpAddress?.ToString();


                /**
                 * __rowId
                 * Optional: Yes
                 * Default: NIL.
                 * If specified, a row with this ID will be overwritten, instead of creating a new
                 * row. After uploading a row the first time, you should use this __rowId on
                 * subsequent POST / PUT requests for the same game.
                 **/
                long inputRowId = -1;
                if (postedObject["__rowId"] != null)
                {
                    inputRowId = postedObject["__rowId"].Value <long>();
                }

                // prepare variables for holding check data
                long   lookupRowId = -1;
                string lookupRowPw = string.Empty;

                if (inputRowId < 0)
                {
                    // no input row ID, so this game is either new or something's gone wrong, try to grab a rowId and rowPw
                    // this is a special feature of our implementation, though it was taken from the kebbz gamelist php implementation
                    // this might be removed, it existed on the php list for easier injection of games from what I could tell
                    GameData dat = _gameListContext.CheckGame(inputAddr, inputClientReqId);
                    if (dat != null)
                    {
                        lookupRowId = dat.rowId;
                        lookupRowPw = dat.rowPW;
                    }
                }
                else
                {
                    // grab the existing game's rowPw
                    GameData dat = _gameListContext.CheckGame(inputRowId);
                    if (dat != null)
                    {
                        lookupRowId = dat.rowId;
                        lookupRowPw = dat.rowPW;
                    }
                }

                // no game already exists
                if ((lookupRowId < 0) || ((inputRowPW == null && lookupRowPw == null) || (inputRowPW == lookupRowPw)))
                {
                    // process custom fields
                    Dictionary <string, string> customValues = new Dictionary <string, string>();
                    postedObject.Properties().ToList().ForEach(dr =>
                    {
                        if (!dr.Name.StartsWith("__") && dr.Value.Type != JTokenType.Null)
                        {
                            customValues[dr.Name] = (dr.Value.Type == JTokenType.String ? ("\"" + dr.Value.ToString() + "\"") : dr.Value.ToString());
                        }
                    });

                    GameData tmpGame = null;
                    if (lookupRowId < 0)
                    {
                        // create game
                        tmpGame = _gameListContext.AddGame(inputGameId, DateTime.UtcNow, inputTimeoutSec, inputRowPW, inputClientReqId, inputAddr, customValues);
                    }
                    else if ((inputRowPW == null && lookupRowPw == null) || (inputRowPW == lookupRowPw))
                    {
                        // update game
                        tmpGame = _gameListContext.UpdateGame(lookupRowId, DateTime.UtcNow, inputTimeoutSec, inputClientReqId, inputAddr, customValues);
                    }

                    if (tmpGame == null)
                    {
                        return(StatusCode(500)); // Error
                    }
                    PostGameResponse retVal = new PostGameResponse()
                    {
                        POST = new Dictionary <string, JToken>()
                        {
                            { "__clientReqId", inputClientReqId },
                            { "__rowId", lookupRowId },
                            { "__gameId", inputGameId },
                        }
                    };

                    Plugin?.TransformPostResponse(ref retVal);

                    //return Json(retVal);
                    string responseString = JsonConvert.SerializeObject(retVal);
                    Response.Headers.ContentLength = responseString.Length;
                    return(Content(responseString, "application/json"));
                }
                else if (inputRowPW != lookupRowPw)
                {
                    return(StatusCode(401)); // Unauthorized
                }
                else
                {
                    return(StatusCode(400)); // Bad Request
                }
            }
        }