Browse Source

Merge branch 'featAvarice'

pull/8/head
Anton Tarasenko 3 years ago
parent
commit
72f61c033b
  1. 172
      sources/Avarice/Avarice.uc
  2. 76
      sources/Avarice/AvariceAPI.uc
  3. 437
      sources/Avarice/AvariceLink.uc
  4. 97
      sources/Avarice/AvariceMessage.uc
  5. 149
      sources/Avarice/AvariceStreamReader.uc
  6. BIN
      sources/Avarice/AvariceTcpStream.uc
  7. 38
      sources/Avarice/Events/Avarice_OnMessage_Signal.uc
  8. 40
      sources/Avarice/Events/Avarice_OnMessage_Slot.uc
  9. BIN
      sources/Avarice/Tests/TEST_AvariceStreamReader.uc
  10. 16
      sources/Data/Collections/AssociativeArray.uc
  11. 2
      sources/Global.uc
  12. 3
      sources/Manifest.uc
  13. 161
      sources/Text/Codecs/Utf8Decoder.uc
  14. 122
      sources/Text/Codecs/Utf8Encoder.uc
  15. BIN
      sources/Text/Tests/TEST_UTF8EncoderDecoder.uc
  16. 10
      sources/Unreal/NativeActorRef.uc

172
sources/Avarice/Avarice.uc

@ -0,0 +1,172 @@
/**
* 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": "<service_name>", "t": "<command_type>", "p": <any_json_value> }
* where
* * <service_name> describes a particular source of messages
* (it can be a name of the database or an alias for
* a connected application);
* * <command_type> simply states the name of a command, for a database it
* can be "get", "set", "delete", etc..
* * <any_json_value> 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.
*
* 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 <https://www.gnu.org/licenses/>.
*/
class Avarice extends Feature
config(AcediaAvarice);
// The feature itself is dead simple - it simply creates list of
// `AvariceLink` objects, according to its settings, and stores them
var private array<AvariceLink> createdLinks;
struct AvariceLinkRecord
{
var string name;
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<AvariceLinkRecord> 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 const int TECHO, TEND, TCOLON;
var private LoggerAPI.Definition errorBadAddress;
protected function OnEnabled()
{
local int i;
local Text name;
local MutableText host;
local int port;
local AvariceLink nextLink;
for (i = 0; i < link.length; i += 1)
{
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));
}
_.memory.Free(name);
_.memory.Free(host);
}
}
protected function OnDisabled()
{
_.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 MutableText host,
out int port)
{
local bool success;
local Parser parser;
parser = _.text.ParseString(address);
parser.Skip()
.MUntil(host, T(TCOLON).GetCharacter(0))
.Match(T(TCOLON))
.MUnsignedInteger(port)
.Skip();
success = parser.Ok() && parser.GetRemainingLength() == 0;
parser.FreeSelf();
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<AvariceLink> 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\"")
}

76
sources/Avarice/AvariceAPI.uc

@ -0,0 +1,76 @@
/**
* API for Avarice functionality of Acedia.
* Copyright 2020 - 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 <https://www.gnu.org/licenses/>.
*/
class AvariceAPI extends AcediaObject;
/**
* 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<AvariceLink> GetAllLinks()
{
local Avarice avariceFeature;
local array<AvariceLink> emptyResult;
avariceFeature = Avarice(class'Avarice'.static.GetInstance());
if (avariceFeature != none) {
return avariceFeature.GetAllLinks();
}
return emptyResult;
}
/**
* 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)
{
local int i;
local Text nextName;
local array<AvariceLink> 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
{
}

437
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 <https://www.gnu.org/licenses/>.
*/
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 <slot>()
*/
/* 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 <slot>()
*/
/* 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 <slot>()
*/
/* 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 <slot>(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")
}

97
sources/Avarice/AvariceMessage.uc

@ -0,0 +1,97 @@
/**
* 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 <https://www.gnu.org/licenses/>.
*/
class AvariceMessage extends AcediaObject;
// Every message from Avarice has following structure:
// { "s": "<service_name>", "t": "<command_type>", "p": <any_json_value> }
// 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;
super.StaticConstructor();
default.messageTemplate = __().collections.EmptyAssociativeArray();
ResetTemplate(default.messageTemplate);
}
protected function Finalizer()
{
__().memory.Free(type);
__().memory.Free(service);
__().memory.Free(parameters);
type = none;
service = none;
parameters = none;
}
private static final function ResetTemplate(AssociativeArray template)
{
if (template == none) {
return;
}
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 (type == none) return none;
if (service == none) return none;
template = default.messageTemplate;
ResetTemplate(template);
template.SetItem(T(TT), type);
template.SetItem(T(TS), service);
if (parameters != none) {
template.SetItem(T(TP), parameters);
}
result = _.json.Print(template);
return result;
}
defaultproperties
{
TS = 0
stringConstants(0) = "s"
TT = 1
stringConstants(1) = "t"
TP = 2
stringConstants(2) = "p"
}

