From dbd81e04ef2748420637ab5ee249469cec976dad Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 18 Jul 2021 17:10:18 +0700 Subject: [PATCH 1/6] Initial Avarice feature commit --- sources/Avarice/Avarice.uc | 80 ++++++ sources/Avarice/AvariceAPI.uc | 54 ++++ sources/Avarice/AvariceClient.uc | 88 ++++++ sources/Avarice/AvariceMessage.uc | 93 +++++++ sources/Avarice/AvariceTCPLink.uc | 170 ++++++++++++ .../Avarice/Tests/TEST_UTF8EncoderDecoder.uc | Bin 0 -> 16540 bytes sources/Avarice/Utf8Decoder.uc | 260 ++++++++++++++++++ sources/Avarice/Utf8Encoder.uc | 121 ++++++++ sources/Global.uc | 2 + sources/Manifest.uc | 2 + 10 files changed, 870 insertions(+) create mode 100644 sources/Avarice/Avarice.uc create mode 100644 sources/Avarice/AvariceAPI.uc create mode 100644 sources/Avarice/AvariceClient.uc create mode 100644 sources/Avarice/AvariceMessage.uc create mode 100644 sources/Avarice/AvariceTCPLink.uc create mode 100644 sources/Avarice/Tests/TEST_UTF8EncoderDecoder.uc create mode 100644 sources/Avarice/Utf8Decoder.uc create mode 100644 sources/Avarice/Utf8Encoder.uc diff --git a/sources/Avarice/Avarice.uc b/sources/Avarice/Avarice.uc new file mode 100644 index 0000000..1209199 --- /dev/null +++ b/sources/Avarice/Avarice.uc @@ -0,0 +1,80 @@ +/** + * + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class Avarice extends Feature + config(AcediaAvarice); + +struct AvariceLink +{ + var string name; + var string host; +}; + +var private config array link; + +var private LoggerAPI.Definition errorBadAddress; + +protected function OnEnabled() +{ + local int i; + local string host; + local int port; + local AvariceTCPLink nextTCPLink; + for (i = 0; i < link.length; i += 1) + { + if (!ParseAddress(link[i].host, host, port)) { + _.logger.Auto(errorBadAddress).Arg(_.text.FromString(link[i].name)); + } + nextTCPLink = AvariceTCPLink(_.memory.Allocate(class'AvariceTCPLink')); + nextTCPLink.Connect(link[i].name, host, port); + } +} + +protected function OnDisabled() +{ + local LevelInfo level; + local AvariceTCPLink nextTCPLink; + level = _.unreal.GetLevel(); + foreach level.DynamicActors(class'AvariceTCPLink', nextTCPLink) { + nextTCPLink.Destroy(); + } +} + +private final function bool ParseAddress( + string address, + out string host, + out int port) +{ + local bool success; + local Parser parser; + parser = _.text.ParseString(address); + parser.Skip() + .MUntilS(host, _.text.GetCharacter(":")) + .MatchS(":") + .MUnsignedInteger(port) + .Skip(); + success = parser.Ok() && parser.GetRemainingLength() == 0; + parser.FreeSelf(); + return success; +} + +defaultproperties +{ + errorBadAddress = (l=LOG_Error,m="Cannot parse address \"%1\"") +} \ No newline at end of file diff --git a/sources/Avarice/AvariceAPI.uc b/sources/Avarice/AvariceAPI.uc new file mode 100644 index 0000000..1f06036 --- /dev/null +++ b/sources/Avarice/AvariceAPI.uc @@ -0,0 +1,54 @@ +/** + * 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 . + */ +class AvariceAPI extends AcediaObject; + +public final function AvariceMessage MessageFromText(Text message) +{ + local Parser parser; + local AvariceMessage result; + local AssociativeArray parsedMessage; + if (message == none) return none; + parser = _.text.Parse(message); + parsedMessage = _.json.ParseObjectWith(parser); + parser.FreeSelf(); + if (!HasNecessaryMessageKeys(parsedMessage)) + { + _.memory.Free(parsedMessage); + return none; + } + result = AvariceMessage(_.memory.Allocate(class'AvariceMessage')); + result.SetID(parsedMessage.GetText(P("i"))); + result.SetGroup(parsedMessage.GetText(P("g"))); + result.data = parsedMessage.TakeItem(P("p")); + _.memory.Free(parsedMessage); + return result; +} + +private final function bool HasNecessaryMessageKeys(AssociativeArray message) +{ + if (message == none) return false; + if (!message.HasKey(P("i"))) return false; + if (!message.HasKey(P("g"))) return false; + + return true; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Avarice/AvariceClient.uc b/sources/Avarice/AvariceClient.uc new file mode 100644 index 0000000..4b37733 --- /dev/null +++ b/sources/Avarice/AvariceClient.uc @@ -0,0 +1,88 @@ +class AvariceClient extends AcediaObject; + +enum AvariceClientState +{ + ACS_Waiting, + ACS_ReadingID, + ACS_ReadingLength, + ACS_ReadingPayload, + ACS_Invalid +}; + +var private int currentID; +var private int currentMessageLength; +var private array currentPayload; + +var private AvariceClientState currentState; +var private int bytesLeftToRead; +var private byte buffer[255]; +var private array longBuffer; +var private int pendingBytes; + +public final function PushByte(byte nextByte) +{ + if (nextByte == 0) + { + if (bytesLeftToRead > 0) + { + // ACK for short message (with id) + } + currentState = ACS_Waiting; + ResetBuffer(); + return; + } + else if (currentState == ACS_Invalid) + { + // ACK of invalid message's end + return; + } + else if (currentState == ACS_Waiting) + { + currentID = nextByte; + currentID = currentID << 8; + currentState = ACS_ReadingID; + } + else if (currentState == ACS_ReadingID) + { + currentID += nextByte; + currentState = ACS_ReadingLength; + bytesLeftToRead = 2; + } + else if (currentState == ACS_ReadingLength) + { + bytesLeftToRead -= 1; + if (bytesLeftToRead > 0) + { + currentMessageLength = nextByte; + currentMessageLength = currentMessageLength << 8; + } + else + { + currentMessageLength += nextByte; + currentState = ACS_ReadingPayload; + bytesLeftToRead = currentMessageLength; + } + } + else if (currentState == ACS_ReadingPayload) + { + currentPayload[currentPayload.length] = nextByte; + // Decode payload into `AvariceMessage` + // Send messages via Acedia's signals + bytesLeftToRead -= 1; + if (bytesLeftToRead == 0) + { + currentState = ACS_Waiting; + // ACK into buffer + } + } +} + +private final function ResetBuffer() +{ + pendingBytes = 0; + longBuffer.length = 0; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Avarice/AvariceMessage.uc b/sources/Avarice/AvariceMessage.uc new file mode 100644 index 0000000..42551d2 --- /dev/null +++ b/sources/Avarice/AvariceMessage.uc @@ -0,0 +1,93 @@ +class AvariceMessage extends AcediaObject; + +var private Text messageID; +var private Text messageGroup; + +var public AcediaObject data; + +var private AssociativeArray messageTemplate; + +public static function StaticConstructor() +{ + if (StaticConstructorGuard()) return; + super.StaticConstructor(); + + default.messageTemplate = __().collections.EmptyAssociativeArray(); + ResetTemplate(default.messageTemplate); +} + +protected function Finalizer() +{ + __().memory.Free(messageID); + __().memory.Free(messageGroup); + __().memory.Free(data); + messageID = none; + messageGroup = none; + data = none; +} + +private static final function ResetTemplate(AssociativeArray template) +{ + if (template == none) { + return; + } + template.SetItem(P("i"), none); + template.SetItem(P("g"), none); + template.SetItem(P("p"), none); +} + +public final function SetID(Text id) +{ + _.memory.Free(messageID); + messageID = none; + if (id != none) { + messageID = id.Copy(); + } +} + +public final function Text GetID() +{ + if (messageID != none) { + return messageID.Copy(); + } + return none; +} + +public final function SetGroup(Text group) +{ + _.memory.Free(messageGroup); + messageGroup = none; + if (group != none) { + messageGroup = group.Copy(); + } +} + +public final function Text GetGroup() +{ + if (messageGroup != none) { + return messageGroup.Copy(); + } + return none; +} + +public final function MutableText ToText() +{ + local MutableText result; + local AssociativeArray template; + if (messageID == none) return none; + if (messageGroup == none) return none; + + template = default.messageTemplate; + template.SetItem(P("i"), messageID); + template.SetItem(P("g"), messageGroup); + if (data != none) { + template.SetItem(P("p"), data); + } + result = _.json.Print(template); + ResetTemplate(template); + return result; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Avarice/AvariceTCPLink.uc b/sources/Avarice/AvariceTCPLink.uc new file mode 100644 index 0000000..335a1c8 --- /dev/null +++ b/sources/Avarice/AvariceTCPLink.uc @@ -0,0 +1,170 @@ +class AvariceTcpLink extends TcpLink + dependson(LoggerAPI); + +var private Global _; + +var private string linkName; +var private string linkHost; +var private int linkPort; +var private IpAddr remoteAddress; +var private int ttt; + +var private bool didWorkLastTick; + +var private array buffer; + +var private Utf8Encoder encoder; +var private Utf8Decoder decoder; + +var private LoggerAPI.Definition infoSuccess; +var private LoggerAPI.Definition fatalBadPort; +var private LoggerAPI.Definition fatalCannotBindPort; +var private LoggerAPI.Definition fatalCannotResolveHost; +var private LoggerAPI.Definition fatalCannotConnect; + +public final function bool Connect(string name, string host, int port) +{ + local InternetLink.IpAddr ip; + local int usedPort; + // Apparently `TcpLink` ignores default values for these variables, + // so we set them here + linkMode = MODE_Binary; + receiveMode = RMODE_Manual; + _ = class'Global'.static.GetInstance(); + encoder = Utf8Encoder(_.memory.Allocate(class'Utf8Encoder')); + decoder = Utf8Decoder(_.memory.Allocate(class'Utf8Decoder')); + linkName = name; + linkHost = host; + linkPort = port; + if (port <= 0) + { + _.logger.Auto(fatalBadPort) + .ArgInt(port) + .Arg(_.text.FromString(linkName)); + return false; + } + if (BindPort(, true) <= 0) + { + _.logger.Auto(fatalCannotBindPort) + .ArgInt(port) + .Arg(_.text.FromString(name)); + return false; + } + StringToIpAddr(host, remoteAddress); + remoteAddress.port = port; + if (remoteAddress.addr == 0) { + Resolve(host); + } + else { + OpenAddress(); + } + return true; +} + +event Resolved(IpAddr resolvedAddress) +{ + remoteAddress.addr = resolvedAddress.addr; + OpenAddress(); +} + +private final function bool OpenAddress() +{ + if (!OpenNoSteam(remoteAddress)) { + _.logger.Auto(fatalCannotConnect).Arg(_.text.FromString(linkName)); + } + _.logger.Auto(infoSuccess).Arg(_.text.FromString(linkName)); +} + +event ResolveFailed() +{ + _.logger.Auto(fatalCannotResolveHost).Arg(_.text.FromString(linkHost)); + // !Shut down! +} + +event Tick(float delta) +{ + local array toSend; + local AvariceMessage nextAMessage; + local MutableText nextMessage; + local int i, j, dataRead, totalRead, iter; + local byte data[255]; + if (didWorkLastTick) + { + didWorkLastTick = false; + return; + } + if (!IsDataPending()) { + return; + } + while (true) { + dataRead = ReadBinary(255, data); + for (i = 0; i < dataRead; i += 1) { + ttt += 1; + decoder.PushByte(data[i]); + } + if (dataRead <= 0) { + break; + } + } + if (ttt >= 4095) { + toSend = encoder.Encode(_.text.FromString("FLUSH")); + data[0] = toSend[0]; + data[1] = toSend[1]; + data[2] = toSend[2]; + data[3] = toSend[3]; + data[4] = toSend[4]; + data[5] = 0; + SendBinary(6, data); + } + if (dataRead > 0) { + didWorkLastTick = true; + } + // Obtain! + nextMessage = decoder.PopText(); + while (nextMessage != none) + { + Log("SIZE:" @ nextMessage.GetLength() @ ttt); + StopWatch(false); + nextAMessage = _.avarice.MessageFromText(nextMessage); + nextMessage.FreeSelf(); + nextMessage = nextAMessage.ToText(); + toSend = encoder.Encode(nextMessage); + toSend[toSend.length] = 0; + j = 0; + for (i = 0; i < toSend.length; i += 1) + { + data[j] = toSend[i]; + j += 1; + if (j >= 255) { + j = 0; + SendBinary(255, data); + } + } + if (j > 0) { + SendBinary(j, data); + } + nextMessage.FreeSelf(); + nextMessage = decoder.PopText(); + StopWatch(true); + } +} + +event Opened() +{ + //Log("[TestTcp] Accepted!"); + LOG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); +} + +event Closed() +{ + //Log("[TestTcp] Closed!"); +} + +defaultproperties +{ + infoSuccess = (l=LOG_Info,m="Successfully started Avarice link \"%1\"") + fatalBadPort = (l=LOG_Fatal,m="Bad port \"%1\" specified for Avarice link \"%2\"") + fatalCannotBindPort = (l=LOG_Fatal,m="Cannot bind port for Avarice link \"%1\"") + fatalCannotResolveHost = (l=LOG_Fatal,m="Cannot resolve host \"%1\" for Avarice link \"%2\"") + fatalCannotConnect = (l=LOG_Fatal,m="Connection for Avarice link \"%1\" was rejected") +} \ No newline at end of file diff --git a/sources/Avarice/Tests/TEST_UTF8EncoderDecoder.uc b/sources/Avarice/Tests/TEST_UTF8EncoderDecoder.uc new file mode 100644 index 0000000000000000000000000000000000000000..950d0daa77987d328e49cd106741cee99777b141 GIT binary patch literal 16540 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 Ezb943E&u=k literal 0 HcmV?d00001 diff --git a/sources/Avarice/Utf8Decoder.uc b/sources/Avarice/Utf8Decoder.uc new file mode 100644 index 0000000..89f677a --- /dev/null +++ b/sources/Avarice/Utf8Decoder.uc @@ -0,0 +1,260 @@ +/** + * Class for decoding UTF8 byte stream into Acedia's `MutableText` value. + * It is made to work with incoming, and possibly incomplete, streams of + * bytes: instead of consuming the whole utf8 text, it is made to consume it + * byte-by-byte and store `MutableText`s that it parsed from the stream + * (assumes that separate `MutableText`s are separated by `0` byte). + * 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 (except overlong encoding of zero). + * It, however, 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 . + */ +class Utf8Decoder extends AcediaObject; + +/** + * `Utf8Decoder` consumes byte by byte with `PushByte()` method and it's + * algorithm is simple: + * 1. If it encounters a byte that encodes a singular code point by + * itself (starts with `0` bit) - it is added as a codepoint; + * 2. If it encounters byte which indicates that next code point is + * composed out of several bytes (starts with 110, 1110 or 11110) - + * remembers that it has to read several "inner" bytes belonging to + * the same code point and starts to expect them instead; + * 3. If it ever encounters a byte with unexpected (and thus invalid) + * bit prefix - enters a failed state; + * 4. If it ever encounters a `0` byte: + * * If it was not in a failed state - records `MutableText` + * accumulated so far; + * * Clears failed state. + */ + +var private bool failedState; + +// Variables for building a multi-byte code point +var private int nextCodePoint; +var private int innerBytesLeft; + +// `MutableText` we are building right now +var private MutableText nextText; +// `MutableText`s we have already built +var private array outputQueue; + +// 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; + +protected function Constructor() +{ + nextText = _.text.Empty(); +} + +protected function Finalizer() +{ + _.memory.Free(nextText); + _.memory.FreeMany(outputQueue); + nextText = none; + failedState = false; + outputQueue.length = 0; + innerBytesLeft = 0; + nextCodePoint = 0; +} + +/** + * Checks whether data in the `MutableText` that caller `Utf8Decoder` is + * currently filling was detected to be invalid. + * + * This state can be reset by pushing `0` byte into caller `Utf8Decoder`. + * See `PushByte()` for more info. + * + * @return `true` iff caller `Utf8Decoder` is not in a failed state. + */ +public final function bool Failed() +{ + return failedState; +} + +/** + * Checks whether caller `Utf8Decoder` has any data put in + * the `MutableText` it is currently building. + * Result is guaranteed to be `false` after `self.PushByte(0)` call, since + * it starts a brand new `MutableText`. + */ +public final function bool HasUnfinishedData() +{ + if (innerBytesLeft > 0) return true; + if (nextText.GetLength() > 0) return true; + return false; +} + +/** + * Returns next `MutableText` that was successfully decoded by + * the caller `Utf8Decoder`, removing it from the output queue. + * + * @return Next `MutableText` in the caller `Utf8Decoder`'s output queue. + * `none` iff output queue is empty. `MutableText`s are returned in order + * they were decoded. + */ +public final function MutableText PopText() +{ + local MutableText result; + if (outputQueue.length <= 0) { + return none; + } + result = outputQueue[0]; + outputQueue.Remove(0, 1); + return result; +} + +/** + * Adds next `byte` from the byte stream that is supposed to encode UTF8 text. + * To finish building `MutableText` pass `0` byte into this method, which will + * `MutableText` built so far into an "output queue" (accessible with + * `PopText()`) and start building a new one. + * + * This method expects `byte`s, in order, from a sequence that has correct + * UTF8 encoding. If method detects incorrect UTF8 sequence - it will be put + * into a "failed state", discarding `MutableText` it was currently building, + * along with any further input (except `0` byte). + * Pushing `0` byte will restore `Utf8Decoder` from a failed state and it + * will start building a new `MutableText`. + * + * @param nextByte next byte from byte stream that is supposed to encode + * UTF8 text. `0` will make caller `Utf8Decoder` start building new + * `MutableText`. + * @return `true` iff caller `Utf8Decoder` was not in a failed state and + * operation was successful. + */ +public final function bool PushByte(byte nextByte) +{ + if (nextByte == 0) return QueueCurrentText(); + if (failedState) return false; + 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()`) + failedState = true; + 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` and `failedState == false` +// 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) + { + failedState = true; + 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) + { + // We forbid overlong encoding of `0` + // (as does the Unicode standard) + if (nextCodePoint == 0) + { + failedState = true; + return false; + } + AppendCodePoint(nextCodePoint); + } + return true; +} + +private final function AppendCodePoint(int codePoint) +{ + local Text.Character nextCharacter; + nextCharacter.codePoint = codePoint; + nextText.AppendCharacter(nextCharacter); +} + +// Return `true` if `MutableText` was added to the queue +// (there were no encoding errors) +private final function bool QueueCurrentText() +{ + local bool result; + // If we still do not have all bytes for the character we were building - + // then passed UTF8 was invalid + failedState = failedState || innerBytesLeft > 0; + result = !failedState; + if (failedState) { + _.memory.Free(nextText); + } + else { + outputQueue[outputQueue.length] = nextText; + } + failedState = false; + innerBytesLeft = 0; + nextText = _.text.Empty(); + return result; +} + +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 +} \ No newline at end of file diff --git a/sources/Avarice/Utf8Encoder.uc b/sources/Avarice/Utf8Encoder.uc new file mode 100644 index 0000000..e66321d --- /dev/null +++ b/sources/Avarice/Utf8Encoder.uc @@ -0,0 +1,121 @@ +/** + * Class for encoding Acedia's `MutableText` value into UTF8 byte + * representation. + * 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 . + */ +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` as an array of `byte`s. + * Empty array if `text == none` or `text` contains invalid Unicode + * code points. + */ +public final function array Encode(Text text) +{ + local int i, nextCodepoint, textLength; + local array buffer; + if (__().text.IsEmpty(text)) { + return buffer; // empty array + } + textLength = text.GetLength(); + for (i = 0; i < textLength; i += 1) + { + nextCodepoint = text.GetCharacter(i).codePoint; + if (nextCodepoint <= utfLimit1) { + buffer[buffer.length] = nextCodepoint; + } + else if (nextCodepoint <= utfLimit2) + { + // Drop 6 bits that will be recorded inside second byte and + // add 2-byte sequence mask + buffer[buffer.length] = utfMask2 | (nextCodepoint >> 6); + // Take only last 6 bits for the second (last) byte + // + add inner-byte sequence mask + buffer[buffer.length] = 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[buffer.length] = utfMask3 | (nextCodepoint >> 12); + // Drop 6 bits that will be recorded inside third byte and + // add inner-byte sequence mask + buffer[buffer.length] = + utfMaskIn | ((nextCodepoint >> 6) & lastSixBits); + // Take only last 6 bits for the third (last) byte + // + add inner-byte sequence mask + buffer[buffer.length] = 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[buffer.length] = utfMask4 | (nextCodepoint >> 18); + // Drop 12 bits that will be recorded inside third and fourth bytes + // and add inner-byte sequence mask + buffer[buffer.length] = + utfMaskIn | ((nextCodepoint >> 12) & lastSixBits); + // Drop 6 bits that will be recorded inside fourth byte + // and add inner-byte sequence mask + buffer[buffer.length] = + utfMaskIn | ((nextCodepoint >> 6) & lastSixBits); + // Take only last 6 bits for the fourth (last) byte + // + add inner-byte sequence mask + buffer[buffer.length] = utfMaskIn | (nextCodepoint & lastSixBits); + } + else + { + // Outside of known Unicode range + // Should not be possible, since `Text` is expected to + // contain only correct Unicode + buffer.length = 0; + 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 +} \ No newline at end of file diff --git a/sources/Global.uc b/sources/Global.uc index bf1a4e4..e475a65 100644 --- a/sources/Global.uc +++ b/sources/Global.uc @@ -40,6 +40,7 @@ var public UserAPI users; var public PlayersAPI players; var public JSONAPI json; var public DBAPI db; +var public AvariceAPI avarice; var public KFFrontend kf; @@ -74,6 +75,7 @@ protected function Initialize() players = PlayersAPI(memory.Allocate(class'PlayersAPI')); json = JSONAPI(memory.Allocate(class'JSONAPI')); db = DBAPI(memory.Allocate(class'DBAPI')); + avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); kf = KFFrontend(memory.Allocate(class'KF1_Frontend')); json.StaticConstructor(); } \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index f0353db..f0629aa 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -23,6 +23,7 @@ defaultproperties { features(0) = class'Commands' + features(1) = class'Avarice' commands(0) = class'ACommandHelp' commands(1) = class'ACommandDosh' commands(2) = class'ACommandNick' @@ -55,4 +56,5 @@ defaultproperties testCases(20) = class'TEST_CommandDataBuilder' testCases(21) = class'TEST_LogMessage' testCases(22) = class'TEST_LocalDatabase' + testCases(23) = class'TEST_UTF8EncoderDecoder' } \ No newline at end of file From 323bf71e709ffb2a56135bfd6c82f1817dab4526 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 28 Jul 2021 04:47:01 +0700 Subject: [PATCH 2/6] Refactor utf8 encoder and decoder Make decoder work with byte arrays containing a single complete text, instead of byte streams with several separate texts. Moved codecs into "Text" category. --- .../Avarice/Tests/TEST_UTF8EncoderDecoder.uc | Bin 16540 -> 0 bytes sources/Avarice/Utf8Decoder.uc | 260 ------------------ sources/Text/Codecs/Utf8Decoder.uc | 161 +++++++++++ .../{Avarice => Text/Codecs}/Utf8Encoder.uc | 39 +-- sources/Text/Tests/TEST_UTF8EncoderDecoder.uc | Bin 0 -> 10184 bytes 5 files changed, 181 insertions(+), 279 deletions(-) delete mode 100644 sources/Avarice/Tests/TEST_UTF8EncoderDecoder.uc delete mode 100644 sources/Avarice/Utf8Decoder.uc create mode 100644 sources/Text/Codecs/Utf8Decoder.uc rename sources/{Avarice => Text/Codecs}/Utf8Encoder.uc (76%) create mode 100644 sources/Text/Tests/TEST_UTF8EncoderDecoder.uc diff --git a/sources/Avarice/Tests/TEST_UTF8EncoderDecoder.uc b/sources/Avarice/Tests/TEST_UTF8EncoderDecoder.uc deleted file mode 100644 index 950d0daa77987d328e49cd106741cee99777b141..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16540 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 Ezb943E&u=k diff --git a/sources/Avarice/Utf8Decoder.uc b/sources/Avarice/Utf8Decoder.uc deleted file mode 100644 index 89f677a..0000000 --- a/sources/Avarice/Utf8Decoder.uc +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Class for decoding UTF8 byte stream into Acedia's `MutableText` value. - * It is made to work with incoming, and possibly incomplete, streams of - * bytes: instead of consuming the whole utf8 text, it is made to consume it - * byte-by-byte and store `MutableText`s that it parsed from the stream - * (assumes that separate `MutableText`s are separated by `0` byte). - * 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 (except overlong encoding of zero). - * It, however, 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 . - */ -class Utf8Decoder extends AcediaObject; - -/** - * `Utf8Decoder` consumes byte by byte with `PushByte()` method and it's - * algorithm is simple: - * 1. If it encounters a byte that encodes a singular code point by - * itself (starts with `0` bit) - it is added as a codepoint; - * 2. If it encounters byte which indicates that next code point is - * composed out of several bytes (starts with 110, 1110 or 11110) - - * remembers that it has to read several "inner" bytes belonging to - * the same code point and starts to expect them instead; - * 3. If it ever encounters a byte with unexpected (and thus invalid) - * bit prefix - enters a failed state; - * 4. If it ever encounters a `0` byte: - * * If it was not in a failed state - records `MutableText` - * accumulated so far; - * * Clears failed state. - */ - -var private bool failedState; - -// Variables for building a multi-byte code point -var private int nextCodePoint; -var private int innerBytesLeft; - -// `MutableText` we are building right now -var private MutableText nextText; -// `MutableText`s we have already built -var private array outputQueue; - -// 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; - -protected function Constructor() -{ - nextText = _.text.Empty(); -} - -protected function Finalizer() -{ - _.memory.Free(nextText); - _.memory.FreeMany(outputQueue); - nextText = none; - failedState = false; - outputQueue.length = 0; - innerBytesLeft = 0; - nextCodePoint = 0; -} - -/** - * Checks whether data in the `MutableText` that caller `Utf8Decoder` is - * currently filling was detected to be invalid. - * - * This state can be reset by pushing `0` byte into caller `Utf8Decoder`. - * See `PushByte()` for more info. - * - * @return `true` iff caller `Utf8Decoder` is not in a failed state. - */ -public final function bool Failed() -{ - return failedState; -} - -/** - * Checks whether caller `Utf8Decoder` has any data put in - * the `MutableText` it is currently building. - * Result is guaranteed to be `false` after `self.PushByte(0)` call, since - * it starts a brand new `MutableText`. - */ -public final function bool HasUnfinishedData() -{ - if (innerBytesLeft > 0) return true; - if (nextText.GetLength() > 0) return true; - return false; -} - -/** - * Returns next `MutableText` that was successfully decoded by - * the caller `Utf8Decoder`, removing it from the output queue. - * - * @return Next `MutableText` in the caller `Utf8Decoder`'s output queue. - * `none` iff output queue is empty. `MutableText`s are returned in order - * they were decoded. - */ -public final function MutableText PopText() -{ - local MutableText result; - if (outputQueue.length <= 0) { - return none; - } - result = outputQueue[0]; - outputQueue.Remove(0, 1); - return result; -} - -/** - * Adds next `byte` from the byte stream that is supposed to encode UTF8 text. - * To finish building `MutableText` pass `0` byte into this method, which will - * `MutableText` built so far into an "output queue" (accessible with - * `PopText()`) and start building a new one. - * - * This method expects `byte`s, in order, from a sequence that has correct - * UTF8 encoding. If method detects incorrect UTF8 sequence - it will be put - * into a "failed state", discarding `MutableText` it was currently building, - * along with any further input (except `0` byte). - * Pushing `0` byte will restore `Utf8Decoder` from a failed state and it - * will start building a new `MutableText`. - * - * @param nextByte next byte from byte stream that is supposed to encode - * UTF8 text. `0` will make caller `Utf8Decoder` start building new - * `MutableText`. - * @return `true` iff caller `Utf8Decoder` was not in a failed state and - * operation was successful. - */ -public final function bool PushByte(byte nextByte) -{ - if (nextByte == 0) return QueueCurrentText(); - if (failedState) return false; - 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()`) - failedState = true; - 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` and `failedState == false` -// 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) - { - failedState = true; - 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) - { - // We forbid overlong encoding of `0` - // (as does the Unicode standard) - if (nextCodePoint == 0) - { - failedState = true; - return false; - } - AppendCodePoint(nextCodePoint); - } - return true; -} - -private final function AppendCodePoint(int codePoint) -{ - local Text.Character nextCharacter; - nextCharacter.codePoint = codePoint; - nextText.AppendCharacter(nextCharacter); -} - -// Return `true` if `MutableText` was added to the queue -// (there were no encoding errors) -private final function bool QueueCurrentText() -{ - local bool result; - // If we still do not have all bytes for the character we were building - - // then passed UTF8 was invalid - failedState = failedState || innerBytesLeft > 0; - result = !failedState; - if (failedState) { - _.memory.Free(nextText); - } - else { - outputQueue[outputQueue.length] = nextText; - } - failedState = false; - innerBytesLeft = 0; - nextText = _.text.Empty(); - return result; -} - -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 -} \ No newline at end of file diff --git a/sources/Text/Codecs/Utf8Decoder.uc b/sources/Text/Codecs/Utf8Decoder.uc new file mode 100644 index 0000000..5f86e8f --- /dev/null +++ b/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 . + */ +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 +} \ No newline at end of file diff --git a/sources/Avarice/Utf8Encoder.uc b/sources/Text/Codecs/Utf8Encoder.uc similarity index 76% rename from sources/Avarice/Utf8Encoder.uc rename to sources/Text/Codecs/Utf8Encoder.uc index e66321d..af0042b 100644 --- a/sources/Avarice/Utf8Encoder.uc +++ b/sources/Text/Codecs/Utf8Encoder.uc @@ -1,6 +1,8 @@ /** * 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 *------------------------------------------------------------------------------ @@ -38,69 +40,68 @@ var private int lastSixBits; * code points - this method will return empty array. * * @param text `Text` object to encode. - * @return UTF8 representation of passed `text` as an array of `byte`s. - * Empty array if `text == none` or `text` contains invalid Unicode + * @return UTF8 representation of passed `text` inside `ByteArrayRef`. + * `none` iff `text == none` or `text` contains invalid Unicode * code points. */ -public final function array Encode(Text text) +public final function ByteArrayRef Encode(Text text) { local int i, nextCodepoint, textLength; - local array buffer; + local ByteArrayRef buffer; if (__().text.IsEmpty(text)) { - return buffer; // empty array + 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[buffer.length] = nextCodepoint; + buffer.AddItem(nextCodepoint); } else if (nextCodepoint <= utfLimit2) { // Drop 6 bits that will be recorded inside second byte and // add 2-byte sequence mask - buffer[buffer.length] = utfMask2 | (nextCodepoint >> 6); + buffer.AddItem(utfMask2 | (nextCodepoint >> 6)); // Take only last 6 bits for the second (last) byte // + add inner-byte sequence mask - buffer[buffer.length] = utfMaskIn | (nextCodepoint & lastSixBits); + 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[buffer.length] = utfMask3 | (nextCodepoint >> 12); + buffer.AddItem(utfMask3 | (nextCodepoint >> 12)); // Drop 6 bits that will be recorded inside third byte and // add inner-byte sequence mask - buffer[buffer.length] = - utfMaskIn | ((nextCodepoint >> 6) & lastSixBits); + buffer.AddItem(utfMaskIn | ((nextCodepoint >> 6) & lastSixBits)); // Take only last 6 bits for the third (last) byte // + add inner-byte sequence mask - buffer[buffer.length] = utfMaskIn | (nextCodepoint & lastSixBits); + 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[buffer.length] = utfMask4 | (nextCodepoint >> 18); + buffer.AddItem(utfMask4 | (nextCodepoint >> 18)); // Drop 12 bits that will be recorded inside third and fourth bytes // and add inner-byte sequence mask - buffer[buffer.length] = - utfMaskIn | ((nextCodepoint >> 12) & lastSixBits); + buffer.AddItem(utfMaskIn | ((nextCodepoint >> 12) & lastSixBits)); // Drop 6 bits that will be recorded inside fourth byte // and add inner-byte sequence mask - buffer[buffer.length] = - utfMaskIn | ((nextCodepoint >> 6) & lastSixBits); + buffer.AddItem(utfMaskIn | ((nextCodepoint >> 6) & lastSixBits)); // Take only last 6 bits for the fourth (last) byte // + add inner-byte sequence mask - buffer[buffer.length] = utfMaskIn | (nextCodepoint & lastSixBits); + buffer.AddItem(utfMaskIn | (nextCodepoint & lastSixBits)); } else { // Outside of known Unicode range // Should not be possible, since `Text` is expected to // contain only correct Unicode - buffer.length = 0; + _.memory.Free(buffer); + buffer = none; break; } } diff --git a/sources/Text/Tests/TEST_UTF8EncoderDecoder.uc b/sources/Text/Tests/TEST_UTF8EncoderDecoder.uc new file mode 100644 index 0000000000000000000000000000000000000000..63daa53c947bb8e886f30a4850629f20d15a80a5 GIT binary patch literal 10184 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 N;%Y=qF7SL;{tpwt-JAdb literal 0 HcmV?d00001 From ee4f669a2d4d56629c8c3d6d2c344ee8def4d915 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 28 Jul 2021 19:04:45 +0700 Subject: [PATCH 3/6] Add `AvariceStreamReader` --- sources/Avarice/AvariceStreamReader.uc | 149 ++++++++++++++++++ .../Avarice/Tests/TEST_AvariceStreamReader.uc | Bin 0 -> 6148 bytes sources/Manifest.uc | 1 + 3 files changed, 150 insertions(+) create mode 100644 sources/Avarice/AvariceStreamReader.uc create mode 100644 sources/Avarice/Tests/TEST_AvariceStreamReader.uc diff --git a/sources/Avarice/AvariceStreamReader.uc b/sources/Avarice/AvariceStreamReader.uc new file mode 100644 index 0000000..5ec474e --- /dev/null +++ b/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 . + */ +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 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 PopMessages() +{ + local array 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 +} \ No newline at end of file diff --git a/sources/Avarice/Tests/TEST_AvariceStreamReader.uc b/sources/Avarice/Tests/TEST_AvariceStreamReader.uc new file mode 100644 index 0000000000000000000000000000000000000000..1adf053797efda9942849fcb05fb9980a942b0f6 GIT binary patch literal 6148 zcmd^@?N1v=5XSd&rT!0BzQpv!1gc6^nx=J7AV@$EHj1LEA_m*osxg*dLaZqNdfMkV z!*aLBkvJqDkgAn^cl$Ck`^?OyZX6S~Qt{0)F|Cyeq`k(54 zqWfWJTVKbl$r@k$NulpM{obK~`7K|EM?G{ZqKVE!v3vePINWJza;|MRxT(ww>juHw?*+ zM%x9{a!JMUD5GcoLFUwaHw%|0wHqF(Z=t@quBYk)OMHoZe2eE!G>Wg0h8@i6C^{}Q znuT*+ncb6|+zOvLx0!gGXz7~_Bu%u6y~a55k?pV_j?}xY86%t377|%Fkp2_h5kpzn zwJS65HP#?$d_8AsdWgZ3#G?%P>tu7dVW(M zve5_5;L)``7VR1+HpbF4&=^*Gq)rT2b$N^s=DSm|3$>Q1sm=bxxFjf$RJ`e(|+OC zw&7po#C*4}3_0@HF&r93_>Wle+=Xce_6&Y){Z6$C9xX@hUF^It4rcY@ln@`iSE}d8j$KaKUL3@0{5+% zC-SodHpS1aa4c(z=TB4GK)S6MJG85xMa1D-jiI4qeA3cA{I{vO*z3FYQ>iUxac*pD zuQSCpqx@VKYy@xe$_x6coP1HZ;vFa7k(9n$bj9Q!F~7XKZxaX+{QSgD05jqu+}}~ zT2qO|ku3b4>>z4gPE$>>aco%Bg?Cp!Vr`+QY6yd9IT+WpzgVR_STdd-2;utJ9*UC!isXmX~)I!9O4=HN5%h^)wa%#;@!m@JSTLh80Uy@VD%F}cgMYc$>@qDjh zpmgU&3Dtg$ag|@oSRv!~EXRNNZkaYH#d6FK*n;Yw(|I92-?MmtAE{u8DOmN%wT`$) zkD){JI#osF&}d6h{!>XNQXAKLSjYRv%|^w?+#<>;gH+A%TujlcC+t^K9YB^mmuzPT zV)N2CgA@2Wi}7-F#@!OjbxEL(>Z>|&8*6%xxsNN>Ro#~Dij_)DIp%0Rv}%s0$NE*F zTDLlWrg{ow&*YcB@hw##>wYi>T$iq_6s6IxpTh5BT6ZiC&ZTi64dwlK-Y=`?e){RA zm#XksKDMbW|EP@P|A+T)a*|_LjzTKwJ;gNVRaJ~&7WfC^bGhCo7O+WraYs* zZfxLvu<&=go*~0|%iA6vk8d4qMFgiIUlLMJ`g=wmT`QH9^q*R{BB{vZThqttxT1Hd z#fGwkxQ}}M42)G8w&J~*HD9X^i}GU?Sj@)zREOQ289K@a@;;OQc?WdQj&(n=Cvb)Z zCN?KB_l128n8+&&d9jE}%v1L@b(h!wWp=6Ze(Kws_02~3MJ@U`YP$ccmgR?KW14p< zveYYGf75_#@fcsZrsk1%iC=gZCxf=ln{#oq;|I&GlunY=qjBZFB;dt}d)3O@&LEfg F{y(PeuWJAR literal 0 HcmV?d00001 diff --git a/sources/Manifest.uc b/sources/Manifest.uc index f0629aa..b1697b2 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -57,4 +57,5 @@ defaultproperties testCases(21) = class'TEST_LogMessage' testCases(22) = class'TEST_LocalDatabase' testCases(23) = class'TEST_UTF8EncoderDecoder' + testCases(24) = class'TEST_AvariceStreamReader' } \ No newline at end of file From ec3a751162d3f1943fa4a869f126004e05a5b3f4 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 3 Aug 2021 04:24:58 +0700 Subject: [PATCH 4/6] Add `TakeItem()` an ability to deallocate key --- sources/Data/Collections/AssociativeArray.uc | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/sources/Data/Collections/AssociativeArray.uc b/sources/Data/Collections/AssociativeArray.uc index d01ecad..a2983a6 100644 --- a/sources/Data/Collections/AssociativeArray.uc +++ b/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) * 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 * 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; } /** From cab913eb8aab6273b5640f556813eed42a7bd595 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 3 Aug 2021 04:27:29 +0700 Subject: [PATCH 5/6] Fix unnecessarily requiring `ActorService` --- sources/Unreal/NativeActorRef.uc | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sources/Unreal/NativeActorRef.uc b/sources/Unreal/NativeActorRef.uc index 65bd227..85fcb2c 100644 --- a/sources/Unreal/NativeActorRef.uc +++ b/sources/Unreal/NativeActorRef.uc @@ -80,13 +80,11 @@ public final function NativeActorRef Set(Actor newValue) public function bool IsEqual(Object other) { - local NativeActorRef otherBox; - local ActorService service; + local NativeActorRef otherBox; otherBox = NativeActorRef(other); - if (otherBox == none) return false; - service = ActorService(class'ActorService'.static.Require()); - if (service == none) return false; - + if (otherBox == none) { + return false; + } return Get() == otherBox.Get(); } From e6aaf2083f8b0127955ecff73732af5c475c2dcc Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 3 Aug 2021 04:28:32 +0700 Subject: [PATCH 6/6] Refactor Avarice to provide a proper interface --- sources/Avarice/Avarice.uc | 136 +++++- sources/Avarice/AvariceAPI.uc | 70 ++- sources/Avarice/AvariceClient.uc | 88 ---- sources/Avarice/AvariceLink.uc | 437 ++++++++++++++++++ sources/Avarice/AvariceMessage.uc | 112 ++--- sources/Avarice/AvariceTCPLink.uc | 170 ------- sources/Avarice/AvariceTcpStream.uc | Bin 0 -> 26976 bytes .../Events/Avarice_OnMessage_Signal.uc | 38 ++ .../Avarice/Events/Avarice_OnMessage_Slot.uc | 40 ++ 9 files changed, 733 insertions(+), 358 deletions(-) delete mode 100644 sources/Avarice/AvariceClient.uc create mode 100644 sources/Avarice/AvariceLink.uc delete mode 100644 sources/Avarice/AvariceTCPLink.uc create mode 100644 sources/Avarice/AvariceTcpStream.uc create mode 100644 sources/Avarice/Events/Avarice_OnMessage_Signal.uc create mode 100644 sources/Avarice/Events/Avarice_OnMessage_Slot.uc diff --git a/sources/Avarice/Avarice.uc b/sources/Avarice/Avarice.uc index 1209199..f162618 100644 --- a/sources/Avarice/Avarice.uc +++ b/sources/Avarice/Avarice.uc @@ -1,5 +1,24 @@ /** - * + * This feature makes it possible to use TCP connection to exchange + * messages (represented by JSON objects) with external applications. + * There are some peculiarities to UnrealEngine's `TCPLink`, so to simplify + * communication process for external applications, they are expected to + * connect to the server through the "Avarice" utility that can accept a stream + * of utf8-encoded JSON messageand feed them to our `TCPLink` (we use child + * class `AvariceTcpStream`) in a way it can receive them. + * Every message sent to us must have the following structure: + * { "s": "", "t": "", "p": } + * where + * * describes a particular source of messages + * (it can be a name of the database or an alias for + * a connected application); + * * simply states the name of a command, for a database it + * can be "get", "set", "delete", etc.. + * * can be an arbitrary json value and can be used to + * pass any additional information along with the message. + * Acedia provides a special treatment for any messages that have their + * service set to "echo" - it always returns them back as-is, except for the + * message type that gets set to "end". * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -20,53 +39,84 @@ class Avarice extends Feature config(AcediaAvarice); -struct AvariceLink +// The feature itself is dead simple - it simply creates list of +// `AvariceLink` objects, according to its settings, and stores them +var private array createdLinks; + +struct AvariceLinkRecord { var string name; - var string host; + var string address; }; +// List all the names (and addresses) of all Avarice instances Acedia must +// connect to. +// `name` parameter is a useful (case-insensitive) identifier that +// can be used in other configs to point at each link. +// `address` must have form "host:port", where "host" is either ip or +// domain name and "port" is a numerical port value. +var private config array link; + +// In case Avarice utility is launched after link started trying open +// the connection - that connection attempt will fail. To fix that link must +// try connecting again. +// This variable sets the time (in seconds), after which link will +// re-attempt opening connection. Setting value too low can prevent any +// connection from opening, setting it too high might make you wait for +// connection too long. +var private config float reconnectTime; -var private config array link; +var private const int TECHO, TEND, TCOLON; var private LoggerAPI.Definition errorBadAddress; protected function OnEnabled() { - local int i; - local string host; - local int port; - local AvariceTCPLink nextTCPLink; + local int i; + local Text name; + local MutableText host; + local int port; + local AvariceLink nextLink; for (i = 0; i < link.length; i += 1) { - if (!ParseAddress(link[i].host, host, port)) { + name = _.text.FromString(link[i].name); + if (ParseAddress(link[i].address, host, port)) + { + nextLink = AvariceLink(_.memory.Allocate(class'AvariceLink')); + nextLink.Initialize(name, host, port); + nextLink.StartUp(); + nextLink.OnMessage(self, T(TECHO)).connect = EchoHandler; + createdLinks[createdLinks.length] = nextLink; + } + else { _.logger.Auto(errorBadAddress).Arg(_.text.FromString(link[i].name)); } - nextTCPLink = AvariceTCPLink(_.memory.Allocate(class'AvariceTCPLink')); - nextTCPLink.Connect(link[i].name, host, port); + _.memory.Free(name); + _.memory.Free(host); } } protected function OnDisabled() { - local LevelInfo level; - local AvariceTCPLink nextTCPLink; - level = _.unreal.GetLevel(); - foreach level.DynamicActors(class'AvariceTCPLink', nextTCPLink) { - nextTCPLink.Destroy(); - } + _.memory.FreeMany(createdLinks); +} + +// Reply back any messages from "echo" service +private function EchoHandler(AvariceLink link, AvariceMessage message) +{ + link.SendMessage(T(TECHO), T(TEND), message.parameters); } private final function bool ParseAddress( - string address, - out string host, - out int port) + string address, + out MutableText host, + out int port) { local bool success; local Parser parser; parser = _.text.ParseString(address); parser.Skip() - .MUntilS(host, _.text.GetCharacter(":")) - .MatchS(":") + .MUntil(host, T(TCOLON).GetCharacter(0)) + .Match(T(TCOLON)) .MUnsignedInteger(port) .Skip(); success = parser.Ok() && parser.GetRemainingLength() == 0; @@ -74,7 +124,49 @@ private final function bool ParseAddress( return success; } +/** + * Method that returns all the `AvariceLink` created by this feature. + * + * @return Array of links created by this feature. + * Guaranteed to not contain `none` values. + */ +public final function array GetAllLinks() +{ + local int i; + while (i < createdLinks.length) + { + if (createdLinks[i] == none) { + createdLinks.Remove(i, 1); + } + else { + i += 1; + } + } + return createdLinks; +} + +/** + * Returns its current `reconnectTime` setting that describes amount of time + * between connection attempts. + * + * @return Value of `reconnectTime` config variable. + */ +public final static function float GetReconnectTime() +{ + return default.reconnectTime; +} + defaultproperties { + // Settings + reconnectTime = 10.0 + // `Text` constants + TECHO = 0 + stringConstants(0) = "echo" + TEND = 1 + stringConstants(1) = "end" + TCOLON = 2 + stringConstants(2) = ":" + // Log messages errorBadAddress = (l=LOG_Error,m="Cannot parse address \"%1\"") } \ No newline at end of file diff --git a/sources/Avarice/AvariceAPI.uc b/sources/Avarice/AvariceAPI.uc index 1f06036..ddcdc1d 100644 --- a/sources/Avarice/AvariceAPI.uc +++ b/sources/Avarice/AvariceAPI.uc @@ -1,4 +1,5 @@ /** + * API for Avarice functionality of Acedia. * Copyright 2020 - 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -18,35 +19,56 @@ */ class AvariceAPI extends AcediaObject; -public final function AvariceMessage MessageFromText(Text message) +/** + * Method that returns all the `AvariceLink` created by `Avarice` feature. + * + * @return Array of links created by this feature. + * Guaranteed to not contain `none` values. + * Empty if `Avarice` feature is currently disabled. + */ +public final function array GetAllLinks() { - local Parser parser; - local AvariceMessage result; - local AssociativeArray parsedMessage; - if (message == none) return none; - parser = _.text.Parse(message); - parsedMessage = _.json.ParseObjectWith(parser); - parser.FreeSelf(); - if (!HasNecessaryMessageKeys(parsedMessage)) - { - _.memory.Free(parsedMessage); - return none; + local Avarice avariceFeature; + local array emptyResult; + avariceFeature = Avarice(class'Avarice'.static.GetInstance()); + if (avariceFeature != none) { + return avariceFeature.GetAllLinks(); } - result = AvariceMessage(_.memory.Allocate(class'AvariceMessage')); - result.SetID(parsedMessage.GetText(P("i"))); - result.SetGroup(parsedMessage.GetText(P("g"))); - result.data = parsedMessage.TakeItem(P("p")); - _.memory.Free(parsedMessage); - return result; + return emptyResult; } -private final function bool HasNecessaryMessageKeys(AssociativeArray message) +/** + * Finds and returns `AvariceLink` by its name, specified in "AcediaAvarice" + * config, if it exists. + * + * @param linkName Name of the `AvariceLink` to find. + * @return `AvariceLink` corresponding to name `linkName`. + * If `linkName == none` or `AvariceLink` with such name does not exist - + * returns `none`. + */ +public final function AvariceLink GetLink(Text linkName) { - if (message == none) return false; - if (!message.HasKey(P("i"))) return false; - if (!message.HasKey(P("g"))) return false; - - return true; + local int i; + local Text nextName; + local array allLinks; + if (linkName == none) { + return none; + } + allLinks = GetAllLinks(); + for (i = 0; i < allLinks.length; i += 1) + { + if (allLinks[i] == none) { + continue; + } + nextName = allLinks[i].GetName(); + if (linkName.Compare(nextName, SCASE_INSENSITIVE)) + { + _.memory.Free(nextName); + return allLinks[i]; + } + _.memory.Free(nextName); + } + return none; } defaultproperties diff --git a/sources/Avarice/AvariceClient.uc b/sources/Avarice/AvariceClient.uc deleted file mode 100644 index 4b37733..0000000 --- a/sources/Avarice/AvariceClient.uc +++ /dev/null @@ -1,88 +0,0 @@ -class AvariceClient extends AcediaObject; - -enum AvariceClientState -{ - ACS_Waiting, - ACS_ReadingID, - ACS_ReadingLength, - ACS_ReadingPayload, - ACS_Invalid -}; - -var private int currentID; -var private int currentMessageLength; -var private array currentPayload; - -var private AvariceClientState currentState; -var private int bytesLeftToRead; -var private byte buffer[255]; -var private array longBuffer; -var private int pendingBytes; - -public final function PushByte(byte nextByte) -{ - if (nextByte == 0) - { - if (bytesLeftToRead > 0) - { - // ACK for short message (with id) - } - currentState = ACS_Waiting; - ResetBuffer(); - return; - } - else if (currentState == ACS_Invalid) - { - // ACK of invalid message's end - return; - } - else if (currentState == ACS_Waiting) - { - currentID = nextByte; - currentID = currentID << 8; - currentState = ACS_ReadingID; - } - else if (currentState == ACS_ReadingID) - { - currentID += nextByte; - currentState = ACS_ReadingLength; - bytesLeftToRead = 2; - } - else if (currentState == ACS_ReadingLength) - { - bytesLeftToRead -= 1; - if (bytesLeftToRead > 0) - { - currentMessageLength = nextByte; - currentMessageLength = currentMessageLength << 8; - } - else - { - currentMessageLength += nextByte; - currentState = ACS_ReadingPayload; - bytesLeftToRead = currentMessageLength; - } - } - else if (currentState == ACS_ReadingPayload) - { - currentPayload[currentPayload.length] = nextByte; - // Decode payload into `AvariceMessage` - // Send messages via Acedia's signals - bytesLeftToRead -= 1; - if (bytesLeftToRead == 0) - { - currentState = ACS_Waiting; - // ACK into buffer - } - } -} - -private final function ResetBuffer() -{ - pendingBytes = 0; - longBuffer.length = 0; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Avarice/AvariceLink.uc b/sources/Avarice/AvariceLink.uc new file mode 100644 index 0000000..8012cb4 --- /dev/null +++ b/sources/Avarice/AvariceLink.uc @@ -0,0 +1,437 @@ +/** + * Provide interface for the connection to Avarice application. + * It's parameters are defined in Acedia's config. + * Class provides methods to obtain its configuration information + * (name, address, port), methods to check and change the status of connection, + * signals to handle arriving messages and ways to send messages back. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class AvariceLink extends AcediaObject; + +/** + * Objects of this class are supposed to be obtained via the + * `AvariceAPI.GetLink()` method. Available links are automatically initialized + * based on the configs and their parameters cannot be changed. + * It is also possible to spawn a link of your own by creating an object of + * this class (`AvariceLink`) and calling `Initialize()` method with + * appropriate parameters. To start the link then simply call `StartUp()`. + * But such links will not appear in the list of available links in + * `AvariceAPI`. + */ + +// Actual work of dealing with network input/output is done in +// the `AvariceTcpStream` `Actor` class that is stored inside this reference +var private NativeActorRef tcpStream; + +// `tcpStream` communicates with this class by informing it about specific +// events. This enum describes all of their types. +enum AvariceNetworkMessage +{ + // Connection with Avarice established - can happen several times in case + // connection is interrupted + ANM_Connected, + // We have lost connection with Avarice, but normally will attempt to + // reconnect back + ANM_Disconnected, + // JSON message received + ANM_Message, + // Connection died: either was manually closed, host address could not + // be resolved or invalid data was received from Avarice + ANM_Death +}; + +// Name of this link, specified in the config +var private Text linkName; +// Host of the Avarice instance we are connecting to +var private Text linkHost; +// Port used by the Avarice instance we are connecting to +var private int linkPort; + +var private SimpleSignal onConnectedSignal; +var private SimpleSignal onDisconnectedSignal; +var private SimpleSignal onDeathSignal; +// We want to have a separate signal for each message "service", since most +// users of `AvariceLink` would only care about one particular service. +// To achieve that we use this array as a "service name" <-> "signal" map. +var private AssociativeArray serviceSignalMap; + +var private const int TSERVICE_PREFIX, TTYPE_PREFIX; +var private const int TPARAMS_PREFIX, TMESSAGE_SUFFIX; + +var private LoggerAPI.Definition fatalCannotSpawn; + +protected function Constructor() +{ + onConnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); + onDisconnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); + onDeathSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); + serviceSignalMap = _.collections.EmptyAssociativeArray(); +} + +protected function Finalizer() +{ + local Actor storedStream; + _.memory.Free(onConnectedSignal); + _.memory.Free(onDisconnectedSignal); + _.memory.Free(onDeathSignal); + _.memory.Free(serviceSignalMap); + _.memory.Free(linkName); + _.memory.Free(linkHost); + onConnectedSignal = none; + onDisconnectedSignal = none; + onDeathSignal = none; + serviceSignalMap = none; + linkName = none; + linkHost = none; + linkPort = 0; + if (tcpStream == none) { + return; + } + storedStream = tcpStream.Get(); + if (storedStream != none) { + storedStream.Destroy(); + } + tcpStream.FreeSelf(); + tcpStream = none; +} + +/** + * Initializes this caller `AvariceLink` with config data. + * + * Can only successfully (for that `name` and `host` must not be `none`) + * be called once. + * + * @param name Alias (case-insensitive) of caller `AvariceLink`. + * Must not be `none`. + * @param host Host of the Avarice instance that caller `AvariceLink` is + * connecting to. Must not be `none`. + * @param name Port used by the Avarice instance that caller `AvariceLink` + * is connecting to. + */ +public final function Initialize(Text name, Text host, int port) +{ + if (tcpStream != none) return; + if (name == none) return; + if (host == none) return; + + linkName = name.Copy(); + linkHost = host.Copy(); + linkPort = port; + tcpStream = _.unreal.ActorRef(none); +} + +/** + * Signal that will be emitted whenever caller `AvariceLink` connects to + * Avarice. This event can be emitted multiple times if case link temporarily + * loses it's TCP connection or if connection is killed off due to errors + * (or manually). + * + * [Signature] + * void () + */ +/* SIGNAL */ +public final function SimpleSlot OnConnected(AcediaObject receiver) +{ + return SimpleSlot(onConnectedSignal.NewSlot(receiver)); +} + +/** + * Signal that will be emitted whenever caller `AvariceLink` disconnects from + * Avarice. Disconnects can temporarily be cause by network issue and + * `AvariceLink` will attempt to restore it's connection. To detect when + * connection was permanently severed use `OnDeath` signal instead. + * + * [Signature] + * void () + */ +/* SIGNAL */ +public final function SimpleSlot OnDisconnected(AcediaObject receiver) +{ + return SimpleSlot(onDisconnectedSignal.NewSlot(receiver)); +} + +/** + * Signal that will be emitted whenever connection is closed and dropped: + * either due to bein unable to resolve host's address, receiving incorrect + * input from Avarice or someone manually closing it. + * + * [Signature] + * void () + */ +/* SIGNAL */ +public final function SimpleSlot OnDeath(AcediaObject receiver) +{ + return SimpleSlot(onDeathSignal.NewSlot(receiver)); +} + +/** + * Signal that will be emitted whenever caller `AvariceLink` disconnects from + * Avarice. Disconnects can temporarily be cause by network issue and + * `AvariceLink` will attempt to restore it's connection. To detect when + * connection was permanently severed use `OnDeath` signal instead. + * + * @param service Name of the service, whos messages one wants to receive. + * `none` will be treated as an empty `Text`. + * + * [Signature] + * void (AvariceLink link, AvariceMessage message) + * @param link Link that has received message. + * @param message Received message. + * Can be any JSON-compatible value (see `JSONAPI.IsCompatible()` + * for more information). + */ +/* SIGNAL */ +public final function Avarice_OnMessage_Slot OnMessage( + AcediaObject receiver, + Text service) +{ + return Avarice_OnMessage_Slot(GetServiceSignal(service).NewSlot(receiver)); +} + +private final function Avarice_OnMessage_Signal GetServiceSignal(Text service) +{ + local Avarice_OnMessage_Signal result; + if (service != none) { + service = service.Copy(); + } + else { + service = Text(_.memory.Allocate(class'Text')); + } + result = Avarice_OnMessage_Signal(serviceSignalMap.GetItem(service)); + if (result == none) + { + result = Avarice_OnMessage_Signal( + _.memory.Allocate(class'Avarice_OnMessage_Signal')); + serviceSignalMap.SetItem(service, result); + } + else { + service.FreeSelf(); + } + return result; +} + +/** + * Starts caller `AvariceLink`, making it attempt to connect to the Avarice + * with parameters that should be first specified by the `Initialize()` call. + * + * Does nothing if the caller `AvariceLink` is either not initialized or + * is already active (`IsActive() == true`). + */ +public final function StartUp() +{ + local AvariceTcpStream newStream; + if (tcpStream == none) return; + if (tcpStream.Get() != none) return; + + newStream = AvariceTcpStream(_.memory.Allocate(class'AvariceTcpStream')); + if (newStream == none) + { + // `linkName` has to be defined if `tcpStream` is defined + _.logger.Auto(fatalCannotSpawn).Arg(linkName.Copy()); + return; + } + tcpStream.Set(newStream); + newStream.StartUp(self, class'Avarice'.static.GetReconnectTime()); +} + +/** + * Shuts down any connections related to the caller `AvariceLink`. + * + * Does nothing if the caller `AvariceLink` is either not initialized or + * is already inactive (`IsActive() == false`). + */ +public final function ShutDown() +{ + local Actor storedStream; + if (tcpStream == none) return; + storedStream = tcpStream.Get(); + if (storedStream == none) return; + + storedStream.Destroy(); + tcpStream.Set(none); +} + +/** + * Checks whether caller `AvariceLink` is currently active: either connected or + * currently attempts to connect to Avarice. + * + * See also `IsConnected()`. + * + * @return `true` if caller `AvariceLink` is either connected or currently + * attempting to connect to Avarice. `false` otherwise. + */ +public final function bool IsActive() +{ + if (tcpStream == none) { + return false; + } + return tcpStream.Get() != none; +} + +/** + * Checks whether caller `AvariceLink` is currently connected or to Avarice. + * + * See also `IsActive()`. + * + * @return `true` iff caller `AvariceLink` is currently connected to Avarice. + */ +public final function bool IsConnected() +{ + local AvariceTcpStream storedStream; + if (tcpStream == none) { + return false; + } + storedStream = AvariceTcpStream(tcpStream.Get()); + return storedStream.linkState == STATE_Connected; +} + +/** + * Returns name caller `AvariceLink` was initialized with. + * Defined through the config files. + * + * @return Name of the caller `AvariceLink`. + * `none` iff caller link was not yet initialized. + */ +public final function Text GetName() +{ + if (linkName != none) { + return linkName.Copy(); + } + // `linkName` cannot be `none` after `Initialize()` call + return none; +} + +/** + * Returns host name (without port number) caller `AvariceLink` was + * initialized with. Defined through the config files. + * + * See `GetPort()` method for port number. + * + * @return Host name of the caller `AvariceLink`. + * `none` iff caller link was not yet initialized. + */ +public final function Text GetHost() +{ + if (linkHost != none) { + return linkHost.Copy(); + } + // `linkName` cannot be `none` after `Initialize()` call + return none; +} + +/** + * Returns port number caller `AvariceLink` was initialized with. + * Defined through the config files. + * + * @return Host name of the caller `AvariceLink`. + * If caller link was not yet initialized, method makes no guarantees + * about returned number. + */ +public final function int GetPort() +{ + return linkPort; +} + +/** + * Send a message to the Avarice that caller `AvariceLink` is connected to. + * + * Message can only be set if caller `AvariceLink` was initialized and is + * currently connected (see `IsConnected()`) to Avarice. + * + * @param service Name of the service this message is addressed. + * As an example, to address a database one would specify its name, + * like "db". Cannot be `none`. + * @param type Name of this message. As an example, to address + * a database, one would specify here WHAT that database must do, + * like "get" to fetch some data. Cannot be `none`. + * @param parameters Parameters of the command. Can be any value that is + * JSON-compatible (see `JSONAPI.IsCompatible()` for details). + * @return `true` if message was successfully sent and `false` otherwise. + * Note that this method returning `true` does not necessarily mean that + * message has arrived (which is impossible to know at this moment), + * instead simply saying that network call to send data was successful. + * Avarice does not provide any mechanism to verify message arrival, so if + * you need that confirmation - it is necessary that service you are + * addressing make a reply. + */ +public final function bool SendMessage( + Text service, + Text type, + AcediaObject parameters) +{ + local Mutabletext parametesAsJSON; + local MutableText message; + local AvariceTcpStream storedStream; + if (tcpStream == none) return false; + if (service == none) return false; + if (type == none) return false; + storedStream = AvariceTcpStream(tcpStream.Get()); + if (storedStream == none) return false; + if (storedStream.linkState != STATE_Connected) return false; + parametesAsJSON = _.json.Print(parameters); + if (parametesAsJSON == none) return false; + + message = _.text.Empty(); + message.Append(T(TSERVICE_PREFIX)) + .Append(_.json.Print(service)) + .Append(T(TTYPE_PREFIX)) + .Append(_.json.Print(type)) + .Append(T(TPARAMS_PREFIX)) + .Append(parametesAsJSON) + .Append(T(TMESSAGE_SUFFIX)); + storedStream.SendMessage(message); + message.FreeSelf(); + parametesAsJSON.FreeSelf(); + return true; +} + +// This is a public method, but it is not a part of +// `AvariceLink` interface. +// It is used as a communication channel with `AvariceTcpStream` and +// should not be called outside of that class. +public final function ReceiveNetworkMessage( + AvariceNetworkMessage message, + optional AvariceMessage avariceMessage) +{ + if (message == ANM_Connected) { + onConnectedSignal.Emit(); + } + else if (message == ANM_Disconnected) { + onDisconnectedSignal.Emit(); + } + else if (message == ANM_Message && avariceMessage != none) { + GetServiceSignal(avariceMessage.service).Emit(self, avariceMessage); + } + else if (message == ANM_Death) { + onDeathSignal.Emit(); + tcpStream.Set(none); + } +} + +defaultproperties +{ + TSERVICE_PREFIX = 0 + stringConstants(0) = "&{\"s\":" + TTYPE_PREFIX = 1 + stringConstants(1) = ",\"t\":" + TPARAMS_PREFIX = 2 + stringConstants(2) = ",\"p\":" + TMESSAGE_SUFFIX = 3 + stringConstants(3) = "&}" + fatalCannotSpawn = (l=LOG_Error,m="Cannot spawn new actor of class `AvariceTcpStream`, avarice link \"%1\" will not be created") +} \ No newline at end of file diff --git a/sources/Avarice/AvariceMessage.uc b/sources/Avarice/AvariceMessage.uc index 42551d2..2655819 100644 --- a/sources/Avarice/AvariceMessage.uc +++ b/sources/Avarice/AvariceMessage.uc @@ -1,12 +1,44 @@ +/** + * Object that represents a message received from Avarice. + * For performance's sake it does not provide a getter/setter interface and + * exposes public fields instead. However, for Acedia to correctly function + * you are not supposed modify those fields in any way, only using them to + * read necessary data. + * All `AvariceMessage`'s fields will be automatically deallocated, so if + * you need their data - you have to make a copy, instead of simply storing + * a reference to them. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ class AvariceMessage extends AcediaObject; -var private Text messageID; -var private Text messageGroup; - -var public AcediaObject data; +// Every message from Avarice has following structure: +// { "s": "", "t": "", "p": } +// Value of the "s" field +var public Text service; +// Value of the "t" field +var public Text type; +// Value of the "p" field +var public AcediaObject parameters; var private AssociativeArray messageTemplate; +var private const int TS, TT, TP; + public static function StaticConstructor() { if (StaticConstructorGuard()) return; @@ -18,12 +50,12 @@ public static function StaticConstructor() protected function Finalizer() { - __().memory.Free(messageID); - __().memory.Free(messageGroup); - __().memory.Free(data); - messageID = none; - messageGroup = none; - data = none; + __().memory.Free(type); + __().memory.Free(service); + __().memory.Free(parameters); + type = none; + service = none; + parameters = none; } private static final function ResetTemplate(AssociativeArray template) @@ -31,63 +63,35 @@ private static final function ResetTemplate(AssociativeArray template) if (template == none) { return; } - template.SetItem(P("i"), none); - template.SetItem(P("g"), none); - template.SetItem(P("p"), none); -} - -public final function SetID(Text id) -{ - _.memory.Free(messageID); - messageID = none; - if (id != none) { - messageID = id.Copy(); - } -} - -public final function Text GetID() -{ - if (messageID != none) { - return messageID.Copy(); - } - return none; -} - -public final function SetGroup(Text group) -{ - _.memory.Free(messageGroup); - messageGroup = none; - if (group != none) { - messageGroup = group.Copy(); - } -} - -public final function Text GetGroup() -{ - if (messageGroup != none) { - return messageGroup.Copy(); - } - return none; + template.SetItem(T(default.TS), none); + template.SetItem(T(default.TT), none); + template.SetItem(T(default.TP), none); } public final function MutableText ToText() { local MutableText result; local AssociativeArray template; - if (messageID == none) return none; - if (messageGroup == none) return none; + if (type == none) return none; + if (service == none) return none; template = default.messageTemplate; - template.SetItem(P("i"), messageID); - template.SetItem(P("g"), messageGroup); - if (data != none) { - template.SetItem(P("p"), data); + ResetTemplate(template); + template.SetItem(T(TT), type); + template.SetItem(T(TS), service); + if (parameters != none) { + template.SetItem(T(TP), parameters); } result = _.json.Print(template); - ResetTemplate(template); return result; } defaultproperties { + TS = 0 + stringConstants(0) = "s" + TT = 1 + stringConstants(1) = "t" + TP = 2 + stringConstants(2) = "p" } \ No newline at end of file diff --git a/sources/Avarice/AvariceTCPLink.uc b/sources/Avarice/AvariceTCPLink.uc deleted file mode 100644 index 335a1c8..0000000 --- a/sources/Avarice/AvariceTCPLink.uc +++ /dev/null @@ -1,170 +0,0 @@ -class AvariceTcpLink extends TcpLink - dependson(LoggerAPI); - -var private Global _; - -var private string linkName; -var private string linkHost; -var private int linkPort; -var private IpAddr remoteAddress; -var private int ttt; - -var private bool didWorkLastTick; - -var private array buffer; - -var private Utf8Encoder encoder; -var private Utf8Decoder decoder; - -var private LoggerAPI.Definition infoSuccess; -var private LoggerAPI.Definition fatalBadPort; -var private LoggerAPI.Definition fatalCannotBindPort; -var private LoggerAPI.Definition fatalCannotResolveHost; -var private LoggerAPI.Definition fatalCannotConnect; - -public final function bool Connect(string name, string host, int port) -{ - local InternetLink.IpAddr ip; - local int usedPort; - // Apparently `TcpLink` ignores default values for these variables, - // so we set them here - linkMode = MODE_Binary; - receiveMode = RMODE_Manual; - _ = class'Global'.static.GetInstance(); - encoder = Utf8Encoder(_.memory.Allocate(class'Utf8Encoder')); - decoder = Utf8Decoder(_.memory.Allocate(class'Utf8Decoder')); - linkName = name; - linkHost = host; - linkPort = port; - if (port <= 0) - { - _.logger.Auto(fatalBadPort) - .ArgInt(port) - .Arg(_.text.FromString(linkName)); - return false; - } - if (BindPort(, true) <= 0) - { - _.logger.Auto(fatalCannotBindPort) - .ArgInt(port) - .Arg(_.text.FromString(name)); - return false; - } - StringToIpAddr(host, remoteAddress); - remoteAddress.port = port; - if (remoteAddress.addr == 0) { - Resolve(host); - } - else { - OpenAddress(); - } - return true; -} - -event Resolved(IpAddr resolvedAddress) -{ - remoteAddress.addr = resolvedAddress.addr; - OpenAddress(); -} - -private final function bool OpenAddress() -{ - if (!OpenNoSteam(remoteAddress)) { - _.logger.Auto(fatalCannotConnect).Arg(_.text.FromString(linkName)); - } - _.logger.Auto(infoSuccess).Arg(_.text.FromString(linkName)); -} - -event ResolveFailed() -{ - _.logger.Auto(fatalCannotResolveHost).Arg(_.text.FromString(linkHost)); - // !Shut down! -} - -event Tick(float delta) -{ - local array toSend; - local AvariceMessage nextAMessage; - local MutableText nextMessage; - local int i, j, dataRead, totalRead, iter; - local byte data[255]; - if (didWorkLastTick) - { - didWorkLastTick = false; - return; - } - if (!IsDataPending()) { - return; - } - while (true) { - dataRead = ReadBinary(255, data); - for (i = 0; i < dataRead; i += 1) { - ttt += 1; - decoder.PushByte(data[i]); - } - if (dataRead <= 0) { - break; - } - } - if (ttt >= 4095) { - toSend = encoder.Encode(_.text.FromString("FLUSH")); - data[0] = toSend[0]; - data[1] = toSend[1]; - data[2] = toSend[2]; - data[3] = toSend[3]; - data[4] = toSend[4]; - data[5] = 0; - SendBinary(6, data); - } - if (dataRead > 0) { - didWorkLastTick = true; - } - // Obtain! - nextMessage = decoder.PopText(); - while (nextMessage != none) - { - Log("SIZE:" @ nextMessage.GetLength() @ ttt); - StopWatch(false); - nextAMessage = _.avarice.MessageFromText(nextMessage); - nextMessage.FreeSelf(); - nextMessage = nextAMessage.ToText(); - toSend = encoder.Encode(nextMessage); - toSend[toSend.length] = 0; - j = 0; - for (i = 0; i < toSend.length; i += 1) - { - data[j] = toSend[i]; - j += 1; - if (j >= 255) { - j = 0; - SendBinary(255, data); - } - } - if (j > 0) { - SendBinary(j, data); - } - nextMessage.FreeSelf(); - nextMessage = decoder.PopText(); - StopWatch(true); - } -} - -event Opened() -{ - //Log("[TestTcp] Accepted!"); - LOG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); -} - -event Closed() -{ - //Log("[TestTcp] Closed!"); -} - -defaultproperties -{ - infoSuccess = (l=LOG_Info,m="Successfully started Avarice link \"%1\"") - fatalBadPort = (l=LOG_Fatal,m="Bad port \"%1\" specified for Avarice link \"%2\"") - fatalCannotBindPort = (l=LOG_Fatal,m="Cannot bind port for Avarice link \"%1\"") - fatalCannotResolveHost = (l=LOG_Fatal,m="Cannot resolve host \"%1\" for Avarice link \"%2\"") - fatalCannotConnect = (l=LOG_Fatal,m="Connection for Avarice link \"%1\" was rejected") -} \ No newline at end of file diff --git a/sources/Avarice/AvariceTcpStream.uc b/sources/Avarice/AvariceTcpStream.uc new file mode 100644 index 0000000000000000000000000000000000000000..afc2dc7ce7171f9ca5d608bd8894a54324e0fbe9 GIT binary patch literal 26976 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. + */ +class Avarice_OnMessage_Signal extends Signal; + +public final function Emit(AvariceLink link, AvariceMessage message) +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + Avarice_OnMessage_Slot(nextSlot).connect(link, message); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'Avarice_OnMessage_Slot' +} \ No newline at end of file diff --git a/sources/Avarice/Events/Avarice_OnMessage_Slot.uc b/sources/Avarice/Events/Avarice_OnMessage_Slot.uc new file mode 100644 index 0000000..32a750f --- /dev/null +++ b/sources/Avarice/Events/Avarice_OnMessage_Slot.uc @@ -0,0 +1,40 @@ +/** + * Slot class implementation for `Avarice`'s `OnMessage` signal. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class Avarice_OnMessage_Slot extends Slot; + +delegate connect(AvariceLink link, AvariceMessage message) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file