From e6aaf2083f8b0127955ecff73732af5c475c2dcc Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 3 Aug 2021 04:28:32 +0700 Subject: [PATCH] Refactor Avarice to provide a proper interface --- sources/Avarice/Avarice.uc | 136 +++++- sources/Avarice/AvariceAPI.uc | 70 ++- sources/Avarice/AvariceClient.uc | 88 ---- sources/Avarice/AvariceLink.uc | 437 ++++++++++++++++++ sources/Avarice/AvariceMessage.uc | 112 ++--- sources/Avarice/AvariceTCPLink.uc | 170 ------- sources/Avarice/AvariceTcpStream.uc | Bin 0 -> 26976 bytes .../Events/Avarice_OnMessage_Signal.uc | 38 ++ .../Avarice/Events/Avarice_OnMessage_Slot.uc | 40 ++ 9 files changed, 733 insertions(+), 358 deletions(-) delete mode 100644 sources/Avarice/AvariceClient.uc create mode 100644 sources/Avarice/AvariceLink.uc delete mode 100644 sources/Avarice/AvariceTCPLink.uc create mode 100644 sources/Avarice/AvariceTcpStream.uc create mode 100644 sources/Avarice/Events/Avarice_OnMessage_Signal.uc create mode 100644 sources/Avarice/Events/Avarice_OnMessage_Slot.uc diff --git a/sources/Avarice/Avarice.uc b/sources/Avarice/Avarice.uc index 1209199..f162618 100644 --- a/sources/Avarice/Avarice.uc +++ b/sources/Avarice/Avarice.uc @@ -1,5 +1,24 @@ /** - * + * This feature makes it possible to use TCP connection to exchange + * messages (represented by JSON objects) with external applications. + * There are some peculiarities to UnrealEngine's `TCPLink`, so to simplify + * communication process for external applications, they are expected to + * connect to the server through the "Avarice" utility that can accept a stream + * of utf8-encoded JSON messageand feed them to our `TCPLink` (we use child + * class `AvariceTcpStream`) in a way it can receive them. + * Every message sent to us must have the following structure: + * { "s": "", "t": "", "p": } + * where + * * describes a particular source of messages + * (it can be a name of the database or an alias for + * a connected application); + * * simply states the name of a command, for a database it + * can be "get", "set", "delete", etc.. + * * can be an arbitrary json value and can be used to + * pass any additional information along with the message. + * Acedia provides a special treatment for any messages that have their + * service set to "echo" - it always returns them back as-is, except for the + * message type that gets set to "end". * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -20,53 +39,84 @@ class Avarice extends Feature config(AcediaAvarice); -struct AvariceLink +// The feature itself is dead simple - it simply creates list of +// `AvariceLink` objects, according to its settings, and stores them +var private array createdLinks; + +struct AvariceLinkRecord { var string name; - var string host; + var string address; }; +// List all the names (and addresses) of all Avarice instances Acedia must +// connect to. +// `name` parameter is a useful (case-insensitive) identifier that +// can be used in other configs to point at each link. +// `address` must have form "host:port", where "host" is either ip or +// domain name and "port" is a numerical port value. +var private config array link; + +// In case Avarice utility is launched after link started trying open +// the connection - that connection attempt will fail. To fix that link must +// try connecting again. +// This variable sets the time (in seconds), after which link will +// re-attempt opening connection. Setting value too low can prevent any +// connection from opening, setting it too high might make you wait for +// connection too long. +var private config float reconnectTime; -var private config array link; +var private const int TECHO, TEND, TCOLON; var private LoggerAPI.Definition errorBadAddress; protected function OnEnabled() { - local int i; - local string host; - local int port; - local AvariceTCPLink nextTCPLink; + local int i; + local Text name; + local MutableText host; + local int port; + local AvariceLink nextLink; for (i = 0; i < link.length; i += 1) { - if (!ParseAddress(link[i].host, host, port)) { + name = _.text.FromString(link[i].name); + if (ParseAddress(link[i].address, host, port)) + { + nextLink = AvariceLink(_.memory.Allocate(class'AvariceLink')); + nextLink.Initialize(name, host, port); + nextLink.StartUp(); + nextLink.OnMessage(self, T(TECHO)).connect = EchoHandler; + createdLinks[createdLinks.length] = nextLink; + } + else { _.logger.Auto(errorBadAddress).Arg(_.text.FromString(link[i].name)); } - nextTCPLink = AvariceTCPLink(_.memory.Allocate(class'AvariceTCPLink')); - nextTCPLink.Connect(link[i].name, host, port); + _.memory.Free(name); + _.memory.Free(host); } } protected function OnDisabled() { - local LevelInfo level; - local AvariceTCPLink nextTCPLink; - level = _.unreal.GetLevel(); - foreach level.DynamicActors(class'AvariceTCPLink', nextTCPLink) { - nextTCPLink.Destroy(); - } + _.memory.FreeMany(createdLinks); +} + +// Reply back any messages from "echo" service +private function EchoHandler(AvariceLink link, AvariceMessage message) +{ + link.SendMessage(T(TECHO), T(TEND), message.parameters); } private final function bool ParseAddress( - string address, - out string host, - out int port) + string address, + out MutableText host, + out int port) { local bool success; local Parser parser; parser = _.text.ParseString(address); parser.Skip() - .MUntilS(host, _.text.GetCharacter(":")) - .MatchS(":") + .MUntil(host, T(TCOLON).GetCharacter(0)) + .Match(T(TCOLON)) .MUnsignedInteger(port) .Skip(); success = parser.Ok() && parser.GetRemainingLength() == 0; @@ -74,7 +124,49 @@ private final function bool ParseAddress( return success; } +/** + * Method that returns all the `AvariceLink` created by this feature. + * + * @return Array of links created by this feature. + * Guaranteed to not contain `none` values. + */ +public final function array GetAllLinks() +{ + local int i; + while (i < createdLinks.length) + { + if (createdLinks[i] == none) { + createdLinks.Remove(i, 1); + } + else { + i += 1; + } + } + return createdLinks; +} + +/** + * Returns its current `reconnectTime` setting that describes amount of time + * between connection attempts. + * + * @return Value of `reconnectTime` config variable. + */ +public final static function float GetReconnectTime() +{ + return default.reconnectTime; +} + defaultproperties { + // Settings + reconnectTime = 10.0 + // `Text` constants + TECHO = 0 + stringConstants(0) = "echo" + TEND = 1 + stringConstants(1) = "end" + TCOLON = 2 + stringConstants(2) = ":" + // Log messages errorBadAddress = (l=LOG_Error,m="Cannot parse address \"%1\"") } \ No newline at end of file diff --git a/sources/Avarice/AvariceAPI.uc b/sources/Avarice/AvariceAPI.uc index 1f06036..ddcdc1d 100644 --- a/sources/Avarice/AvariceAPI.uc +++ b/sources/Avarice/AvariceAPI.uc @@ -1,4 +1,5 @@ /** + * API for Avarice functionality of Acedia. * Copyright 2020 - 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -18,35 +19,56 @@ */ class AvariceAPI extends AcediaObject; -public final function AvariceMessage MessageFromText(Text message) +/** + * Method that returns all the `AvariceLink` created by `Avarice` feature. + * + * @return Array of links created by this feature. + * Guaranteed to not contain `none` values. + * Empty if `Avarice` feature is currently disabled. + */ +public final function array GetAllLinks() { - local Parser parser; - local AvariceMessage result; - local AssociativeArray parsedMessage; - if (message == none) return none; - parser = _.text.Parse(message); - parsedMessage = _.json.ParseObjectWith(parser); - parser.FreeSelf(); - if (!HasNecessaryMessageKeys(parsedMessage)) - { - _.memory.Free(parsedMessage); - return none; + local Avarice avariceFeature; + local array emptyResult; + avariceFeature = Avarice(class'Avarice'.static.GetInstance()); + if (avariceFeature != none) { + return avariceFeature.GetAllLinks(); } - result = AvariceMessage(_.memory.Allocate(class'AvariceMessage')); - result.SetID(parsedMessage.GetText(P("i"))); - result.SetGroup(parsedMessage.GetText(P("g"))); - result.data = parsedMessage.TakeItem(P("p")); - _.memory.Free(parsedMessage); - return result; + return emptyResult; } -private final function bool HasNecessaryMessageKeys(AssociativeArray message) +/** + * Finds and returns `AvariceLink` by its name, specified in "AcediaAvarice" + * config, if it exists. + * + * @param linkName Name of the `AvariceLink` to find. + * @return `AvariceLink` corresponding to name `linkName`. + * If `linkName == none` or `AvariceLink` with such name does not exist - + * returns `none`. + */ +public final function AvariceLink GetLink(Text linkName) { - if (message == none) return false; - if (!message.HasKey(P("i"))) return false; - if (!message.HasKey(P("g"))) return false; - - return true; + local int i; + local Text nextName; + local array allLinks; + if (linkName == none) { + return none; + } + allLinks = GetAllLinks(); + for (i = 0; i < allLinks.length; i += 1) + { + if (allLinks[i] == none) { + continue; + } + nextName = allLinks[i].GetName(); + if (linkName.Compare(nextName, SCASE_INSENSITIVE)) + { + _.memory.Free(nextName); + return allLinks[i]; + } + _.memory.Free(nextName); + } + return none; } defaultproperties diff --git a/sources/Avarice/AvariceClient.uc b/sources/Avarice/AvariceClient.uc deleted file mode 100644 index 4b37733..0000000 --- a/sources/Avarice/AvariceClient.uc +++ /dev/null @@ -1,88 +0,0 @@ -class AvariceClient extends AcediaObject; - -enum AvariceClientState -{ - ACS_Waiting, - ACS_ReadingID, - ACS_ReadingLength, - ACS_ReadingPayload, - ACS_Invalid -}; - -var private int currentID; -var private int currentMessageLength; -var private array currentPayload; - -var private AvariceClientState currentState; -var private int bytesLeftToRead; -var private byte buffer[255]; -var private array longBuffer; -var private int pendingBytes; - -public final function PushByte(byte nextByte) -{ - if (nextByte == 0) - { - if (bytesLeftToRead > 0) - { - // ACK for short message (with id) - } - currentState = ACS_Waiting; - ResetBuffer(); - return; - } - else if (currentState == ACS_Invalid) - { - // ACK of invalid message's end - return; - } - else if (currentState == ACS_Waiting) - { - currentID = nextByte; - currentID = currentID << 8; - currentState = ACS_ReadingID; - } - else if (currentState == ACS_ReadingID) - { - currentID += nextByte; - currentState = ACS_ReadingLength; - bytesLeftToRead = 2; - } - else if (currentState == ACS_ReadingLength) - { - bytesLeftToRead -= 1; - if (bytesLeftToRead > 0) - { - currentMessageLength = nextByte; - currentMessageLength = currentMessageLength << 8; - } - else - { - currentMessageLength += nextByte; - currentState = ACS_ReadingPayload; - bytesLeftToRead = currentMessageLength; - } - } - else if (currentState == ACS_ReadingPayload) - { - currentPayload[currentPayload.length] = nextByte; - // Decode payload into `AvariceMessage` - // Send messages via Acedia's signals - bytesLeftToRead -= 1; - if (bytesLeftToRead == 0) - { - currentState = ACS_Waiting; - // ACK into buffer - } - } -} - -private final function ResetBuffer() -{ - pendingBytes = 0; - longBuffer.length = 0; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Avarice/AvariceLink.uc b/sources/Avarice/AvariceLink.uc new file mode 100644 index 0000000..8012cb4 --- /dev/null +++ b/sources/Avarice/AvariceLink.uc @@ -0,0 +1,437 @@ +/** + * Provide interface for the connection to Avarice application. + * It's parameters are defined in Acedia's config. + * Class provides methods to obtain its configuration information + * (name, address, port), methods to check and change the status of connection, + * signals to handle arriving messages and ways to send messages back. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class AvariceLink extends AcediaObject; + +/** + * Objects of this class are supposed to be obtained via the + * `AvariceAPI.GetLink()` method. Available links are automatically initialized + * based on the configs and their parameters cannot be changed. + * It is also possible to spawn a link of your own by creating an object of + * this class (`AvariceLink`) and calling `Initialize()` method with + * appropriate parameters. To start the link then simply call `StartUp()`. + * But such links will not appear in the list of available links in + * `AvariceAPI`. + */ + +// Actual work of dealing with network input/output is done in +// the `AvariceTcpStream` `Actor` class that is stored inside this reference +var private NativeActorRef tcpStream; + +// `tcpStream` communicates with this class by informing it about specific +// events. This enum describes all of their types. +enum AvariceNetworkMessage +{ + // Connection with Avarice established - can happen several times in case + // connection is interrupted + ANM_Connected, + // We have lost connection with Avarice, but normally will attempt to + // reconnect back + ANM_Disconnected, + // JSON message received + ANM_Message, + // Connection died: either was manually closed, host address could not + // be resolved or invalid data was received from Avarice + ANM_Death +}; + +// Name of this link, specified in the config +var private Text linkName; +// Host of the Avarice instance we are connecting to +var private Text linkHost; +// Port used by the Avarice instance we are connecting to +var private int linkPort; + +var private SimpleSignal onConnectedSignal; +var private SimpleSignal onDisconnectedSignal; +var private SimpleSignal onDeathSignal; +// We want to have a separate signal for each message "service", since most +// users of `AvariceLink` would only care about one particular service. +// To achieve that we use this array as a "service name" <-> "signal" map. +var private AssociativeArray serviceSignalMap; + +var private const int TSERVICE_PREFIX, TTYPE_PREFIX; +var private const int TPARAMS_PREFIX, TMESSAGE_SUFFIX; + +var private LoggerAPI.Definition fatalCannotSpawn; + +protected function Constructor() +{ + onConnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); + onDisconnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); + onDeathSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); + serviceSignalMap = _.collections.EmptyAssociativeArray(); +} + +protected function Finalizer() +{ + local Actor storedStream; + _.memory.Free(onConnectedSignal); + _.memory.Free(onDisconnectedSignal); + _.memory.Free(onDeathSignal); + _.memory.Free(serviceSignalMap); + _.memory.Free(linkName); + _.memory.Free(linkHost); + onConnectedSignal = none; + onDisconnectedSignal = none; + onDeathSignal = none; + serviceSignalMap = none; + linkName = none; + linkHost = none; + linkPort = 0; + if (tcpStream == none) { + return; + } + storedStream = tcpStream.Get(); + if (storedStream != none) { + storedStream.Destroy(); + } + tcpStream.FreeSelf(); + tcpStream = none; +} + +/** + * Initializes this caller `AvariceLink` with config data. + * + * Can only successfully (for that `name` and `host` must not be `none`) + * be called once. + * + * @param name Alias (case-insensitive) of caller `AvariceLink`. + * Must not be `none`. + * @param host Host of the Avarice instance that caller `AvariceLink` is + * connecting to. Must not be `none`. + * @param name Port used by the Avarice instance that caller `AvariceLink` + * is connecting to. + */ +public final function Initialize(Text name, Text host, int port) +{ + if (tcpStream != none) return; + if (name == none) return; + if (host == none) return; + + linkName = name.Copy(); + linkHost = host.Copy(); + linkPort = port; + tcpStream = _.unreal.ActorRef(none); +} + +/** + * Signal that will be emitted whenever caller `AvariceLink` connects to + * Avarice. This event can be emitted multiple times if case link temporarily + * loses it's TCP connection or if connection is killed off due to errors + * (or manually). + * + * [Signature] + * void () + */ +/* SIGNAL */ +public final function SimpleSlot OnConnected(AcediaObject receiver) +{ + return SimpleSlot(onConnectedSignal.NewSlot(receiver)); +} + +/** + * Signal that will be emitted whenever caller `AvariceLink` disconnects from + * Avarice. Disconnects can temporarily be cause by network issue and + * `AvariceLink` will attempt to restore it's connection. To detect when + * connection was permanently severed use `OnDeath` signal instead. + * + * [Signature] + * void () + */ +/* SIGNAL */ +public final function SimpleSlot OnDisconnected(AcediaObject receiver) +{ + return SimpleSlot(onDisconnectedSignal.NewSlot(receiver)); +} + +/** + * Signal that will be emitted whenever connection is closed and dropped: + * either due to bein unable to resolve host's address, receiving incorrect + * input from Avarice or someone manually closing it. + * + * [Signature] + * void () + */ +/* SIGNAL */ +public final function SimpleSlot OnDeath(AcediaObject receiver) +{ + return SimpleSlot(onDeathSignal.NewSlot(receiver)); +} + +/** + * Signal that will be emitted whenever caller `AvariceLink` disconnects from + * Avarice. Disconnects can temporarily be cause by network issue and + * `AvariceLink` will attempt to restore it's connection. To detect when + * connection was permanently severed use `OnDeath` signal instead. + * + * @param service Name of the service, whos messages one wants to receive. + * `none` will be treated as an empty `Text`. + * + * [Signature] + * void (AvariceLink link, AvariceMessage message) + * @param link Link that has received message. + * @param message Received message. + * Can be any JSON-compatible value (see `JSONAPI.IsCompatible()` + * for more information). + */ +/* SIGNAL */ +public final function Avarice_OnMessage_Slot OnMessage( + AcediaObject receiver, + Text service) +{ + return Avarice_OnMessage_Slot(GetServiceSignal(service).NewSlot(receiver)); +} + +private final function Avarice_OnMessage_Signal GetServiceSignal(Text service) +{ + local Avarice_OnMessage_Signal result; + if (service != none) { + service = service.Copy(); + } + else { + service = Text(_.memory.Allocate(class'Text')); + } + result = Avarice_OnMessage_Signal(serviceSignalMap.GetItem(service)); + if (result == none) + { + result = Avarice_OnMessage_Signal( + _.memory.Allocate(class'Avarice_OnMessage_Signal')); + serviceSignalMap.SetItem(service, result); + } + else { + service.FreeSelf(); + } + return result; +} + +/** + * Starts caller `AvariceLink`, making it attempt to connect to the Avarice + * with parameters that should be first specified by the `Initialize()` call. + * + * Does nothing if the caller `AvariceLink` is either not initialized or + * is already active (`IsActive() == true`). + */ +public final function StartUp() +{ + local AvariceTcpStream newStream; + if (tcpStream == none) return; + if (tcpStream.Get() != none) return; + + newStream = AvariceTcpStream(_.memory.Allocate(class'AvariceTcpStream')); + if (newStream == none) + { + // `linkName` has to be defined if `tcpStream` is defined + _.logger.Auto(fatalCannotSpawn).Arg(linkName.Copy()); + return; + } + tcpStream.Set(newStream); + newStream.StartUp(self, class'Avarice'.static.GetReconnectTime()); +} + +/** + * Shuts down any connections related to the caller `AvariceLink`. + * + * Does nothing if the caller `AvariceLink` is either not initialized or + * is already inactive (`IsActive() == false`). + */ +public final function ShutDown() +{ + local Actor storedStream; + if (tcpStream == none) return; + storedStream = tcpStream.Get(); + if (storedStream == none) return; + + storedStream.Destroy(); + tcpStream.Set(none); +} + +/** + * Checks whether caller `AvariceLink` is currently active: either connected or + * currently attempts to connect to Avarice. + * + * See also `IsConnected()`. + * + * @return `true` if caller `AvariceLink` is either connected or currently + * attempting to connect to Avarice. `false` otherwise. + */ +public final function bool IsActive() +{ + if (tcpStream == none) { + return false; + } + return tcpStream.Get() != none; +} + +/** + * Checks whether caller `AvariceLink` is currently connected or to Avarice. + * + * See also `IsActive()`. + * + * @return `true` iff caller `AvariceLink` is currently connected to Avarice. + */ +public final function bool IsConnected() +{ + local AvariceTcpStream storedStream; + if (tcpStream == none) { + return false; + } + storedStream = AvariceTcpStream(tcpStream.Get()); + return storedStream.linkState == STATE_Connected; +} + +/** + * Returns name caller `AvariceLink` was initialized with. + * Defined through the config files. + * + * @return Name of the caller `AvariceLink`. + * `none` iff caller link was not yet initialized. + */ +public final function Text GetName() +{ + if (linkName != none) { + return linkName.Copy(); + } + // `linkName` cannot be `none` after `Initialize()` call + return none; +} + +/** + * Returns host name (without port number) caller `AvariceLink` was + * initialized with. Defined through the config files. + * + * See `GetPort()` method for port number. + * + * @return Host name of the caller `AvariceLink`. + * `none` iff caller link was not yet initialized. + */ +public final function Text GetHost() +{ + if (linkHost != none) { + return linkHost.Copy(); + } + // `linkName` cannot be `none` after `Initialize()` call + return none; +} + +/** + * Returns port number caller `AvariceLink` was initialized with. + * Defined through the config files. + * + * @return Host name of the caller `AvariceLink`. + * If caller link was not yet initialized, method makes no guarantees + * about returned number. + */ +public final function int GetPort() +{ + return linkPort; +} + +/** + * Send a message to the Avarice that caller `AvariceLink` is connected to. + * + * Message can only be set if caller `AvariceLink` was initialized and is + * currently connected (see `IsConnected()`) to Avarice. + * + * @param service Name of the service this message is addressed. + * As an example, to address a database one would specify its name, + * like "db". Cannot be `none`. + * @param type Name of this message. As an example, to address + * a database, one would specify here WHAT that database must do, + * like "get" to fetch some data. Cannot be `none`. + * @param parameters Parameters of the command. Can be any value that is + * JSON-compatible (see `JSONAPI.IsCompatible()` for details). + * @return `true` if message was successfully sent and `false` otherwise. + * Note that this method returning `true` does not necessarily mean that + * message has arrived (which is impossible to know at this moment), + * instead simply saying that network call to send data was successful. + * Avarice does not provide any mechanism to verify message arrival, so if + * you need that confirmation - it is necessary that service you are + * addressing make a reply. + */ +public final function bool SendMessage( + Text service, + Text type, + AcediaObject parameters) +{ + local Mutabletext parametesAsJSON; + local MutableText message; + local AvariceTcpStream storedStream; + if (tcpStream == none) return false; + if (service == none) return false; + if (type == none) return false; + storedStream = AvariceTcpStream(tcpStream.Get()); + if (storedStream == none) return false; + if (storedStream.linkState != STATE_Connected) return false; + parametesAsJSON = _.json.Print(parameters); + if (parametesAsJSON == none) return false; + + message = _.text.Empty(); + message.Append(T(TSERVICE_PREFIX)) + .Append(_.json.Print(service)) + .Append(T(TTYPE_PREFIX)) + .Append(_.json.Print(type)) + .Append(T(TPARAMS_PREFIX)) + .Append(parametesAsJSON) + .Append(T(TMESSAGE_SUFFIX)); + storedStream.SendMessage(message); + message.FreeSelf(); + parametesAsJSON.FreeSelf(); + return true; +} + +// This is a public method, but it is not a part of +// `AvariceLink` interface. +// It is used as a communication channel with `AvariceTcpStream` and +// should not be called outside of that class. +public final function ReceiveNetworkMessage( + AvariceNetworkMessage message, + optional AvariceMessage avariceMessage) +{ + if (message == ANM_Connected) { + onConnectedSignal.Emit(); + } + else if (message == ANM_Disconnected) { + onDisconnectedSignal.Emit(); + } + else if (message == ANM_Message && avariceMessage != none) { + GetServiceSignal(avariceMessage.service).Emit(self, avariceMessage); + } + else if (message == ANM_Death) { + onDeathSignal.Emit(); + tcpStream.Set(none); + } +} + +defaultproperties +{ + TSERVICE_PREFIX = 0 + stringConstants(0) = "&{\"s\":" + TTYPE_PREFIX = 1 + stringConstants(1) = ",\"t\":" + TPARAMS_PREFIX = 2 + stringConstants(2) = ",\"p\":" + TMESSAGE_SUFFIX = 3 + stringConstants(3) = "&}" + fatalCannotSpawn = (l=LOG_Error,m="Cannot spawn new actor of class `AvariceTcpStream`, avarice link \"%1\" will not be created") +} \ No newline at end of file diff --git a/sources/Avarice/AvariceMessage.uc b/sources/Avarice/AvariceMessage.uc index 42551d2..2655819 100644 --- a/sources/Avarice/AvariceMessage.uc +++ b/sources/Avarice/AvariceMessage.uc @@ -1,12 +1,44 @@ +/** + * Object that represents a message received from Avarice. + * For performance's sake it does not provide a getter/setter interface and + * exposes public fields instead. However, for Acedia to correctly function + * you are not supposed modify those fields in any way, only using them to + * read necessary data. + * All `AvariceMessage`'s fields will be automatically deallocated, so if + * you need their data - you have to make a copy, instead of simply storing + * a reference to them. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ class AvariceMessage extends AcediaObject; -var private Text messageID; -var private Text messageGroup; - -var public AcediaObject data; +// Every message from Avarice has following structure: +// { "s": "", "t": "", "p": } +// Value of the "s" field +var public Text service; +// Value of the "t" field +var public Text type; +// Value of the "p" field +var public AcediaObject parameters; var private AssociativeArray messageTemplate; +var private const int TS, TT, TP; + public static function StaticConstructor() { if (StaticConstructorGuard()) return; @@ -18,12 +50,12 @@ public static function StaticConstructor() protected function Finalizer() { - __().memory.Free(messageID); - __().memory.Free(messageGroup); - __().memory.Free(data); - messageID = none; - messageGroup = none; - data = none; + __().memory.Free(type); + __().memory.Free(service); + __().memory.Free(parameters); + type = none; + service = none; + parameters = none; } private static final function ResetTemplate(AssociativeArray template) @@ -31,63 +63,35 @@ private static final function ResetTemplate(AssociativeArray template) if (template == none) { return; } - template.SetItem(P("i"), none); - template.SetItem(P("g"), none); - template.SetItem(P("p"), none); -} - -public final function SetID(Text id) -{ - _.memory.Free(messageID); - messageID = none; - if (id != none) { - messageID = id.Copy(); - } -} - -public final function Text GetID() -{ - if (messageID != none) { - return messageID.Copy(); - } - return none; -} - -public final function SetGroup(Text group) -{ - _.memory.Free(messageGroup); - messageGroup = none; - if (group != none) { - messageGroup = group.Copy(); - } -} - -public final function Text GetGroup() -{ - if (messageGroup != none) { - return messageGroup.Copy(); - } - return none; + template.SetItem(T(default.TS), none); + template.SetItem(T(default.TT), none); + template.SetItem(T(default.TP), none); } public final function MutableText ToText() { local MutableText result; local AssociativeArray template; - if (messageID == none) return none; - if (messageGroup == none) return none; + if (type == none) return none; + if (service == none) return none; template = default.messageTemplate; - template.SetItem(P("i"), messageID); - template.SetItem(P("g"), messageGroup); - if (data != none) { - template.SetItem(P("p"), data); + ResetTemplate(template); + template.SetItem(T(TT), type); + template.SetItem(T(TS), service); + if (parameters != none) { + template.SetItem(T(TP), parameters); } result = _.json.Print(template); - ResetTemplate(template); return result; } defaultproperties { + TS = 0 + stringConstants(0) = "s" + TT = 1 + stringConstants(1) = "t" + TP = 2 + stringConstants(2) = "p" } \ No newline at end of file diff --git a/sources/Avarice/AvariceTCPLink.uc b/sources/Avarice/AvariceTCPLink.uc deleted file mode 100644 index 335a1c8..0000000 --- a/sources/Avarice/AvariceTCPLink.uc +++ /dev/null @@ -1,170 +0,0 @@ -class AvariceTcpLink extends TcpLink - dependson(LoggerAPI); - -var private Global _; - -var private string linkName; -var private string linkHost; -var private int linkPort; -var private IpAddr remoteAddress; -var private int ttt; - -var private bool didWorkLastTick; - -var private array buffer; - -var private Utf8Encoder encoder; -var private Utf8Decoder decoder; - -var private LoggerAPI.Definition infoSuccess; -var private LoggerAPI.Definition fatalBadPort; -var private LoggerAPI.Definition fatalCannotBindPort; -var private LoggerAPI.Definition fatalCannotResolveHost; -var private LoggerAPI.Definition fatalCannotConnect; - -public final function bool Connect(string name, string host, int port) -{ - local InternetLink.IpAddr ip; - local int usedPort; - // Apparently `TcpLink` ignores default values for these variables, - // so we set them here - linkMode = MODE_Binary; - receiveMode = RMODE_Manual; - _ = class'Global'.static.GetInstance(); - encoder = Utf8Encoder(_.memory.Allocate(class'Utf8Encoder')); - decoder = Utf8Decoder(_.memory.Allocate(class'Utf8Decoder')); - linkName = name; - linkHost = host; - linkPort = port; - if (port <= 0) - { - _.logger.Auto(fatalBadPort) - .ArgInt(port) - .Arg(_.text.FromString(linkName)); - return false; - } - if (BindPort(, true) <= 0) - { - _.logger.Auto(fatalCannotBindPort) - .ArgInt(port) - .Arg(_.text.FromString(name)); - return false; - } - StringToIpAddr(host, remoteAddress); - remoteAddress.port = port; - if (remoteAddress.addr == 0) { - Resolve(host); - } - else { - OpenAddress(); - } - return true; -} - -event Resolved(IpAddr resolvedAddress) -{ - remoteAddress.addr = resolvedAddress.addr; - OpenAddress(); -} - -private final function bool OpenAddress() -{ - if (!OpenNoSteam(remoteAddress)) { - _.logger.Auto(fatalCannotConnect).Arg(_.text.FromString(linkName)); - } - _.logger.Auto(infoSuccess).Arg(_.text.FromString(linkName)); -} - -event ResolveFailed() -{ - _.logger.Auto(fatalCannotResolveHost).Arg(_.text.FromString(linkHost)); - // !Shut down! -} - -event Tick(float delta) -{ - local array toSend; - local AvariceMessage nextAMessage; - local MutableText nextMessage; - local int i, j, dataRead, totalRead, iter; - local byte data[255]; - if (didWorkLastTick) - { - didWorkLastTick = false; - return; - } - if (!IsDataPending()) { - return; - } - while (true) { - dataRead = ReadBinary(255, data); - for (i = 0; i < dataRead; i += 1) { - ttt += 1; - decoder.PushByte(data[i]); - } - if (dataRead <= 0) { - break; - } - } - if (ttt >= 4095) { - toSend = encoder.Encode(_.text.FromString("FLUSH")); - data[0] = toSend[0]; - data[1] = toSend[1]; - data[2] = toSend[2]; - data[3] = toSend[3]; - data[4] = toSend[4]; - data[5] = 0; - SendBinary(6, data); - } - if (dataRead > 0) { - didWorkLastTick = true; - } - // Obtain! - nextMessage = decoder.PopText(); - while (nextMessage != none) - { - Log("SIZE:" @ nextMessage.GetLength() @ ttt); - StopWatch(false); - nextAMessage = _.avarice.MessageFromText(nextMessage); - nextMessage.FreeSelf(); - nextMessage = nextAMessage.ToText(); - toSend = encoder.Encode(nextMessage); - toSend[toSend.length] = 0; - j = 0; - for (i = 0; i < toSend.length; i += 1) - { - data[j] = toSend[i]; - j += 1; - if (j >= 255) { - j = 0; - SendBinary(255, data); - } - } - if (j > 0) { - SendBinary(j, data); - } - nextMessage.FreeSelf(); - nextMessage = decoder.PopText(); - StopWatch(true); - } -} - -event Opened() -{ - //Log("[TestTcp] Accepted!"); - LOG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); -} - -event Closed() -{ - //Log("[TestTcp] Closed!"); -} - -defaultproperties -{ - infoSuccess = (l=LOG_Info,m="Successfully started Avarice link \"%1\"") - fatalBadPort = (l=LOG_Fatal,m="Bad port \"%1\" specified for Avarice link \"%2\"") - fatalCannotBindPort = (l=LOG_Fatal,m="Cannot bind port for Avarice link \"%1\"") - fatalCannotResolveHost = (l=LOG_Fatal,m="Cannot resolve host \"%1\" for Avarice link \"%2\"") - fatalCannotConnect = (l=LOG_Fatal,m="Connection for Avarice link \"%1\" was rejected") -} \ No newline at end of file diff --git a/sources/Avarice/AvariceTcpStream.uc b/sources/Avarice/AvariceTcpStream.uc new file mode 100644 index 0000000000000000000000000000000000000000..afc2dc7ce7171f9ca5d608bd8894a54324e0fbe9 GIT binary patch literal 26976 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. + */ +class Avarice_OnMessage_Signal extends Signal; + +public final function Emit(AvariceLink link, AvariceMessage message) +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + Avarice_OnMessage_Slot(nextSlot).connect(link, message); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'Avarice_OnMessage_Slot' +} \ No newline at end of file diff --git a/sources/Avarice/Events/Avarice_OnMessage_Slot.uc b/sources/Avarice/Events/Avarice_OnMessage_Slot.uc new file mode 100644 index 0000000..32a750f --- /dev/null +++ b/sources/Avarice/Events/Avarice_OnMessage_Slot.uc @@ -0,0 +1,40 @@ +/** + * Slot class implementation for `Avarice`'s `OnMessage` signal. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class Avarice_OnMessage_Slot extends Slot; + +delegate connect(AvariceLink link, AvariceMessage message) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file