149
sources/Avarice/AvariceStreamReader.uc

@ -0,0 +1,149 @@
/**
* Helper class meant for reading byte stream sent to us by the Avarice
* application.
* Avarice sends us utf8-encoded JSONs one-by-one, prepending each of them
* with 4 bytes (big endian) that encode the length of the following message.
* 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 <https://www.gnu.org/licenses/>.
*/
class AvariceStreamReader extends AcediaObject;
// Are we currently reading length of the message (`true`) or
// the message itself (`false`)?
var private bool readingLength;
// How many byte we have read so far.
// Resets to zero when we finish reading either length of the message or
// the message itself.
var private int readBytes;
// Expected length of the next message
var private int nextMessageLength;
// Message read so far
var private ByteArrayRef nextMessage;
// All the messages we have fully read, but did not yet return
var private array<MutableText> outputQueue;
// For converting read messages into `MutableText`
var private Utf8Decoder decoder;
// Set to `true` if Avarice input was somehow unacceptable.
// Cannot be recovered from.
var private bool hasFailed;
// Maximum allowed size of JSON message sent from avarice;
// Anything more than that is treated as a mistake.
// TODO: make this configurable
var private const int MAX_MESSAGE_LENGTH;
protected function Constructor()
{
readingLength = true;
nextMessage = ByteArrayRef(_.memory.Allocate(class'ByteArrayRef'));
decoder = Utf8Decoder(_.memory.Allocate(class'Utf8Decoder'));
}
protected function Finalizer()
{
_.memory.FreeMany(outputQueue);
_.memory.Free(nextMessage);
_.memory.Free(decoder);
outputQueue.length = 0;
nextMessage = none;
decoder = none;
hasFailed = false;
}
/**
* Adds next `byte` from the input Avarice stream to the reader.
*
* If input stream signals that message we have to read is too long
* (longer then `MAX_MESSAGE_LENGTH`) - enters a failed state and will
* no longer accept any input. Failed status can be checked with
* `Failed()` method.
* Otherwise cannot fail.
*
* @param nextByte Next byte from the Avarice input stream.
* @return `false` if caller `AvariceStreamReader` is in a failed state
* (including if it entered one after pushing this byte)
* and `true` otherwise.
*/
public final function bool PushByte(byte nextByte)
{
if (hasFailed) {
return false;
}
if (readingLength)
{
// Make space for the next 8 bits by shifting previously recorded ones
nextMessageLength = nextMessageLength << 8;
nextMessageLength += nextByte;
readBytes += 1;
if (readBytes >= 4)
{
readingLength = false;
readBytes = 0;
}
// Message either too long or so long it overfilled `MaxInt`
if ( nextMessageLength > MAX_MESSAGE_LENGTH
|| nextMessageLength < 0)
{
hasFailed = true;
return false;
}
return true;
}
nextMessage.AddItem(nextByte);
readBytes += 1;
if (readBytes >= nextMessageLength)
{
outputQueue[outputQueue.length] = decoder.Decode(nextMessage);
nextMessage.Empty();
readingLength = true;
readBytes = 0;
nextMessageLength = 0;
}
return true;
}
/**
* Returns all complete messages read so far.
*
* Even if caller `AvariceStreamReader` entered a failed state - this method
* will return all the messages read before it has failed.
*
* @return aAl complete messages read from Avarice stream so far
*/
public final function array<MutableText> PopMessages()
{
local array<MutableText> result;
result = outputQueue;
outputQueue.length = 0;
return result;
}
/**
* Is caller `AvariceStreamReader` in a failed state?
* See `PushByte()` method for details.
*
* @return `true` iff caller `AvariceStreamReader` has failed.
*/
public final function bool Failed()
{
return hasFailed;
}
defaultproperties
{
MAX_MESSAGE_LENGTH = 26214400 // 25 * 1024 * 1024 = 25MB
}

BIN
sources/Avarice/AvariceTcpStream.uc

Binary file not shown.

38
sources/Avarice/Events/Avarice_OnMessage_Signal.uc

@ -0,0 +1,38 @@
/**
* Signal 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 <https://www.gnu.org/licenses/>.
*/
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'
}

