From 8fa51d9ed1dff5b2c7ed7c1c49fbdcbcb3a1d3a4 Mon Sep 17 00:00:00 2001 From: 20kdc Date: Sun, 19 Jun 2022 20:07:11 +0100 Subject: [PATCH] everything for this release (in particular the CPX extension for game path/etc.) --- caosproxy/spec.txt | 46 ++++++++++++++++------- caosproxy/tools/cpxinfo.py | 25 +++++++++++++ caosproxy/tools/index.mk | 4 +- caosproxy/w32/cpxservg.c | 1 + caosproxy/w32/cpxservi.c | 75 ++++++++++++++++++++++++++++++++------ ciesetup/README.md | 2 + 6 files changed, 126 insertions(+), 27 deletions(-) create mode 100755 caosproxy/tools/cpxinfo.py diff --git a/caosproxy/spec.txt b/caosproxy/spec.txt index 09bfe884..225253ae 100644 --- a/caosproxy/spec.txt +++ b/caosproxy/spec.txt @@ -2,7 +2,7 @@ CPX: CAOS Proxy For Cross-Language And Operating System Use specification version number - 2206182044 + 2206191904 license of specification @@ -33,6 +33,35 @@ critical notes on the shared memory interface the 32-bit little-endian status code (1 for error text, which inhibits other output) is at offset 8 and the response length (including null byte) is at offset 12 +command set (standard) + + The normal C2E command handling essentially reads a null-terminated string, and that's the command. + The command type is determined by the string-separated words of the first line. + + Commands are as follows: + "execute\n" : Executes the (following) CAOS. Output is the result. + "scrp X Y Z W\n" : Adds script with classifier X Y Z W, which follows. Don't add an "endm", it's implied. + +command set (CPX extension) + + These commands are similar to engine commands, but they have stricter requirements on spacing as they're implemented in the CPX server. + They are treated as "starts with this set of bytes", but will still expect null termination if they have a parameter. + All CPX extended command start with the first four bytes "cpx-", and this is used to detect them. + They are as follows: + "cpx-fwd:" : This is used as an "escape command" to say you REALLY want to bypass this CPX server. + "cpx-ver\n" : Output is the CPX server version. + "cpx-gamepath\n" : Output is the game path, i.e: C:\Program Files (x86)\Docking Station\ + +notes on extensions + + Extensions to this protocol, should they ever be required, should pretend to be engine commands. + It's not like the engine is ever going to change. + This is why the standardized API design is careful about the raw request API. + + Do be aware extensions should not be considered a substitute for manual configuration. + Returning the game path should not mean applications should only operate if the game path is correctly returned. + ***Extensions should absolutely not be abused to prevent manual configuration!!!*** + standardized API design Following this part of the specification is not required but recommended. @@ -63,17 +92,6 @@ standardized API design This means no truncating based on the null terminator. However, where the API is using exceptions, errors can have their null terminator stripped and be decoded as Latin-1. -notes on extensions - - Extensions to this protocol, should they ever be required, should pretend to be engine commands. - It's not like the engine is ever going to change. - This is why the standardized API design is careful about the raw request API. - - Do be aware extensions should not be considered a substitute for manual configuration. - Returning the "game path" sounds portable until a virtual machine or separate computer is involved. - Then it comes down to manual configuration. - ***Extensions should absolutely not be abused to prevent manual configuration!!!*** - server error handling It's possible for an error to occur in the CPX Server, rather than in the engine (say, if the game hasn't been started yet) @@ -128,9 +146,9 @@ design rationale implementation status (from most important to least) - CPX Server Windows : PUBLIC BETA + CPX Server Windows : OK Example CPX Client : OK - Storyteller PR : SORT OF + Storyteller : HANDLED BY UPSTREAM CPX Server Unix : BUGGY ALPHA CPX -> SHM : NO diff --git a/caosproxy/tools/cpxinfo.py b/caosproxy/tools/cpxinfo.py new file mode 100755 index 00000000..d2794c2c --- /dev/null +++ b/caosproxy/tools/cpxinfo.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# CAOS Terminal + +# caosprox - CPX server reference implementation +# Written starting in 2022 by 20kdc +# To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. +# You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . + +import socket +import readline +import sys +import struct +import traceback + +import libcpx + +def rrdo(t: bytes) -> str: + return libcpx.raw_request_default(t)[:-1].decode("latin1") + +print("server:", rrdo(b"cpx-ver\n\0")) +print("game path:", rrdo(b"cpx-gamepath\n\0")) +print("game name:", rrdo(b"execute\nouts gnam\0")) +print("engine version:", rrdo(b"execute\noutv vmjr outs \".\" outv vmnr\0")) +print("engine modules:", rrdo(b"execute\nouts modu\0")) + diff --git a/caosproxy/tools/index.mk b/caosproxy/tools/index.mk index 1c1190e2..ba5423b0 100644 --- a/caosproxy/tools/index.mk +++ b/caosproxy/tools/index.mk @@ -1,2 +1,4 @@ -rel: caosproxy/tools/caosterm.py caosproxy/tools/libcpx.py caosproxy/tools/maprooms.py caosproxy/tools/maptypes.py caosproxy/tools/bdmptest.py +rel: caosproxy/tools/libcpx.py +rel: caosproxy/tools/caosterm.py caosproxy/tools/cpxinfo.py +rel: caosproxy/tools/bdmptest.py caosproxy/tools/maprooms.py caosproxy/tools/maptypes.py diff --git a/caosproxy/w32/cpxservg.c b/caosproxy/w32/cpxservg.c index 75ded907..d5eaaf57 100644 --- a/caosproxy/w32/cpxservg.c +++ b/caosproxy/w32/cpxservg.c @@ -15,6 +15,7 @@ // returns non-zero on error extern int cpxservi_serverInit(int host, int port); extern const char * cpxservi_gameID; +// this uses stdio, so we need to be sure we stop using it from main thread if in UI mode! extern void cpxservi_serverLoop(); static HWND globalWindow; diff --git a/caosproxy/w32/cpxservi.c b/caosproxy/w32/cpxservi.c index 01729dd6..4d6c17b6 100644 --- a/caosproxy/w32/cpxservi.c +++ b/caosproxy/w32/cpxservi.c @@ -46,6 +46,7 @@ int cpxservi_serverInit(int host, int port) { } const char * cpxservi_gameID = NULL; +const char * cpxservi_gamePath = NULL; static int sgetc(SOCKET s) { char chr; @@ -77,23 +78,43 @@ static void sputa(SOCKET s, const void * target, int len) { static char gameIDBuffer[8192]; +static char gamePathBuffer[8192]; static char tmpBuffer[8200]; // above + 8 chars for _request -static const char * findGameID() { - if (cpxservi_gameID) - return cpxservi_gameID; +static const char * findRegKey(HKEY hive, const char * base, const char * sub, const char * def, char * buffer) { HKEY key; - if (!RegOpenKeyA(HKEY_CURRENT_USER, "Software\\CyberLife Technology\\Creatures Engine", &key)) { - LONG gameIDBufferLen = 8191; - int result = RegQueryValueA(key, "Default Game", gameIDBuffer, &gameIDBufferLen); + if (!RegOpenKeyA(hive, base, &key)) { + LONG bufferLen = 8191; + DWORD throwaway; // I feel like there's a story to this being required. + int result = RegQueryValueExA(key, sub, NULL, &throwaway, buffer, &bufferLen); RegCloseKey(key); if (!result) { // only try this if it succeeds - ERROR_MORE_DATA will cause it to run off the end of the buffer - gameIDBuffer[gameIDBufferLen] = 0; - return gameIDBuffer; + buffer[bufferLen] = 0; + return buffer; } + printf("caosprox registry warning, found \"%s\" but asking for \"%s\" got %x\n", base, sub, result); + } else { + printf("caosprox registry warning, failed to find \"%s\":\"%s\"\n", base, sub); } - return "Docking Station"; + return def; +} + +static const char * findGameID() { + if (cpxservi_gameID) + return cpxservi_gameID; + return findRegKey(HKEY_CURRENT_USER, "Software\\CyberLife Technology\\Creatures Engine", "Default Game", "Docking Station", gameIDBuffer); +} + +static const char * findGamePath(const char * gameID) { + if (cpxservi_gamePath) + return cpxservi_gamePath; + const char * base = "Software\\CyberLife Technology\\"; + int baseLen = strlen(base); + char name[baseLen + strlen(gameID) + 1]; + strcpy(name, base); + strcpy(name + baseLen, gameID); + return findRegKey(HKEY_LOCAL_MACHINE, name, "Main Directory", "C:\\Program Files (x86)\\Docking Station\\", gameIDBuffer); } typedef struct { @@ -107,6 +128,18 @@ static void transferAreaToClient(SOCKET client, const transfer_t * area, int hdr sputa(client, area, hdrOnly ? 24 : (area->sizeBytes + 24)); } +static void sendStringResponse(SOCKET client, const char * text) { + transfer_t * tmpErr = (transfer_t *) tmpBuffer; + memcpy(tmpErr->magic, "c2e@", 4); + strcpy(tmpErr->data, text); + tmpErr->pid = 0; + tmpErr->resultCode = 0; + tmpErr->sizeBytes = strlen(tmpErr->data) + 1; + tmpErr->maxSizeBytes = 8192; + tmpErr->padding = 0; + transferAreaToClient(client, tmpErr, 0); +} + // Note! doubleSend is 1 if we haven't sent the initial 24-byte header. static void internalError(SOCKET client, const char * text, int doubleSend) { transfer_t * tmpErr = (transfer_t *) tmpBuffer; @@ -122,7 +155,7 @@ static void internalError(SOCKET client, const char * text, int doubleSend) { transferAreaToClient(client, tmpErr, 0); } -static void handleClientWithEverything(SOCKET client, transfer_t * shm, HANDLE resultEvent, HANDLE requestEvent, HANDLE process) { +static void handleClientWithEverything(SOCKET client, const char * gameID, transfer_t * shm, HANDLE resultEvent, HANDLE requestEvent, HANDLE process) { // send SHM state to client transferAreaToClient(client, shm, 1); // now we want a size back @@ -141,7 +174,25 @@ static void handleClientWithEverything(SOCKET client, transfer_t * shm, HANDLE r internalError(client, "failed to get request body", 0); return; } - // actually run the request + // WAIT! This could be intended for CPX. + if ((size >= 8) && !memcmp(shm->data, "cpx-fwd:", 8)) { + // The client wants us to forward this along as if nothing happened. + memmove(shm->data, shm->data + 8, size - 8); + // fallthrough + } else if ((size >= 8) && !memcmp(shm->data, "cpx-ver\n", 8)) { + // The client wants us to give an identifier + sendStringResponse(client, "CPX Server W32"); + return; + } else if ((size >= 8) && !memcmp(shm->data, "cpx-gamepath\n", 13)) { + // The client wants us to give the game path + sendStringResponse(client, findGamePath(gameID)); + return; + } else if ((size >= 4) && !memcmp(shm->data, "cpx-", 4)) { + // It's intended for CPX but we don't recognize it. + internalError(client, "Unrecognized CPX extension command.", 0); + return; + } + // It's intended for the engine ResetEvent(resultEvent); PulseEvent(requestEvent); HANDLE waitHandles[2] = {process, resultEvent}; @@ -177,7 +228,7 @@ static void handleClientWithSHM(SOCKET client, const char * gameID, transfer_t * CloseHandle(process); return; } - handleClientWithEverything(client, shm, resultEvent, requestEvent, process); + handleClientWithEverything(client, gameID, shm, resultEvent, requestEvent, process); CloseHandle(requestEvent); CloseHandle(resultEvent); } diff --git a/ciesetup/README.md b/ciesetup/README.md index 032458eb..c7ca256d 100644 --- a/ciesetup/README.md +++ b/ciesetup/README.md @@ -1,5 +1,7 @@ # ciesetup +*STATUS: Incomplete* + ciesetup is a process designed to take one of three files: + `dockingstation.run` (MD5: 5a175535e104e6150005c1f3213aebc3 )