Anton Tarasenko
3 years ago
16 changed files with 1314 additions and 9 deletions
@ -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\"") |
||||||
|
} |
@ -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 |
||||||
|
{ |
||||||
|
} |
@ -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") |
||||||
|
} |
@ -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" |
||||||
|
} |
@ -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 |
||||||
|
} |
Binary file not shown.
@ -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' |
||||||
|
} |
@ -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 |
||||||
|
{ |
||||||
|
} |
Binary file not shown.
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
Binary file not shown.
Loading…
Reference in new issue