40
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

BIN
sources/Avarice/Tests/TEST_AvariceStreamReader.uc

Binary file not shown.

16
sources/Data/Collections/AssociativeArray.uc

@ -319,13 +319,23 @@ public final function Entry TakeEntry(AcediaObject key)
* Returned value is no longer managed by the `AssociativeArray` (if it was) * Returned value is no longer managed by the `AssociativeArray` (if it was)
* and must be deallocated once you do not need it anymore. * and must be deallocated once you do not need it anymore.
* *
* @param key Key for which to return value. * @param key Key for which to return value.
* @param freeKey Setting this to `true` will also free the key item was
* stored with. Passed argument `key` will not be deallocated, unless it is
* the exact same object as item's key inside caller collection.
* @return Value, stored with given key `key`. If there is no value with * @return Value, stored with given key `key`. If there is no value with
* such a key method will return `none`. * such a key method will return `none`.
*/ */
public final function AcediaObject TakeItem(AcediaObject key) public final function AcediaObject TakeItem(
AcediaObject key,
optional bool freeKey)
{ {
return TakeEntry(key).value; local Entry entry;
entry = TakeEntry(key);
if (freeKey) {
_.memory.Free(entry.key);
}
return entry.value;
} }
/** /**

2
sources/Global.uc

@ -40,6 +40,7 @@ var public UserAPI users;
var public PlayersAPI players; var public PlayersAPI players;
var public JSONAPI json; var public JSONAPI json;
var public DBAPI db; var public DBAPI db;
var public AvariceAPI avarice;
var public KFFrontend kf; var public KFFrontend kf;
@ -74,6 +75,7 @@ protected function Initialize()
players = PlayersAPI(memory.Allocate(class'PlayersAPI')); players = PlayersAPI(memory.Allocate(class'PlayersAPI'));
json = JSONAPI(memory.Allocate(class'JSONAPI')); json = JSONAPI(memory.Allocate(class'JSONAPI'));
db = DBAPI(memory.Allocate(class'DBAPI')); db = DBAPI(memory.Allocate(class'DBAPI'));
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI'));
kf = KFFrontend(memory.Allocate(class'KF1_Frontend')); kf = KFFrontend(memory.Allocate(class'KF1_Frontend'));
json.StaticConstructor(); json.StaticConstructor();
} }

3
sources/Manifest.uc

@ -23,6 +23,7 @@
defaultproperties defaultproperties
{ {
features(0) = class'Commands_Feature' features(0) = class'Commands_Feature'
features(1) = class'Avarice'
commands(0) = class'ACommandHelp' commands(0) = class'ACommandHelp'
commands(1) = class'ACommandDosh' commands(1) = class'ACommandDosh'
commands(2) = class'ACommandNick' commands(2) = class'ACommandNick'
@ -56,4 +57,6 @@ defaultproperties
testCases(21) = class'TEST_LogMessage' testCases(21) = class'TEST_LogMessage'
testCases(22) = class'TEST_LocalDatabase' testCases(22) = class'TEST_LocalDatabase'
testCases(23) = class'TEST_FeatureConfig' testCases(23) = class'TEST_FeatureConfig'
testCases(24) = class'TEST_UTF8EncoderDecoder'
testCases(25) = class'TEST_AvariceStreamReader'
} }

161
sources/Text/Codecs/Utf8Decoder.uc

@ -0,0 +1,161 @@
/**
* Class for decoding UTF8 byte stream into Acedia's `MutableText` value.
* This is a separate object instead of just a method, because it allows
* to make code simpler by storing state variables related to
* the decoding process.
* This implementation should correctly convert any valid UTF8, but it is
* not guaranteed to reject any invalid UTF8. In particular, it accepts
* overlong code point encodings. It does check whether every byte has
* a correct bit prefix and does not attempt to repair input data if it finds
* invalid one.
* See [wiki page](https://en.wikipedia.org/wiki/UTF-8) for details.
* 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 <https://www.gnu.org/licenses/>.
*/
class Utf8Decoder extends AcediaObject;
// Variables for building a multi-byte code point.
// Stored as a class member variables to avoid copying them between methods.
var private MutableText builtText;
var private int nextCodePoint;
var private int innerBytesLeft;
// These masks (`maskDropN`) allow to turn into zero first `N` bits in
// the byte with `&` operator.
var private byte maskDrop1, maskDrop2, maskDrop3, maskDrop4, maskDrop5;
// These masks (`maskTakeN`) allow to turn into zero all but first `N` bits
// in the byte with `&` operator.
// `maskTakeN == ~maskDropN`.
var private byte maskTake1, maskTake2, maskTake3, maskTake4, maskTake5;
/**
* Decodes passed `byte` array (that contains utf8-encoded text) into
* the `MutableText` type.
*
* @param byteStream Byte stream to decode.
* @return `MutableText` that contains `byteStream`'s text data.
* `none` iff either `byteStream == none` or it's contents do not
* correspond to a (valid) utf8-encoded text.
*/
public final function MutableText Decode(ByteArrayRef byteStream)
{
local int i;
local int length;
local MutableText result;
if (byteStream == none) {
return none;
}
nextCodePoint = 0;
innerBytesLeft = 0;
builtText = _.text.Empty();
length = byteStream.GetLength();
for (i = 0; i < length; i += 1)
{
if (!PushByte(byteStream.GetItem(i)))
{
_.memory.Free(builtText);
return none;
}
}
if (innerBytesLeft <= 0) {
result = builtText;
}
else {
_.memory.Free(builtText);
}
builtText = none;
return result;
}
private final function bool PushByte(byte nextByte)
{
if (innerBytesLeft > 0) {
return PushInnerByte(nextByte);
}
// Form of 0xxxxxxx means 1 byte per code point
if ((nextByte & maskTake1) == 0)
{
AppendCodePoint(nextByte);
return true;
}
// Form of 110xxxxx means 2 bytes per code point
if ((nextByte & maskTake3) == maskTake2) // maskTake2 == 1 1 0 0 0 0 0 0
{
nextCodePoint = nextByte & maskDrop3;
innerBytesLeft = 1;
return true;
}
// Form of 1110xxxx means 3 bytes per code point
if ((nextByte & maskTake4) == maskTake3) // maskTake3 == 1 1 1 0 0 0 0 0
{
nextCodePoint = nextByte & maskDrop4;
innerBytesLeft = 2;
return true;
}
// Form of 11110xxx means 4 bytes per code point
if ((nextByte & maskTake5) == maskTake4) // maskTake4 == 1 1 1 1 0 0 0 0
{
nextCodePoint = nextByte & maskDrop5;
innerBytesLeft = 3;
return true;
}
// `nextByte` must have has one of the above forms
// (or 10xxxxxx that is handled in `PushInnerByte()`)
return false;
}
// This method is responsible for pushing "inner" bytes: bytes that come
// after the first one when code point is encoded with multiple bytes.
// All of them are expected to have 10xxxxxx prefix.
// Assumes `innerBytesLeft > 0` to avoid needless checks.
private final function bool PushInnerByte(byte nextByte)
{
// Fail if `nextByte` does not have an expected form: 10xxxxxx
if ((nextByte & maskTake2) != maskTake1) {
return false;
}
// Since inner bytes have the form of 10xxxxxx, they all carry only 6 bits
// that actually encode code point, so to make space for those bits we must
// shift previously added code points by `6`
nextCodePoint = (nextCodePoint << 6) + (nextByte & maskDrop2);
innerBytesLeft -= 1;
if (innerBytesLeft <= 0) {
AppendCodePoint(nextCodePoint);
}
return true;
}
private final function AppendCodePoint(int codePoint)
{
local Text.Character nextCharacter;
nextCharacter.codePoint = codePoint;
builtText.AppendCharacter(nextCharacter);
}
defaultproperties
{
maskDrop1 = 127 // 0 1 1 1 1 1 1 1
maskDrop2 = 63 // 0 0 1 1 1 1 1 1
maskDrop3 = 31 // 0 0 0 1 1 1 1 1
maskDrop4 = 15 // 0 0 0 0 1 1 1 1
maskDrop5 = 7 // 0 0 0 0 0 1 1 1
maskTake1 = 128 // 1 0 0 0 0 0 0 0
maskTake2 = 192 // 1 1 0 0 0 0 0 0
maskTake3 = 224 // 1 1 1 0 0 0 0 0
maskTake4 = 240 // 1 1 1 1 0 0 0 0
maskTake5 = 248 // 1 1 1 1 1 0 0 0
}

