commit 4624668be370e073a0ad61ed069a5c653d471498 Author: Anton Tarasenko Date: Fri Aug 12 01:44:17 2022 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +book diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..e0a9b8b --- /dev/null +++ b/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Anton Tarasenko"] +language = "en" +multilingual = false +src = "src" +title = "Lazy cookbook" diff --git a/src/SUMMARY.md b/src/SUMMARY.md new file mode 100644 index 0000000..f00c41b --- /dev/null +++ b/src/SUMMARY.md @@ -0,0 +1,17 @@ +# Summary + +- [Collections recipes](./collections/index.md) + - [Storing built-in values](./collections/storing_builtin.md) + - [Using iterators](./collections/iterators.md) + - [Using JSON data](./collections/using_json.md) +- [Acedia events system](./events/index.md) + - [Connecting to signal](./events/connecting.md) + - [Disconnecting from signal](./events/disconnecting.md) + - [Custom signals](./events/custom.md) +- [Feature recipes](./feature/index.md) + - [Creating a `Feature`](./feature/creating_feature.md) + - [Packaging a `Feature`](./feature/packaging_feature.md) +- [AcediaObject recipes](./object/index.md) + - [Allocating object](./object/allocate.md) + - [Detect object reallocation](./object/detect_reallocation.md) + - [Force object deallocation](./object/force_deallocation.md) diff --git a/src/collections/index.md b/src/collections/index.md new file mode 100644 index 0000000..c7032b9 --- /dev/null +++ b/src/collections/index.md @@ -0,0 +1,4 @@ +# Collections recipes + +A collection of `how-to`s for working with Acedia's collections: +`ArrayList` and `HashTable` classes. diff --git a/src/collections/iterators.md b/src/collections/iterators.md new file mode 100644 index 0000000..0b21b7c --- /dev/null +++ b/src/collections/iterators.md @@ -0,0 +1,74 @@ +# Using iterators + +## Simple iteration + +For both `ArrayList` and `HashTable`: + +```unrealscript +function ListKeyValue(Collection data) +{ + local AcediaObject key, value; + local CollectionIterator iter; + + for (iter = data.Iterate(); !iter.HasFinished(); iter.Next()) + { + key = iter.GetKey(); + value = iter.Get(); + // Do what you want here + _.memory.Free(key); + _.memory.Free(value); + } + iter.FreeSelf(); +} +``` + +## Skipping `none` values + +```unrealscript +function ListKeyValue(Collection data) +{ + local AcediaObject key, value; + local CollectionIterator iter; + + iter = data.Iterate().LeaveOnlyNotNone(); + while (!iter.HasFinished()) + { + key = iter.GetKey(); + value = iter.Get(); + // Do what you want here + _.memory.Free(key); + _.memory.Free(value); + iter.Next(); + } + iter.FreeSelf(); +} +``` + +## Iteration over `Text` keys only + +Text keys are only relevant for `HashTable`s: + +```unrealscript +function ListKeyValue(HashTable data) +{ + local AcediaObject key, value; + local Text textKey; + local CollectionIterator iter; + + for (iter = data.Iterate(); !iter.HasFinished(); iter.Next()) + { + key = iter.GetKey(); + value = iter.Get(); + textKey = Text(key); + if (textKey != none) + { + Log(textKey.ToString() $ ":" + @ _.text.IntoString(_.json.Print(value))); + } + _.memory.Free(key); + _.memory.Free(value); + // `textKey` is the same reference as `key`! + } + iter.FreeSelf(); +} +``` diff --git a/src/collections/storing_builtin.md b/src/collections/storing_builtin.md new file mode 100644 index 0000000..fbff79d --- /dev/null +++ b/src/collections/storing_builtin.md @@ -0,0 +1,53 @@ +# Storing built-in values + +## Storing in the array + +`ArrayList` serves the role of the regular array and storing built-in value data +inside it is simple: + +```unrealscript +local ArrayList data; + +data = _.collections.EmptyArrayList(); +// Add as the last element +data.AddFloat(-2.5); +// Set at a particular index (length will be auto-adjusted) +data.Set(2, "just a string"); + +Log("data[0] =" @ data.GetFloat(0)); // data[0] = -2.5 +Log("data[1] =" @ data.IsNone(1)); // data[1] = true +Log("data[2] =" @ data.GetString(2)); // data[2] = just a string +``` + +## Getting `string` as a `Text` + +By default Acedia's collections use `Text` to store `string`s, so we can also +get their values as `Text`: + +```unrealscript +local Text textInstance; +local ArrayList data; + +data = _.collections.EmptyArrayList(); +data.SetString(0, "Hello, world!"); +textInstance = data.GetText(0); +Log("data[0] =" @ textInstance.ToString()); +// Same as any object returned by a function, `textInstance` must be released +textInstance.FreeSelf(); +``` + +## Storing values by keys + +For storing values using keys instead of numeric indices, the Acedia's way is to +use `HashTable`: + +```unrealscript +local HashTable data; + +data = _.collections.EmptyHashTable(); +data.SetBool(P("Deal damage?"), true); +data.SetInt(P("Damage amount"), 9001); + +Log("Deal damage?" @ data.GetFloat(P("Deal damage?"))); // Deal damage? true +Log("Damage amount:" @ data.IsNone(1)); // Damage amount: 9001 +``` diff --git a/src/collections/using_json.md b/src/collections/using_json.md new file mode 100644 index 0000000..5b19001 --- /dev/null +++ b/src/collections/using_json.md @@ -0,0 +1,123 @@ +# Using JSON data + +JSON data can be stored inside Acedia's collections using only built-in values +types and `ArrayList`/`HashTable` collections: + +* JSON's *null* can be stored as `none`; +* JSON's *true*/*false* can be stored as `bool`' +* JSON's *number* can be stored as either `int` or `float` (actually Acedia also + contains `BigInt` type for storing arbitrarily large integer values, but it + isn't yet implemented into JSON parsing); +* JSON's *string* can be stored as `string`/`Text`/`MutableText`; +* JSON's *array* can be stored as an `ArrayList`; +* JSON's *object* can be stored as `HashTable` with `Text` keys. + +> **NOTE:** JSON *does not* have a separate undefined type and Acedia uses +> `none` instead of the missing values. + +## Converting JSON into Acedia collections + +### Parsing JSON input + +To get an Acedia collection from a JSON object like this one: + +```json +{ + "innerObject": { + "my_bool": true, + "array": [ + "Engine.Actor", + false, + null, + { + "something \"here\"": "yes", + "maybe": 0.003 + }, + 56.6 + ], + "one more": { + "nope": 324532, + "whatever": false, + "o rly?": "ya rly" + }, + "my_int": -9823452 + }, + "some_var": -7.32, + "another_var": "aye!" +} +``` + +recorded in the `string` named `jsonData` you can do the following: + +```unrealscript +local Parser parser; +local HashTable jsonObject; + +parser = _.text.ParseString(jsonData); +jsonObject = HashTable(_.json.ParseWith(parser)); +if (!parser.Ok()) { + // Handle errors +} +else if (!parser.Skip().HasFinished()) { + // There is more input left after parsing JSON value - is this a problem? +} +// '/innerObject/array/3/maybe' is 0.003 +Log("'/innerObject/array/3/maybe' is" + @ jsonObject.GetFloatBy(P("/innerObject/array/3/maybe"))); +``` + +### Constructing by hand + +Example of constructing the same object by hand: + +```unrealscript +local HashTable jsonObject; +local HashTable innerObject, oneMore, anonymousObject; +local ArrayList jsonArray; + +anonymousObject = _.collections.EmptyHashTable(); +anonymousObject.SetString(P("something \"here\""), "yes"); +anonymousObject.SetFloat(P("maybe"), 0.003); + +jsonArray = _.collections.EmptyArrayList() + .AddString("Engine.Actor") + .jsonArray.AddBool(false) + .jsonArray.AddItem(none) + .jsonArray.AddItem(anonymousObject) + .jsonArray.AddFloat(56.6); + +oneMore = _.collections.EmptyHashTable() + .SetString(P("o rly?"), "ya rly") + .SetFloat(P("nope"), 324532) + .SetBool(P("whatever"), false); + +innerObject = _.collections.EmptyHashTable() + .SetItem(P("array"), jsonArray) + .SetItem(P("one more"), oneMore) + .SetBool(P("my_bool"), true) + .SetInt(P("my_int"), -9823452); + +// Put it all together! +jsonObject = _.collections.EmptyHashTable() + .SetItem(P("innerObject"), innerObject) + .SetFloat(P("some_var"), -7.32) + .SetString(P("another_var"), "aye!"); +// If you only want to keep `jsonObject`, release other references - +// they won't disappear +anonymousObject.FreeSelf(); +jsonArray.FreeSelf(); +oneMore.FreeSelf(); +innerObject.FreeSelf(); +``` + +## Converting Acedia collections into JSON + +Use `_.json.Print()` to get a compact JSON representation: + +```unrealscript +// {"innerObject":{"my_bool":true,"array":["Engine.Actor",false,null,{"something \"here\"":"yes","maybe":0.003},56.6],"one more":{"nope":324532,"whatever":false,"o rly?":"ya rly"},"my_int":-9823452},"some_var":-7.32,"another_var":"aye!"} +Log(_.text.IntoString(_.json.Print(jsonObject))); +``` + +or `_.json.PrettyPrint()` for a nice-looking (multiline, indented and colored) +result. diff --git a/src/events/connecting.md b/src/events/connecting.md new file mode 100644 index 0000000..7f514c5 --- /dev/null +++ b/src/events/connecting.md @@ -0,0 +1,43 @@ +# Connecting to signal + +## From inside `AcediaObject` (or its child class) + +Supposing you want to connect to a signal function +`_server.unreal.gameRules.OnNetDamage()`: + +```unrealscript +_server.unreal.gameRules.OnNetDamage(self).connect = handler; +``` + +where `handler()` can be any function with appropriate signature: + +```unrealscript +function int Handler( + int originalDamage, + int damage, + Pawn injured, + Pawn instigator, + Vector hitLocation, + out Vector momentum, + class damageType) +{ + // Do whatever + return damage; +} +``` + +## From inside non-`AcediaObject` + +Pass any other `AcediaObject` object as an argument to your signal function. +For example: + +```unrealscript +local ServiceAnchor receiver; + +receiver = ServiceAnchor(_.memory.Allocate(class'ServiceAnchor')); +_server.unreal.gameRules.OnNetDamage(receiver).connect = handler; +``` + +> **NOTE:** Signal *does not* keep the reference to passed argument and once +> `receiver` gets deallocated - signal will stop notifying handlers connected +> through it. diff --git a/src/events/custom.md b/src/events/custom.md new file mode 100644 index 0000000..00db3ae --- /dev/null +++ b/src/events/custom.md @@ -0,0 +1,163 @@ +# Custom signals + +## Simple notification events + +If you need to add an event with handlers that don't take any parameters and +don't return anything, then easiest way it to use `SimpleSignal` / `SingleSlot` +classes: + +```unrealscript +class MyEventClass extends AcediaObject; + +var private SimpleSignal onMyEventSignal; + +protected function Constructor() +{ + onMyEventSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); +} + +protected function Finalizer() +{ + _.memory.Free(onMyEventSignal); + onMyEventSignal = none; +} + +public function SimpleSlot OnMyEvent(AcediaObject receiver) +{ + return SimpleSlot(onMyEventSignal.NewSlot(receiver)); +} + +// Suppose you want to emit the `signal` when this function is called... +public function SimpleSlot FireOffMyEvent(AcediaObject receiver) +{ + // ...simply call this and all the slots will have their handlers called + onMyEventSignal.Emit(); +} +``` + +Then you can use `OnMyEvent()` as a *signal function*: + +```unrealscript +// To add handlers +myEventClassInstance.OnMyEvent(self).connect = handler; +// To remove handlers +myEventClassInstance.OnMyEvent(self).Disconnect(); +``` + +## Events with parameters + +Some of the events, like `OnNetDamage()`, can take +parameters. +To create signals like that, follow the template and define new classes like so: + +```unrealscript +class MySignal extends Signal; + +public final function Emit() +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + MySlot(nextSlot).connect(); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'MySlot' +} +``` + +```unrealscript +class MySlot extends Slot; + +delegate connect() +{ + DummyCall(); // This allows Acedia to cleanup slots without set handlers +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} +``` + +Here you can use any set of parameters instead of ``. + +## Events with return values + +Sometimes you want your handlers to respond in some way to the event. +You can either allow them to modify input parameters (e.g. by declaring them as +`out`) or allow them to have return value. +`OnNetDamage()`, for example, is allowed to modify incoming damage by returning +a new value. + +To add `signal`s / `slot`s that handle return value use following templates: + +```unrealscript +class MySignal extends Signal; + +public final function Emit() +{ + local newValue; + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + newValue = (nextSlot).connect(); + // This check is necessary before using returned value + if (!nextSlot.IsEmpty()) + { + // Now handle `newValue` however you see fit + } + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); + // Return whatever you see fit after handling all the slots + return ; +} + +defaultproperties +{ + relatedSlotClass = class'MySlot' +} +``` + +```unrealscript +class MySlot extends Slot; + +delegate connect() +{ + DummyCall(); + // Return anything you want: + // this value will be filtered inside corresponding `Signal` + // if no handler is set to the associated slot + return ; +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} +``` diff --git a/src/events/disconnecting.md b/src/events/disconnecting.md new file mode 100644 index 0000000..8caa135 --- /dev/null +++ b/src/events/disconnecting.md @@ -0,0 +1,20 @@ +# Disconnecting from signal + +## From inside `AcediaObject` (or its child class) + +Supposing you want to disconnect from a signal function +`_server.unreal.gameRules.OnNetDamage()`: + +```unrealscript +_server.unreal.gameRules.OnNetDamage(self).Disconnect(); +``` + +## From inside non-`AcediaObject` + +To do that you must have passed another `AcediaObject` as an argument to +`OnNetDamage()` (or any other signal). +Supposing that object was called `receiver`: + +```unrealscript +_server.unreal.gameRules.OnNetDamage(receiver).Disconnect(); +``` diff --git a/src/events/index.md b/src/events/index.md new file mode 100644 index 0000000..450ac17 --- /dev/null +++ b/src/events/index.md @@ -0,0 +1,4 @@ +# Acedia events system + +A collection of `how-to`s for working with Acedia's event system: `Signal`s and +`Slot`s. diff --git a/src/feature/creating_feature.md b/src/feature/creating_feature.md new file mode 100644 index 0000000..55f06bb --- /dev/null +++ b/src/feature/creating_feature.md @@ -0,0 +1,155 @@ +# Creating a Feature + +## Prepare classes + +When creating a `Mutator` you must create a child `Mutator` class, but +when creating a `Feature` you must create two classes: one for the `Feature` +itself and one for its config. +Name your config class something human-readable and your `Feature` class should +have the same name, but with '_Feature' suffix. +For example, if you want your configs to be stored inside 'MyFeatureConfig.ini', +bare minimum class skeletons are: + +```unrealscript +// MyFeature.uc +class MyFeature extends FeatureConfig + perobjectconfig + config(MyFeatureConfig); + +defaultproperties +{ + configName = "MyFeatureConfig" +} +``` + +```unrealscript +// MyFeature_Feature.uc +class MyFeature_Feature extends Feature; + +defaultproperties +{ + configClass = class'MyFeature' +} +``` + +## Setup config variables in config file + +Config variables are meant to be added into your `FeatureConfig` class. +Assuming you want to add `bool` and `int` settings, simply add two public config +variables: + +```unrealscript +// MyFeature.uc +class MyFeature extends FeatureConfig + perobjectconfig + config(MyFeatureConfig); + +var public config int levelOfAwesome; +var public config bool enableTrollEngine; + +defaultproperties +{ + configName = "MyFeatureConfig" +} +``` + +To setup their default values, instead of `defaultproperties` +define `DefaultIt()` method: + +```unrealscript +// MyFeature.uc +protected function DefaultIt() +{ + levelOfAwesome = 11; + enableTrollEngine = true; +} +``` + +### Conversion to/from `HashTable` + +To make full use out of your `Feature`, Acedia also requires that you provide +methods to convert to and from its collection. +For our example simply add two more methods: + +```unrealscript +// MyFeature.uc +protected function HashTable ToData() +{ + local HashTable data; + + data = __().collections.EmptyHashTable(); + data.SetInt(P("levelOfAwesome"), levelOfAwesome); + data.SetBool(P("enableTrollEngine"), enableTrollEngine); + return data; +} + +protected function FromData(HashTable source) +{ + if (source == none) { + return; + } + // Second parameters should are fallback - use values from `DefaultIt()` + levelOfAwesome = source.GetInt(P("levelOfAwesome"), 11); + enableTrollEngine = source.GetBool(P("enableTrollEngine"), true); +} +``` + +## Setup your feature to use your config + +Recommended way to do it is to duplicate your config variables in your +`Feature` class: + +```unrealscript +// MyFeature_Feature.uc +class MyFeature_Feature extends Feature; + +// Not actually config variables +var public /*config*/ int levelOfAwesome; +var public /*config*/ bool enableTrollEngine; + +defaultproperties +{ + configClass = class'MyFeature' +} +``` + +Then add code that will handle setting config data for your `Feature`: + +```unrealscript +// MyFeature_Feature.uc +protected function SwapConfig(FeatureConfig config) +{ + local MyFeature newConfig; + newConfig = MyFeature(config); + if (newConfig == none) { + return; + } + levelOfAwesome = newConfig.levelOfAwesome; + enableTrollEngine = enableTrollEngine.disableTick; + // Here you can also add any logic that needs to be performed when + // values of the config variables were swapped mid-game, if you care to + // support it. + // Just beware that this method can be called when you `Feature` is + // both enabled and not (can be checked with `IsEnabled()`). +} +``` + +## Run initialization / shutdown logic + +Instead of normal Acedia's `Constructor()` or `Finalizer()`, for `Featuire`s +one should use: + +```unrealscript +protected function OnEnabled() +{ +} + +protected function OnDisabled() +{ +} +``` + +These methods are called when feature gets enabled or disabled, which can, +in theory, happen several times per game. +They aren't called when `Feature`'s config is changed mid-game, so sometimes +it is convenient to move part of the initialization logic into `SwapConfig()`. diff --git a/src/feature/index.md b/src/feature/index.md new file mode 100644 index 0000000..b364152 --- /dev/null +++ b/src/feature/index.md @@ -0,0 +1,18 @@ +# Feature recipes + +A collection of `how-to`s for working with `Feature` class. + +This class is Acedia's replacement for `Mutators`: a certain subset of +functionality that can be enabled or disabled, according to server owner's +wishes. +Unlike `Mutator`s: + +* There is no limit for the amount of `Feature`s that can be active + at the same time; +* They also provide built-in ability to have several different configs + that can be swapped during the runtime; +* They can be enabled / disabled during the runtime. + Achieving these points currently comes at the cost of developer having to + perform additional work; +* They are server-side and for now are not supposed to be created for + the clients. diff --git a/src/feature/packaging_feature.md b/src/feature/packaging_feature.md new file mode 100644 index 0000000..14ab41f --- /dev/null +++ b/src/feature/packaging_feature.md @@ -0,0 +1,42 @@ +# Packaging a Feature + +Suppose you've created your own `Feature` class and want to compile it to be +usable with Acedia. +In this how-to we will assume that your class is named `MyFeature_Feature` +(same as [here](./creating_feature.md)). + +## Create regular UnrealScript mod + +First step is still to prepare compilation of `*.u` file, like one would do for +any regular UnrealScript mod and put your `*.uc` script files inside. + +## Create manifest + +To know what is available in each package, Acedia reads its manifest. +To add a manifest create a file named exactly 'Manifest.uc' in your package: + +```unrealscript +// Manifest.uc +class Manifest extends _manifest + abstract; + +defaultproperties +{ +} +``` + +## Register feature in the manifest + +To add your `Feature` into manifest, simply fill it inside `features` array: + +```unrealscript +// Manifest.uc + class Manifest extends _manifest + abstract; + +defaultproperties +{ + // You can add any amount of features from a single package here + features(0) = class'MyFeature_Feature' +} +``` diff --git a/src/object/allocate.md b/src/object/allocate.md new file mode 100644 index 0000000..58237df --- /dev/null +++ b/src/object/allocate.md @@ -0,0 +1,11 @@ +# Allocating object + +If you've created a new child class (say, `MyCoolObject`) of `AcediaObject` and +want to allocate it, simply do: + +```unrealscript +local MyCoolObject instance; +instance = MyCoolObject(_.memory.Allocate(class'MyCoolObject')); +``` + +It is guaranteed to succeed for non-abstract classes. diff --git a/src/object/detect_reallocation.md b/src/object/detect_reallocation.md new file mode 100644 index 0000000..f458396 --- /dev/null +++ b/src/object/detect_reallocation.md @@ -0,0 +1,50 @@ +# Detect object reallocation + +Sometimes you want to keep track of an object without declaring that you own its +reference with `NewRef()`, thus allowing it to get deallocated even while you +are keeping a reference to it. +How to do it in a safe way and detect that it was deallocated/reallocated? + +## What to do when you start storing a reference + +When you first get an instance of `AcediaObject` you'd like to store in such +a way, check if it's allocated and remember its life version: + +```unrealscript +var int storedReferenceLifeVersion; +var AcediaObject storedReference; + +function Store(AcediaObject newReference) +{ + if (newReference == none) return; + if (!newReference.IsAllocated()) return; + + storedReference = newReference; + storedReferenceLifeVersion = newReference.GetLifeVersion(); + // Not calling `newReference.NewRef()`, so `newReference` can get + // deallocated at any time! +} +``` + +## What to do when you want to check if stored reference was deallocated + +Before using such reference again you simply need to check if life version has +changed. +If it did, then this instance was reallocated and repurposed and you shouldn't +use it anymore. +Othewrwise it is still the same object. + +```unrealscript +function AcediaObject Get() +{ + if (storedReference == none) { + return none; + } + if (storedReference.GetLifeVersion() != storedReferenceLifeVersion) + { + // This object was reallocated! Time to forget about it. + storedReference = none; + } + return storedReference; +} +``` diff --git a/src/object/force_deallocation.md b/src/object/force_deallocation.md new file mode 100644 index 0000000..1d30efe --- /dev/null +++ b/src/object/force_deallocation.md @@ -0,0 +1,10 @@ +# Forcing object deallocation + +`self.FreeSelf()` and `_.memory.Free()` only reduce reference counter of +the object. +But how to forcefully deallocate it? + +**Just do not do that you fucking idiot**. + +If you need to somehow dispose of what that object represents, simply add +a flag that marks that object as disposed. diff --git a/src/object/index.md b/src/object/index.md new file mode 100644 index 0000000..d8ee8a9 --- /dev/null +++ b/src/object/index.md @@ -0,0 +1,9 @@ +# AcediaObject recipes + +A collection of `how-to`s for working with `AcediaObject` class and related +topics. + +Base object class to be used in Acedia instead of an `Object`. +`AcediaObject` provides access to Acedia's APIs through an accessor to +a `Global` object, built-in mechanism for storing unneeded references in +an object pool and constructor/finalizer.