122
sources/Text/Codecs/Utf8Encoder.uc

@ -0,0 +1,122 @@
/**
* Class for encoding Acedia's `MutableText` value into UTF8 byte
* representation.
* This is a separate object instead of just a method to match design of
* `Utf8Decoder`.
* See [wiki page](https://en.wikipedia.org/wiki/UTF-8) for details.
* 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 <https://www.gnu.org/licenses/>.
*/
class Utf8Encoder extends AcediaObject;
// Limits on code point values that can be recorded with 1, 2, 3 and 4 bytes
// respectively
var private int utfLimit1, utfLimit2, utfLimit3, utfLimit4;
// Bit prefixes for UTF8 encoding
var private int utfMask2, utfMask3, utfMask4, utfMaskIn;
// This integer will have only 6 last bits be 1s.
// We need it to zero all but last 6 bits for `int`s (with `&` bit operator).
var private int lastSixBits;
/**
* Encodes passed `Text` object into UTF8 byte representation.
*
* In case passed `text` is somehow broken and contains invalid Unicode
* code points - this method will return empty array.
*
* @param text `Text` object to encode.
* @return UTF8 representation of passed `text` inside `ByteArrayRef`.
* `none` iff `text == none` or `text` contains invalid Unicode
* code points.
*/
public final function ByteArrayRef Encode(Text text)
{
local int i, nextCodepoint, textLength;
local ByteArrayRef buffer;
if (__().text.IsEmpty(text)) {
return none; // empty array
}
buffer = ByteArrayRef(_.memory.Allocate(class'ByteArrayRef'));
textLength = text.GetLength();
for (i = 0; i < textLength; i += 1)
{
nextCodepoint = text.GetCharacter(i).codePoint;
if (nextCodepoint <= utfLimit1) {
buffer.AddItem(nextCodepoint);
}
else if (nextCodepoint <= utfLimit2)
{
// Drop 6 bits that will be recorded inside second byte and
// add 2-byte sequence mask
buffer.AddItem(utfMask2 | (nextCodepoint >> 6));
// Take only last 6 bits for the second (last) byte
// + add inner-byte sequence mask
buffer.AddItem(utfMaskIn | (nextCodepoint & lastSixBits));
}
else if (nextCodepoint <= utfLimit3)
{
// Drop 12 bits that will be recorded inside second and third bytes
// and add 3-byte sequence mask
buffer.AddItem(utfMask3 | (nextCodepoint >> 12));
// Drop 6 bits that will be recorded inside third byte and
// add inner-byte sequence mask
buffer.AddItem(utfMaskIn | ((nextCodepoint >> 6) & lastSixBits));
// Take only last 6 bits for the third (last) byte
// + add inner-byte sequence mask
buffer.AddItem(utfMaskIn | (nextCodepoint & lastSixBits));
}
else if (nextCodepoint <= utfLimit4)
{
// Drop 18 bits that will be recorded inside second, third and
// fourth bytes, then add 4-byte sequence mask
buffer.AddItem(utfMask4 | (nextCodepoint >> 18));
// Drop 12 bits that will be recorded inside third and fourth bytes
// and add inner-byte sequence mask
buffer.AddItem(utfMaskIn | ((nextCodepoint >> 12) & lastSixBits));
// Drop 6 bits that will be recorded inside fourth byte
// and add inner-byte sequence mask
buffer.AddItem(utfMaskIn | ((nextCodepoint >> 6) & lastSixBits));
// Take only last 6 bits for the fourth (last) byte
// + add inner-byte sequence mask
buffer.AddItem(utfMaskIn | (nextCodepoint & lastSixBits));
}
else
{
// Outside of known Unicode range
// Should not be possible, since `Text` is expected to
// contain only correct Unicode
_.memory.Free(buffer);
buffer = none;
break;
}
}
return buffer;
}
defaultproperties
{
utfLimit1 = 127
utfLimit2 = 2047
utfLimit3 = 65535
utfLimit4 = 1114111
utfMask2 = 192 // 1 1 0 0 0 0 0 0
utfMask3 = 224 // 1 1 1 0 0 0 0 0
utfMask4 = 240 // 1 1 1 1 0 0 0 0
utfMaskIn = 128 // 1 0 0 0 0 0 0 0
lastSixBits = 63 // 0 0 1 1 1 1 1 1
}

BIN
sources/Text/Tests/TEST_UTF8EncoderDecoder.uc

Binary file not shown.

10
sources/Unreal/NativeActorRef.uc

@ -80,13 +80,11 @@ public final function NativeActorRef Set(Actor newValue)
public function bool IsEqual(Object other) public function bool IsEqual(Object other)
{ {
local NativeActorRef otherBox; local NativeActorRef otherBox;
local ActorService service;
otherBox = NativeActorRef(other); otherBox = NativeActorRef(other);
if (otherBox == none) return false; if (otherBox == none) {
service = ActorService(class'ActorService'.static.Require()); return false;
if (service == none) return false; }
return Get() == otherBox.Get(); return Get() == otherBox.Get();
} }

Loading…
Cancel
Save