Compare commits

...

107 Commits

Author SHA1 Message Date
Anton Tarasenko f15e704ce2 Fix usage of freed object 1 year ago
Anton Tarasenko 4c0b2a6f1d Update version number 1 year ago
Anton Tarasenko e8ae6fd8d1 Change votings to output outcome message in one line 1 year ago
Anton Tarasenko 93604c7690 Remove temporary comment 1 year ago
Anton Tarasenko 159f1dc5a1 Fix storing user groups in databases not working 1 year ago
Anton Tarasenko 15b1abc8c3 Fix infinite loop bug with UnflectAPI rollback 1 year ago
Anton Tarasenko ff31ef2472 Remove duplicate voting tests 1 year ago
Anton Tarasenko c2a8a5c7de Fix configs 1 year ago
Anton Tarasenko a27e893359 Change `InfoQueryHandler` to respect "help" command's name change 1 year ago
Anton Tarasenko 0ad28839bb Change "usergroups" commands to adapt to new `CommandsAPI` 1 year ago
Anton Tarasenko a26b0adf05 Change `UserAPI` to make "all" user group contain all players 1 year ago
Anton Tarasenko 80cecd1d20 Change Voting classes to work with new API 1 year ago
Anton Tarasenko 7ff806b104 Change built-in commands to support new `CommandAPI` 1 year ago
Anton Tarasenko be9ba80549 Add back voting tests 1 year ago
Anton Tarasenko d7ed4776b4 Fix formatting in some commands-related classes 1 year ago
Anton Tarasenko a7f1a98548 Change CommandAPI and feature to use tools 1 year ago
Anton Tarasenko 86228a960c Add signal classes for `CommandAPI` 1 year ago
Anton Tarasenko c1dccfc2d6 Change data convertion flag to `true` for all feature configs 1 year ago
Anton Tarasenko 001170e092 Add `IsSpectator()` check for `EPlayer` 1 year ago
Anton Tarasenko 87c7ee01bb Add `IntoStrings()` method to `TextAPI` 1 year ago
Anton Tarasenko c76f875620 Add tool classes for `Commands_Feature` 1 year ago
Anton Tarasenko 23dc639536 Change what files are used to store permissions 1 year ago
Anton Tarasenko 7ad3ca55f6 Fix return values in some signals/methods 1 year ago
Anton Tarasenko 757ae39b2e Change environment to not be in debug mode by default 1 year ago
Anton Tarasenko 70c41a5926 Add cleanup for `UnflectApi` 2 years ago
dkanus 632ff8ef2c Merge pull request 'Add voice signal for catching voice messages to `CharApi`' (#14) from feature_voice_messages_api into develop 2 years ago
Anton Tarasenko 87570a4906 Add `OnVoiceMessage()` signal function to `ChatApi` 2 years ago
Anton Tarasenko 8a6891793f Rename local variable to avoid conflicts 2 years ago
Anton Tarasenko 41db52ded8 Add `SendVoiceMessage()` command to `EPlayer` 2 years ago
Anton Tarasenko 4ede173e62 Add `BuiltInVoiceMessage` enum 2 years ago
Anton Tarasenko 26ed844044 Change comment style for `ChatApi` 2 years ago
dkanus ecb29d3759 Merge pull request 'Add "sideeffects" command for viewing active side effects' (#13) from feature_side_effects_command into develop 2 years ago
Anton Tarasenko 9187598252 Add `sideffects` command 2 years ago
Anton Tarasenko fec8535c0f Make `User`'s equality depend on its `UserID` 2 years ago
Anton Tarasenko 3d2518ccdd Add method for checking whether `SideEffect` is active 2 years ago
dkanus 0354396d31 Merge pull request 'Add `UnflectApi`' (#12) from feature_uflect into develop 2 years ago
Anton Tarasenko ced0f1dd99 Add `UnflectApi` 2 years ago
Anton Tarasenko f51cf8ed64 Refactor `SideEffect`s to work better with `UnflectApi` 2 years ago
Anton Tarasenko 10b673ccec Merge branch 'enchance_notify_channels' into develop 2 years ago
Anton Tarasenko c32ae4b972 Merge branch 'refactor_commands' into develop 2 years ago
Anton Tarasenko fc52110d16 Add channel support for notifications 2 years ago
Anton Tarasenko 5daa6e8e02 Remove minimal rounds requirement from voting config 2 years ago
dkanus 4cf66442cb Merge pull request 'Refactor `JsonPointer` into separate (im)mutable versions' (#10) from feature_immutable_json_pointer into develop 2 years ago
Anton Tarasenko 6321368665 Refactor `JsonPointer` into separate (im)mutable versions 2 years ago
Anton Tarasenko 0432c1c074 Add basic voting functionality 2 years ago
Anton Tarasenko 028c2eaf83 Fix documentation and naming for `VotingModel` 2 years ago
Anton Tarasenko 0746aef7c7 Document `VotingModel` class 2 years ago
Anton Tarasenko fed80cc76b Fix noficications not respecting provided duration time 2 years ago
Anton Tarasenko 3924355e79 Fix after merge file duplicate 2 years ago
Anton Tarasenko a9fd4abb64 Merge branch 'develop' into refactor_commands 2 years ago
dkanus 64de53dcd0 Merge pull request 'Add player notifications' (#9) from feat_player_announcements into develop 2 years ago
Anton Tarasenko 070a33b410 Add player notifications 2 years ago
Anton Tarasenko 2606c0d001 Add `VotingModel` for vote counting 2 years ago
Anton Tarasenko 52f9aa5aa5 Add command locks 2 years ago
Anton Tarasenko 677dd84e90 Fix style for remaining `CommandAPI`-related classes 2 years ago
Anton Tarasenko c4247a67d0 Fix style for `CommandParser` 2 years ago
Anton Tarasenko 3d7f11688c Add async methods for registering commands 2 years ago
Anton Tarasenko 9265e97c59 Move `CommandAPI` into base API 2 years ago
Anton Tarasenko 41909851f5 Refactor `Commands_Feature` to use API 2 years ago
Anton Tarasenko 087d8624d3 Change how config is swapped for `Feature`s 2 years ago
Anton Tarasenko 58d1d686b9 Add auto-highlighting of commands' descriptions 2 years ago
Anton Tarasenko 06915cbddf Change auto-alias resolving in commands to be useful 2 years ago
Anton Tarasenko 626124335d Fix style for `CommandDataBuilder` 2 years ago
Anton Tarasenko 82ff13e230 Change directory structure and `CoreAPI` naming 2 years ago
Anton Tarasenko 21a60482d5 Add `FreeN()` set of methods 2 years ago
Anton Tarasenko 1e1b3b3739 Fix `Api` capitalization in API class names 2 years ago
Anton Tarasenko ea79a1bc33 Fix style for `EnvironmentApi` 2 years ago
Anton Tarasenko 5bba953cb0 Fix style for `SchedulerApi` 2 years ago
Anton Tarasenko 15a49d8ddc Fix style for `BigInt` 2 years ago
Anton Tarasenko 7b2747b7c8 Fix `MathApi` formatting/documentation 2 years ago
Anton Tarasenko d10860cb3f Fix style for `MemoryAPI` tests 2 years ago
Anton Tarasenko e027c3cc53 Refactor `MemoryAPI` to only work with `AcediaObject`s 2 years ago
Anton Tarasenko 863e440149 Fix style for `Global` 2 years ago
Anton Tarasenko c8c70836c8 Gix style for `_manifest` base class 2 years ago
Anton Tarasenko a6738c55ad Fix style of `Iter` and all related classes 2 years ago
Anton Tarasenko 78aff18cad Refactor `UserAPI` 2 years ago
Anton Tarasenko 36c8f7f65a Add methods for scheduling `AcediaConfig` saving 2 years ago
Anton Tarasenko adabf42176 Add default config for databases 2 years ago
Anton Tarasenko 566e5866be Fix potential usage of deallocated objects 2 years ago
Anton Tarasenko e553d1a08e Fix `DBAPI` relying on server core API instead of generic one 2 years ago
Anton Tarasenko 90abd8f80e Add `DBConnection` class 2 years ago
Anton Tarasenko 190a609b33 Add tests for new collections' `Append()` methods 2 years ago
Anton Tarasenko 59d6ca492d Add `Append` method to `HashTable` 2 years ago
Anton Tarasenko f4f3684f3f Add methods for checking value existence for `Collection`s 2 years ago
Anton Tarasenko e46debe99f Add specialized getters for returning `Collection`s to themselves 2 years ago
Anton Tarasenko 56933914fa Fix some bugs with dynamic array allocation 2 years ago
Anton Tarasenko 4db04c726c Remove useless code from `ArrayList` 2 years ago
Anton Tarasenko 4a464b025e Add `Append()` method for `ArrayList` 2 years ago
Anton Tarasenko 845a6a944a Fix `LocalDatabase` not supporting database API changes 2 years ago
Anton Tarasenko 71dba2bac7 Add request ID support to `DBAPI` 2 years ago
Anton Tarasenko bdaccd4586 Add accessors to generic Core API to Acedia's objects and actors 2 years ago
Anton Tarasenko 9bb471bdf4 Add tests for new `JSONPointer` methods 2 years ago
Anton Tarasenko 09731be8c3 Add auxiliary methods for JSONPointer 2 years ago
Anton Tarasenko dbf55dfa17 Add ability to copy only part of `JSONPointer` 2 years ago
Anton Tarasenko 822d9507bf Add method for incrementing JSON values into `JSONAPI` 2 years ago
Anton Tarasenko 974f7ee183 Add methods for deep copying of JSON-compatible data 2 years ago
Anton Tarasenko e562072a90 Add methods for testing casing of `BaseText`'s charracters 2 years ago
Anton Tarasenko 74be378e5e Move `DBAPI` from base realm into core realm 2 years ago
Anton Tarasenko 290f756264 Add generic accessors for Core-related API 2 years ago
Anton Tarasenko d709461b9c Remove excessive definition of `sideEffectAPIClass` in `AcediaAdapter` 2 years ago
Anton Tarasenko 007fd29bc3 Change `Global` to create `JSONAPI` earlier in the API chain 2 years ago
Anton Tarasenko 82cb46d886 Clarify documentation for `Feature`'s `EnableMe()` 2 years ago
Anton Tarasenko 82c598c210 Fix bad arg index in error message 2 years ago
Anton Tarasenko 816bf3968d Fix memory leak in `DBAPI`'s `LoadLocal()` 2 years ago
Anton Tarasenko f9841fd473 Remove unnecessary text type conversion in `DBAPI` 2 years ago
Anton Tarasenko 70d3dc5957 Move command classes into CoreRealm 2 years ago
Anton Tarasenko cd056b9daa Document commands-related classes 2 years ago
  1. 2
      config/AcediaAliases_Colors.ini
  2. 8
      config/AcediaAliases_Commands.ini
  3. 117
      config/AcediaCommands.ini
  4. 5
      config/AcediaDB.ini
  5. 11
      config/AcediaSystem.ini
  6. 35
      config/AcediaUsers.ini
  7. 22
      config/AcediaVoting.ini
  8. 3
      sources/Aliases/Aliases.uc
  9. 3
      sources/Aliases/Aliases_Feature.uc
  10. 6
      sources/Avarice/AvariceLink.uc
  11. 163
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc
  12. 306
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc
  13. 69
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc
  14. 197
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandSideEffects.uc
  15. 220
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc
  16. 805
      sources/BaseAPI/API/Commands/Command.uc
  17. 1578
      sources/BaseAPI/API/Commands/CommandAPI.uc
  18. 939
      sources/BaseAPI/API/Commands/CommandDataBuilder.uc
  19. 249
      sources/BaseAPI/API/Commands/CommandList.uc
  20. 983
      sources/BaseAPI/API/Commands/CommandParser.uc
  21. 66
      sources/BaseAPI/API/Commands/CommandPermissions.uc
  22. 81
      sources/BaseAPI/API/Commands/CommandRegistrationJob.uc
  23. 230
      sources/BaseAPI/API/Commands/Commands.uc
  24. 708
      sources/BaseAPI/API/Commands/Commands_Feature.uc
  25. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Signal.uc
  26. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Slot.uc
  27. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Signal.uc
  28. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Slot.uc
  29. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Signal.uc
  30. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Slot.uc
  31. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Signal.uc
  32. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Slot.uc
  33. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Signal.uc
  34. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Slot.uc
  35. 494
      sources/BaseAPI/API/Commands/PlayersParser.uc
  36. 21
      sources/BaseAPI/API/Commands/Tests/MockCommandA.uc
  37. 61
      sources/BaseAPI/API/Commands/Tests/MockCommandB.uc
  38. 0
      sources/BaseAPI/API/Commands/Tests/TEST_Command.uc
  39. 24
      sources/BaseAPI/API/Commands/Tests/TEST_CommandDataBuilder.uc
  40. 351
      sources/BaseAPI/API/Commands/Tests/TEST_Voting.uc
  41. 306
      sources/BaseAPI/API/Commands/Tools/CmdItemsTool.uc
  42. 142
      sources/BaseAPI/API/Commands/Tools/CommandsTool.uc
  43. 177
      sources/BaseAPI/API/Commands/Tools/ItemCard.uc
  44. 119
      sources/BaseAPI/API/Commands/Tools/VotingsTool.uc
  45. 863
      sources/BaseAPI/API/Commands/Voting/Voting.uc
  46. 440
      sources/BaseAPI/API/Commands/Voting/VotingModel.uc
  47. 132
      sources/BaseAPI/API/Commands/Voting/VotingPermissions.uc
  48. 654
      sources/BaseAPI/API/Math/BigInt.uc
  49. 104
      sources/BaseAPI/API/Math/MathAPI.uc
  50. 63
      sources/BaseAPI/API/Math/Tests/TEST_BigInt.uc
  51. 158
      sources/BaseAPI/API/Memory/AcediaObjectPool.uc
  52. 366
      sources/BaseAPI/API/Memory/MemoryAPI.uc
  53. 16
      sources/BaseAPI/API/Memory/Tests/MockActor.uc
  54. 16
      sources/BaseAPI/API/Memory/Tests/MockObject.uc
  55. 10
      sources/BaseAPI/API/Memory/Tests/MockObjectNoPool.uc
  56. 163
      sources/BaseAPI/API/Memory/Tests/TEST_Memory.uc
  57. 333
      sources/BaseAPI/API/Scheduler/SchedulerAPI.uc
  58. 31
      sources/BaseAPI/API/Scheduler/SchedulerDiskRequest.uc
  59. 44
      sources/BaseAPI/API/Scheduler/SchedulerJob.uc
  60. 0
      sources/BaseAPI/API/Scheduler/Tests/MockJob.uc
  61. 0
      sources/BaseAPI/API/Scheduler/Tests/TEST_SchedulerAPI.uc
  62. 277
      sources/BaseAPI/API/SideEffects/SideEffect.uc
  63. 212
      sources/BaseAPI/API/SideEffects/SideEffectAPI.uc
  64. 76
      sources/BaseAPI/API/Unflect/FunctionReplacement.uc
  65. 30
      sources/BaseAPI/API/Unflect/Tests/MockInitialClass.uc
  66. 33
      sources/BaseAPI/API/Unflect/Tests/MockReplacerClass.uc
  67. 87
      sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc
  68. 29
      sources/BaseAPI/API/Unflect/TypeCast.uc
  69. 49
      sources/BaseAPI/API/Unflect/UClass.uc
  70. 30
      sources/BaseAPI/API/Unflect/UClassCast.uc
  71. 28
      sources/BaseAPI/API/Unflect/UField.uc
  72. 30
      sources/BaseAPI/API/Unflect/UFieldCast.uc
  73. 35
      sources/BaseAPI/API/Unflect/UFunction.uc
  74. 17
      sources/BaseAPI/API/Unflect/UFunctionCast.uc
  75. 41
      sources/BaseAPI/API/Unflect/UProperty.uc
  76. 30
      sources/BaseAPI/API/Unflect/UPropertyCast.uc
  77. 30
      sources/BaseAPI/API/Unflect/UState.uc
  78. 30
      sources/BaseAPI/API/Unflect/UStateCast.uc
  79. 48
      sources/BaseAPI/API/Unflect/UStruct.uc
  80. 30
      sources/BaseAPI/API/Unflect/UStructCast.uc
  81. 28
      sources/BaseAPI/API/Unflect/UTextBuffer.uc
  82. 32
      sources/BaseAPI/API/Unflect/Unflect.uc
  83. 432
      sources/BaseAPI/API/Unflect/UnflectApi.uc
  84. 411
      sources/BaseAPI/AcediaEnvironment/AcediaEnvironment.uc
  85. 15
      sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureDisabled_Signal.uc
  86. 38
      sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureDisabled_Slot.uc
  87. 16
      sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureEnabled_Signal.uc
  88. 38
      sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureEnabled_Slot.uc
  89. 132
      sources/BaseAPI/Global.uc
  90. 68
      sources/BaseAPI/Iter.uc
  91. 27
      sources/BaseAPI/_manifest.uc
  92. 839
      sources/BaseRealm/API/Math/BigInt.uc
  93. 131
      sources/BaseRealm/API/Math/MathAPI.uc
  94. 175
      sources/BaseRealm/API/Memory/AcediaObjectPool.uc
  95. 437
      sources/BaseRealm/API/Memory/MemoryAPI.uc
  96. 421
      sources/BaseRealm/API/Scheduler/SchedulerAPI.uc
  97. 47
      sources/BaseRealm/API/Scheduler/SchedulerJob.uc
  98. 526
      sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc
  99. 104
      sources/BaseRealm/Global.uc
  100. 76
      sources/BaseRealm/Iter.uc
  101. Some files were not shown because too many files have changed in this diff Show More

2
config/AcediaAliases_Colors.ini

@ -8,7 +8,7 @@ record=(alias="TextSubHeader",value="rgb(147,112,219)")
record=(alias="TextPositive",value="rgb(60,220,20)")
record=(alias="TextNeutral",value="rgb(255,255,0)")
record=(alias="TextNegative",value="rgb(220,20,60)")
record=(alias="TextSubtle",value="rgb(128,128,128)")
record=(alias="TextSubtle",value="rgb(211,211,211)")
record=(alias="TextEmphasis",value="rgb(0,128,255)")
record=(alias="TextOk",value="rgb(0,255,0)")
record=(alias="TextWarning",value="rgb(255,128,0)")

8
config/AcediaAliases_Commands.ini

@ -0,0 +1,8 @@
; This config file allows you to configure command aliases.
; Remember that aliases are case-insensitive.
[AcediaCore.CommandAliasSource]
record=(alias="yes",value="vote.yes")
record=(alias="no",value="vote.no")
[help CommandAliases]
Alias="hlp"

117
config/AcediaCommands.ini

@ -0,0 +1,117 @@
[default Commands]
autoEnable=true
;= Setting this to `true` enables players to input commands with "mutate"
;= console command.
;= Default is `true`.
useMutateInput=true
;= Setting this to `true` enables players to input commands right in the chat
;= by prepending them with [`chatCommandPrefix`].
;= Default is `true`.
useChatInput=true
;= Chat messages, prepended by this prefix will be treated as commands.
;= Default is "!". Empty values are also treated as "!".
chatCommandPrefix=!
;= Allows to specify which user groups are used in determining command/votings
;= permission.
;= They must be specified in the order of importance: from the group with
;= highest level of permissions to the lowest. When determining player's
;= permission to use a certain command/voting, his group with the highest
;= available permissions will be used.
commandGroup=admin
commandGroup=moderator
commandGroup=trusted
commandGroup=all
;= Add a specified `CommandList` to the specified user group
addCommandList=(name="default",for="all")
addCommandList=(name="moderator",for="moderator")
addCommandList=(name="admin",for="admin")
addCommandList=(name="debug",for="admin")
;= Allows to specify a name for a certain command class
;=
;= NOTE:By default command choses that name by itself and its not recommended
;= to override it. You should only use this setting in case there is naming
;= conflict between commands from different packages.
;=renamingRule=(rename=class'ACommandHelp',to="lol")
;= Allows to specify a name for a certain voting class
;=
;= NOTE:By default voting choses that name by itself and its not recommended
;= to override it. You should only use this setting in case there is naming
;= conflict between votings from different packages.
;=votingRenamingRule=(rename=class'Voting',to="lol")
;= `CommandList` describes a set of commands and votings that can be made
;= available to users inside Commands feature
;=
;= Optionally, permission configs can be specified for commands and votings,
;= allowing server admins to create command lists for different groups player
;= with the same commands, but different permissions.
[default CommandList]
;= Allows to specify if this list should only be added when server is running
;= in debug mode.
;= `true` means yes, `false` means that list will always be available.
debugOnly=false
;= Adds a command of specified class with a "default" permissions config
command=class'ACommandHelp'
command=class'ACommandVote'
;= Adds a voting of specified class with a "default" permissions config
voting=class'Voting'
;= Adds a command of specified class with specified permissions config
;=commandWith=(cmd=,config="")
;= Adds a voting of specified class with specified permissions config
;=commandWith=(vtn=,config="")
[debug CommandList]
debugOnly=true
command=class'ACommandFakers'
[moderator CommandList]
command=class'ACommandNotify'
[admin CommandList]
command=class'ACommandSideEffects'
;= `VotingPermissions` describe use permission settings for a voting
[default VotingPermissions]
;= Determines the duration of the voting period, specified in seconds.
;= Zero or negative values mean unlimited voting period.
votingTime=30
;= Determines how draw will be interpreted.
;= `true` means draw counts as a vote's success, `false` means draw counts as a vote's failure.
drawEqualsSuccess=false
;= Determines whether spectators are allowed to vote.
allowSpectatorVoting=false
;= Specifies which group(s) of players are allowed to see who makes what vote.
allowedToVoteGroup=all
;= Specifies which group(s) of players are allowed to see who makes what vote.
allowedToSeeVotesGroup=all
;= Specifies which group(s) of players are allowed to forcibly end voting.
allowedToForceGroup=admin
allowedToForceGroup=moderator
[anonymous VotingPermissions]
votingTime=30
drawEqualsSuccess=false
allowSpectatorVoting=false
allowedToVoteGroup=all
allowedToSeeVotesGroup=admin
allowedToSeeVotesGroup=moderator
allowedToForceGroup=admin
allowedToForceGroup=moderator
[moderator VotingPermissions]
votingTime=60
drawEqualsSuccess=false
allowSpectatorVoting=false
allowedToVoteGroup=admin
allowedToVoteGroup=moderator
allowedToSeeVotesGroup=admin
allowedToForceGroup=admin
[admin VotingPermissions]
votingTime=60
drawEqualsSuccess=false
allowSpectatorVoting=true
allowedToVoteGroup=admin
allowedToSeeVotesGroup=admin
allowedToForceGroup=admin

5
config/AcediaDB.ini

@ -0,0 +1,5 @@
; Define all databases you want Acedia to use here.
; For simply making default Acedia configs work, set `createIfMissing` below
; to `true`.
[Database LocalDatabase]
createIfMissing=false

11
config/AcediaSystem.ini

@ -1,6 +1,9 @@
; Every single option in this config should be considered [ADVANCED].
; DO NOT CHANGE THEM unless you are sure you know what you're doing.
[AcediaCore.SideEffects]
[AcediaCore.AcediaEnvironment]
debugMode=false
[AcediaCore.ServerSideEffects]
; Acedia requires adding its own `GameRules` to listen to many different
; game events.
; It's normal for a mod to add its own game rules: game rules are
@ -140,13 +143,17 @@ requiredGroup=""
maxVisibleLineWidth=80
maxTotalLineWidth=108
[AcediaCore.PlayerNotificationQueue]
; Maximum time that a notification is allowed to be displayed on the player's screen
maximumNotifyTime=20
[AcediaCore.ColorAPI]
; Changing these values will alter color's definitions in `ColorAPI`,
; changing how Acedia behaves
TextDefault=(R=255,G=255,B=255,A=255)
TextHeader=(R=128,G=0,B=128,A=255)
TextSubHeader=(R=147,G=112,B=219,A=255)
TextSubtle=(R=128,G=128,B=128,A=255)
TextSubtle=(R=211,G=211,B=211,A=255)
TextEmphasis=(R=0,G=128,B=255,A=255)
TextPositive=(R=0,G=128,B=0,A=255)
TextNeutral=(R=255,G=255,B=0,A=255)

35
config/AcediaUsers.ini

@ -0,0 +1,35 @@
; Acedia requires adding its own `GameRules` to listen to many different
; game events.
; In this config you can setup Acedia's user groups and persistent data
; storage. Enabling this feature automatically enables user group support,
; while persistent data is optional.
; Databases can be configured in `AcediaDB.ini`.
[default Users]
; Configures whether to use database (and which) for storing user groups.
; Set `useDatabaseForGroupsData` to `false` if you want to define which users
; belong to what groups inside this config.
useDatabaseForGroupsData=true
groupsDatabaseLink=[local]Database:/group_data
; Configures whether persistent data should be additionally used.
; It can only be stored inside a database.
usePersistentData=true
persistentDataDatabaseLink=[local]Database:/user_data
; Available groups. Only used if `useDatabaseForGroupsData` is set to `false`.
localUserGroup=admin
localUserGroup=moderator
localUserGroup=trusted
; These groups definitions only work in case you're using a config with
; `useDatabaseForGroupsData` set to `false`. Simply add new `user=` record,
; specifying SteamIDs of the players, e.g. `user=76561197960287930`.
; You can also optionally specify a human-readable lable for the SteamID after
; slash "/", e.g. `user=76561197960287930/gabe`.
[admin UserGroup]
;user=
[moderator UserGroup]
;user=
[trusted UserGroup]
;user=

22
config/AcediaVoting.ini

@ -0,0 +1,22 @@
[default VotingSettings]
;= Determines the duration of the voting period, specified in seconds.
votingTime=30
;= Determines whether spectators are allowed to vote.
allowSpectatorVoting=false
;= Specifies which group(s) of players are allowed to see who makes what vote.
allowedToSeeVotesGroup="admin"
allowedToSeeVotesGroup="moderator"
;= Specifies which group(s) of players are allowed to vote.
allowedToVoteGroup="everybody"
[moderator VotingSettings]
votingTime=30
allowSpectatorVoting=true
allowedToSeeVotesGroup="admin"
allowedToVoteGroup="moderator"
allowedToVoteGroup="admin"
[admin VotingSettings]
votingTime=30
allowSpectatorVoting=true
allowedToVoteGroup="admin"

3
sources/Aliases/Aliases.uc

@ -98,7 +98,8 @@ protected function ReadOtherSources(HashTable otherSourcesData)
local HashTableIterator iter;
customSource.length = 0;
iter = HashTableIterator(otherSourcesData.Iterate().LeaveOnlyNotNone());
iter = HashTableIterator(otherSourcesData.Iterate());
iter.LeaveOnlyNotNone();
for (iter = iter; !iter.HasFinished(); iter.Next())
{
key = iter.GetKey();

3
sources/Aliases/Aliases_Feature.uc

@ -112,15 +112,12 @@ protected function SwapConfig(FeatureConfig config)
if (newConfig == none) {
return;
}
_.memory.Free(weaponAliasSource);
DropSources();
weaponAliasSource = GetSource(newConfig.weaponAliasSource);
colorAliasSource = GetSource(newConfig.colorAliasSource);
featureAliasSource = GetSource(newConfig.featureAliasSource);
entityAliasSource = GetSource(newConfig.entityAliasSource);
commandAliasSource = GetSource(newConfig.commandAliasSource);
LoadCustomSources(newConfig.customSource);
_.alias._reloadSources();
}
private function LoadCustomSources(

6
sources/Avarice/AvariceLink.uc

@ -237,10 +237,14 @@ private final function Avarice_OnMessage_Signal GetServiceSignal(
public final function StartUp()
{
local AvariceTcpStream newStream;
local LevelCore core;
if (tcpStream == none) return;
if (tcpStream.Get() != none) return;
core = __level().GetLevelCore();
if (core == none) return;
newStream = AvariceTcpStream(_.memory.Allocate(class'AvariceTcpStream'));
newStream = AvariceTcpStream(core.Allocate(class'AvariceTcpStream'));
if (newStream == none)
{
// `linkName` has to be defined if `tcpStream` is defined

163
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc

@ -0,0 +1,163 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandFakers extends Command
dependsOn(VotingModel);
var private array<UserID> fakers;
protected static function StaticFinalizer() {
__().memory.FreeMany(default.fakers);
default.fakers.length = 0;
}
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("debug"));
builder.Summary(P("Adds fake voters for testing \"vote\" command."));
builder.Describe(P("Displays current fake voters."));
builder.SubCommand(P("amount"));
builder.Describe(P("Specify amount of faker that are allowed to vote."));
builder.ParamInteger(P("fakers_amount"));
builder.SubCommand(P("vote"));
builder.Describe(P("Make a vote as a faker."));
builder.ParamInteger(P("faker_number"));
builder.ParamBoolean(P("vote_for"));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
if (arguments.subCommandName.IsEmpty()) {
DisplayCurrentFakers();
} else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) {
ChangeAmount(arguments.parameters.GetInt(P("fakers_amount")));
} else if (arguments.subCommandName.Compare(P("vote"), SCASE_INSENSITIVE)) {
CastVote(
arguments.parameters.GetInt(P("faker_number")),
arguments.parameters.GetBool(P("vote_for")));
}
}
public final static function /*borrow*/ array<UserID> BorrowDebugVoters() {
return default.fakers;
}
private final function CastVote(int fakerID, bool voteFor) {
local Voting currentVoting;
if (fakerID < 0 || fakerID >= fakers.length) {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("Faker number is out of bounds."));
return;
}
currentVoting = _.commands.GetCurrentVoting();
if (currentVoting == none) {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("There is no voting active right now."));
return;
}
currentVoting.CastVoteByID(fakers[fakerID], voteFor);
_.memory.Free(currentVoting);
}
private final function ChangeAmount(int newAmount) {
local int i;
local Text nextIDName;
local UserID nextID;
local Voting currentVoting;
if (newAmount < 0) {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("Cannot specify negative amount."));
}
if (newAmount == fakers.length) {
callerConsole
.UseColor(_.color.TextNeutral)
.WriteLine(P("Specified same amount of fakers."));
} else if (newAmount > fakers.length) {
for (i = fakers.length; i < newAmount; i += 1) {
nextIDName = _.text.FromString("DEBUG:FAKER:" $ i);
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(nextIDName);
_.memory.Free(nextIDName);
fakers[fakers.length] = nextID;
}
} else {
for (i = fakers.length - 1; i >= newAmount; i -= 1) {
_.memory.Free(fakers[i]);
}
fakers.length = newAmount;
}
default.fakers = fakers;
currentVoting = _.commands.GetCurrentVoting();
if (currentVoting != none) {
currentVoting.SetDebugVoters(default.fakers);
_.memory.Free(currentVoting);
}
}
private function DisplayCurrentFakers() {
local int i;
local VotingModel.PlayerVoteStatus nextVoteStatus;
local MutableText nextNumber;
local Voting currentVoting;
if (fakers.length <= 0) {
callerConsole.WriteLine(P("No fakers!"));
return;
}
currentVoting =_.commands.GetCurrentVoting();
for (i = 0; i < fakers.length; i += 1) {
nextNumber = _.text.FromIntM(i);
callerConsole
.Write(P("Faker #"))
.Write(nextNumber)
.Write(P(": "));
if (currentVoting != none) {
nextVoteStatus = currentVoting.GetVote(fakers[i]);
}
switch (nextVoteStatus) {
case PVS_NoVote:
callerConsole.WriteLine(P("no vote"));
break;
case PVS_VoteFor:
callerConsole.UseColorOnce(_.color.TextPositive).WriteLine(P("vote for"));
break;
case PVS_VoteAgainst:
callerConsole.UseColorOnce(_.color.TextNegative).WriteLine(P("vote against"));
break;
default:
callerConsole.UseColorOnce(_.color.TextFailure).WriteLine(P("vote !ERROR!"));
}
_.memory.Free(nextNumber);
}
}
defaultproperties {
preferredName = "fakers"
}

306
sources/Commands/BuiltInCommands/ACommandHelp.uc → sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc

@ -1,6 +1,6 @@
/**
* Command for displaying help information about registered Acedia's commands.
* Copyright 2021-2022 Anton Tarasenko
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -18,13 +18,62 @@
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandHelp extends Command
dependson(LoggerAPI);
dependson(LoggerAPI)
dependson(CommandAPI);
/**
* # `ACommandHelp`
*
* This built-in command is meant to do two things:
*
* 1. List all available commands and aliases related to them;
* 2. Display help pages for available commands (both by command and
* alias names).
*
* ## Implementation
*
* ### Aliases loading
*
* First thing this command tries to do upon creation is to read all
* command aliases and build a reverse map that allows to access aliases names
* by command + subcommand (even if latter one is empty) that these names
* refer to. This allows us to display relevant aliases names alongside
* command names. Map is stored inside `commandToAliasesMap` variable.
* (If aliases feature isn't yet available, `ACommandHelp` connects to
* `OnFeatureEnabled()` signal to do its job the moment required feature is
* enabled).
* This map is also made to be two-level one - it is...
*
* * a `HashTable` with command names as keys,
* * that points to `HashTable`s with subcommand names as keys,
* * that points at `ArrayList` of aliases.
*
* This allows us to also sort aliases by the subcommand name when displaying
* their list. This work is done by `FillCommandToAliasesMap()` method (that
* uses `ParseCommandNames()` method, which is also used for displaying
* command list).
*
* ### Command list displaying
*
* This is a simple process performed by `DisplayCommandLists()` that calls on
* a bunch of auxiliary `Print...()` methods.
*
* ### Displaying help pages
*
* Similar to the above, this is mostly a simple process that displays
* `Command`s' `CommandData` as a help page. Work is performed by
* `DisplayCommandHelpPages()` method that calls on a bunch of auxiliary
* `Print...()` methods, most notably `PrintHelpPageFor()` that can display
* help pages both for full commands, as well as only for a singular
* subcommand, which allows us to print proper help pages for command aliases
* that refer to a particular subcommand.
*/
// For each key (given by lower case command name) stores another `HashMap`
// that uses sub-command names as keys and returns `ArrayList` of aliases.
var private HashTable commandToAliasesMap;
var LoggerAPI.Definition testMsg;
var private User callerUser;
var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
var public const int TOPEN_BRACKET, TCLOSE_BRACKET, TCOLON_SPACE;
@ -33,7 +82,8 @@ var public const int TBOOLEAN_TRUE_FALSE, TBOOLEAN_ENABLE_DISABLE;
var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO;
var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET;
var public const int TSEPARATOR, TLIST_REGIRESTED_CMDS, TEMPTY_GROUP;
var public const int TALIASES_FOR, TEMPTY, TDOT;
var public const int TALIASES_FOR, TEMPTY, TDOT, TNO_COMMAND_BEGIN;
var public const int TNO_COMMAND_END, TEMPTY_GROUP_BEGIN, TEMPTY_GROUP_END;
protected function Constructor()
{
@ -50,36 +100,42 @@ protected function Constructor()
protected function Finalizer()
{
super.Finalizer();
_.memory.Free(commandToAliasesMap);
commandToAliasesMap = none;
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Name(P("help")).Group(P("core"))
.Summary(P("Displays detailed information about available commands."));
builder.OptionalParams()
.ParamTextList(P("commands"))
.Describe(P("Displays information about all specified commands."));
builder.Option(P("aliases"))
.Describe(P("When displaying available commands, specifying this flag"
@ "will additionally make command to display all of their available"
@ "aliases."))
.Option(P("list"))
.Describe(P("Display available commands. Optionally command groups can"
@ "be specified and then only commands from such groups will be"
@ "listed. Otherwise all commands will be displayed."))
.OptionalParams()
.ParamTextList(P("groups"));
builder.Group(P("core"));
builder.Summary(P("Displays detailed information about available commands."));
builder.OptionalParams();
builder.ParamTextList(P("commands"));
builder.Option(P("aliases"));
builder.Describe(P("When displaying available commands, specifying this flag will additionally"
@ "make command to display all of their available aliases."));
builder.Option(P("list"));
builder.Describe(P("Display available commands. Optionally command groups can be specified and"
@ "then only commands from such groups will be listed. Otherwise all commands will"
@ "be displayed."));
builder.OptionalParams();
builder.ParamTextList(P("groups"));
}
protected function Executed(Command.CallData callData, EPlayer callerPlayer)
{
protected function Executed(
Command.CallData callData,
EPlayer callerPlayer,
CommandPermissions permissions
) {
local bool printedSomething;
local HashTable parameters, options;
local ArrayList commandsToDisplay, commandGroupsToDisplay;
parameters = callData.parameters;
options = callData.options;
callerUser = callerPlayer.GetIdentity();
parameters = callData.parameters;
options = callData.options;
// Print command list if "--list" option was specified
if (options.HasKey(P("list")))
{
@ -88,6 +144,7 @@ protected function Executed(Command.CallData callData, EPlayer callerPlayer)
commandGroupsToDisplay,
options.HasKey(P("aliases")));
_.memory.Free(commandGroupsToDisplay);
printedSomething = true;
}
// Help pages.
// Only need to print them if:
@ -97,9 +154,11 @@ protected function Executed(Command.CallData callData, EPlayer callerPlayer)
if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
{
commandsToDisplay = parameters.GetArrayList(P("commands"));
DisplayCommandHelpPages(commandsToDisplay);
DisplayCommandHelpPages(commandsToDisplay, printedSomething);
_.memory.Free(commandsToDisplay);
}
_.memory.Free(callerUser);
callerUser = none;
}
// If instance of the `Aliases_Feature` is passed as an argument (allowing this
@ -132,6 +191,8 @@ private final function FillCommandToAliasesMap(Feature enabledFeature)
InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]);
commandName.FreeSelf();
subcommandName.FreeSelf();
commandName = none;
subcommandName = none;
}
// Clean up
_.memory.FreeMany(availableAliases);
@ -198,17 +259,11 @@ private final function DisplayCommandLists(
ArrayList commandGroupsToDisplay,
bool displayAliases)
{
local int i;
local array<Text> commandNames, groupsNames;
local Commands_Feature commandsFeature;
local int i;
local array<Text> commandNames, groupsNames;
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none) {
return;
}
if (commandGroupsToDisplay == none) {
groupsNames = commandsFeature.GetGroupsNames();
groupsNames = _.commands.GetGroupsNames();
}
else
{
@ -222,7 +277,7 @@ private final function DisplayCommandLists(
if (groupsNames[i] == none) {
continue;
}
commandNames = commandsFeature.GetCommandNamesInGroup(groupsNames[i]);
commandNames = _.commands.GetCommandNamesInGroup(groupsNames[i]);
if (commandNames.length > 0)
{
callerConsole.UseColorOnce(_.color.TextSubHeader);
@ -233,41 +288,45 @@ private final function DisplayCommandLists(
callerConsole.WriteLine(groupsNames[i]);
}
PrintCommandsNamesArray(
commandsFeature,
commandNames,
displayAliases);
_.memory.FreeMany(commandNames);
} else {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TEMPTY_GROUP_BEGIN));
callerConsole.Write(groupsNames[i]);
callerConsole.WriteLine(T(TEMPTY_GROUP_END));
callerConsole.ResetColor();
}
}
_.memory.FreeMany(groupsNames);
commandsFeature.FreeSelf();
}
private final function PrintCommandsNamesArray(
Commands_Feature commandsFeature,
array<Text> commandsNamesArray,
bool displayAliases)
{
local int i;
local Command nextCommand;
local Command.Data nextData;
array<Text> commandsNamesArray,
bool displayAliases
) {
local int i;
local Command.Data nextData;
local CommandAPI.CommandConfigInfo nextCommandPair;
for (i = 0; i < commandsNamesArray.length; i += 1)
{
nextCommand = commandsFeature.GetCommand(commandsNamesArray[i]);
if (nextCommand == none) {
continue;
}
nextData = nextCommand.BorrowData();
callerConsole
.UseColorOnce(_.color.textEmphasis)
.Write(nextData.name)
.Write(T(TCOLON_SPACE))
.WriteLine(nextData.summary);
if (displayAliases) {
PrintCommandAliases(nextData.name);
nextCommandPair = _.commands.ResolveCommandForUser(
commandsNamesArray[i],
callerUser);
if (nextCommandPair.instance != none && !nextCommandPair.usageForbidden) {
nextData = nextCommandPair.instance.BorrowData();
callerConsole
.UseColorOnce(_.color.textEmphasis)
.Write(commandsNamesArray[i])
.Write(T(TCOLON_SPACE))
.WriteLine(nextData.summary);
if (displayAliases) {
PrintCommandAliases(commandsNamesArray[i]);
}
}
_.memory.Free(nextCommand);
_.memory.Free(nextCommandPair.instance);
}
}
@ -348,46 +407,47 @@ private final function PrintAliasesArray(
callerConsole.WriteBlock();
}
private final function DisplayCommandHelpPages(ArrayList commandList)
{
local int i;
local bool printedSomething;
local Text nextUserProvidedName;
local MutableText referredSubcommand;
local Command nextCommand;
private final function DisplayCommandHelpPages(ArrayList commandList, bool printedSomething) {
local int i;
local Text nextUserProvidedName;
local MutableText referredSubcommand;
local CommandAPI.CommandConfigInfo nextPair;
// If arguments were empty - at least display our own help page
if (commandList == none)
{
PrintHelpPageFor(BorrowData().name, none, BorrowData());
if (commandList == none) {
nextPair.instance = self;
PrintHelpPageFor(usedName, none, nextPair);
return;
}
// Otherwise - print help for specified commands
for (i = 0; i < commandList.GetLength(); i += 1)
{
for (i = 0; i < commandList.GetLength(); i += 1) {
nextUserProvidedName = commandList.GetText(i);
nextCommand = GetCommandFromUserProvidedName(
nextPair = GetCommandFromUserProvidedName(
nextUserProvidedName,
referredSubcommand);
if (nextCommand != none)
{
/*out*/ referredSubcommand);
if (nextPair.instance != none && !nextPair.usageForbidden) {
if (printedSomething) {
callerConsole.WriteLine(T(TSEPARATOR));
}
PrintHelpPageFor(
nextUserProvidedName,
referredSubcommand,
nextCommand.BorrowData());
nextPair);
printedSomething = true;
} else if (nextPair.instance != none) {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TNO_COMMAND_BEGIN));
callerConsole.Write(nextUserProvidedName);
callerConsole.WriteLine(T(TNO_COMMAND_END));
callerConsole.ResetColor();
}
_.memory.Free(nextCommand);
_.memory.Free(nextPair.instance);
_.memory.Free(nextUserProvidedName);
_.memory.Free(referredSubcommand);
// `referredSubcommand` is passed as an `out` parameter on
// `referredSubcommand` is passed as an `out` parameter on
// every iteration, so we need to prevent the possibility of its value
// being used.
// NOTE: `nextCommand` and `nextUserProvidedName` are just
// rewritten.
// NOTE: `nextCommand` and `nextUserProvidedName` are just rewritten.
referredSubcommand = none;
}
}
@ -397,56 +457,49 @@ private final function DisplayCommandHelpPages(ArrayList commandList)
// is passed) and is used to return name of the subcommand for returned
// `Command` that is specified by `nextUserProvidedName` (only relevant for
// aliases that refer to a particular subcommand).
private final function Command GetCommandFromUserProvidedName(
BaseText nextUserProvidedName,
private final function CommandAPI.CommandConfigInfo GetCommandFromUserProvidedName(
BaseText nextUserProvidedName,
out MutableText referredSubcommand)
{
local Command result;
local Text commandAliasValue;
local Commands_Feature commandsFeature;
local MutableText parsedCommandName;
local CommandAPI.CommandConfigInfo result;
local Text commandAliasValue;
local MutableText parsedCommandName;
// Clear `out` parameter no matter what
if (referredSubcommand != none)
{
if (referredSubcommand != none) {
referredSubcommand.FreeSelf();
referredSubcommand = none;
}
// Try accessing (check availability of) `Commands_Feature`
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none) {
return none;
}
// Try getting command using `nextUserProvidedName` as a literal name
result = commandsFeature.GetCommand(nextUserProvidedName);
if (result != none)
{
commandsFeature.FreeSelf();
result = _.commands.ResolveCommandForUser(nextUserProvidedName, callerUser);
if (result.instance != none) {
return result;
}
// On failure - try resolving it as an alias
commandAliasValue = _.alias.ResolveCommand(nextUserProvidedName);
ParseCommandNames(commandAliasValue, parsedCommandName, referredSubcommand);
result = commandsFeature.GetCommand(parsedCommandName);
result = _.commands.ResolveCommandForUser(parsedCommandName, callerUser);
if ( result.instance == none
|| !result.instance.IsSubCommandAllowed(referredSubcommand, result.config)) {
_.memory.Free(result.instance);
return result;
}
// Empty subcommand name from the alias is essentially no subcommand name
if (referredSubcommand != none && referredSubcommand.IsEmpty())
{
if (referredSubcommand != none && referredSubcommand.IsEmpty()) {
referredSubcommand.FreeSelf();
referredSubcommand = none;
}
_.memory.Free(commandAliasValue);
_.memory.Free(parsedCommandName);
commandsFeature.FreeSelf();
_.memory.Free2(commandAliasValue, parsedCommandName);
return result;
}
private final function PrintHelpPageFor(
BaseText commandAlias,
BaseText referredSubcommand,
Command.Data commandData)
{
BaseText commandAlias,
BaseText referredSubcommand,
CommandAPI.CommandConfigInfo commandPair
) {
local Text commandNameLowerCase, commandNameUpperCase;
// Get capitalized command name
commandNameUpperCase = commandAlias.UpperCopy();
// Print header: name + basic info
@ -454,7 +507,7 @@ private final function PrintHelpPageFor(
.Write(commandNameUpperCase)
.UseColor(_.color.textDefault);
commandNameUpperCase.FreeSelf();
if (commandData.requiresTarget) {
if (commandPair.instance.BorrowData().requiresTarget) {
callerConsole.WriteLine(T(TCMD_WITH_TARGET));
}
else {
@ -462,31 +515,27 @@ private final function PrintHelpPageFor(
}
// Print commands and options
commandNameLowerCase = commandAlias.LowerCopy();
PrintCommands(commandData, commandNameLowerCase, referredSubcommand);
PrintCommands(commandPair, commandNameLowerCase, referredSubcommand);
commandNameLowerCase.FreeSelf();
PrintOptions(commandData);
PrintOptions(commandPair.instance.BorrowData());
// Clean up
callerConsole.ResetColor().Flush();
}
private final function PrintCommands(
Command.Data data,
BaseText commandName,
BaseText referredSubcommand)
{
local int i;
local array<SubCommand> subCommands;
CommandAPI.CommandConfigInfo commandPair,
BaseText commandName,
BaseText referredSubcommand
) {
local int i;
local array<Command.SubCommand> subCommands;
subCommands = data.subCommands;
for (i = 0; i < subCommands.length; i += 1)
{
if ( referredSubcommand == none
|| referredSubcommand.Compare(subCommands[i].name))
{
PrintSubCommand(
subCommands[i],
commandName,
referredSubcommand != none);
subCommands = commandPair.instance.BorrowData().subCommands;
for (i = 0; i < subCommands.length; i += 1) {
if (referredSubcommand == none || referredSubcommand.Compare(subCommands[i].name)) {
if (commandPair.instance.IsSubCommandAllowed(subCommands[i].name, commandPair.config)) {
PrintSubCommand(subCommands[i], commandName, referredSubcommand != none);
}
}
}
}
@ -670,4 +719,13 @@ defaultproperties
stringConstants(21) = ""
TDOT = 22
stringConstants(22) = "."
TNO_COMMAND_BEGIN = 23
stringConstants(23) = "Command `"
TNO_COMMAND_END = 24
stringConstants(24) = "` not found!"
TEMPTY_GROUP_BEGIN = 25
stringConstants(25) = "No commands in group \""
TEMPTY_GROUP_END = 26
stringConstants(26) = "\"!"
preferredName = "help"
}

69
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc

@ -0,0 +1,69 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandNotify extends Command
dependsOn(ChatApi);
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("core"));
builder.Summary(P("Notifies players with provided message."));
builder.ParamText(P("message"));
builder.OptionalParams();
builder.ParamNumber(P("duration"));
builder.Describe(P("Notify to players message with distinct header and body."));
builder.RequireTarget();
builder.Option(P("title"));
builder.Describe(P("Specify the optional title of the notification."));
builder.ParamText(P("title"));
builder.Option(P("channel"));
builder.Describe(P("Specify the optional channel. A channel is a grouping mechanism used to"
@ "control the display of related notifications. Only last message from the same channel is"
@ "stored in queue."));
builder.ParamText(P("channel_name"));
}
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local Text title, message, plainTitle, plainMessage;
plainMessage = arguments.parameters.GetText(P("message"));
if (arguments.options.HasKey(P("title"))) {
plainTitle = arguments.options.GetTextBy(P("/title/title"));
}
title = _.text.FromFormatted(plainTitle);
message = _.text.FromFormatted(plainMessage);
target.Notify(
title,
message,
arguments.parameters.GetFloat(P("duration")),
arguments.options.GetTextBy(P("/channel/channel_name")));
_.memory.Free4(title, message, plainTitle, plainMessage);
}
defaultproperties {
preferredName = "notify"
}

197
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandSideEffects.uc

@ -0,0 +1,197 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandSideEffects extends Command;
// Maps `UserID` to `ArrayList` with side effects listed for that player last time
var private HashTable displayedLists;
protected function Constructor() {
super.Constructor();
displayedLists = _.collections.EmptyHashTable();
}
protected function Finalizer() {
super.Finalizer();
_.memory.Free(displayedLists);
displayedLists = none;
}
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("debug"));
builder.Summary(P("Displays information about current side effects."));
builder.Describe(P("This command allows to display current side effects, optionally filtering"
@ "them by specified package names."));
builder.OptionalParams();
builder.ParamTextList(P("package_names"));
builder.SubCommand(P("show"));
builder.Describe(P("This sub-command is only usable after side effects have been shown"
@ "at least once. It takes an index from the last displayed list and displays a verbose"
@ "information about it."));
builder.ParamInteger(P("side_effect_number"));
builder.Option(P("verbose"));
builder.Describe(P("Display verbose information about each side effect."));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local UserID playerID;
local array<SideEffect> relevantSideEffects;
local ArrayList packagesList, storedSideEffectsList;
playerID = instigator.GetUserID();
if (arguments.subCommandName.IsEmpty()) {
relevantSideEffects = _.sideEffects.GetAll();
packagesList = arguments.parameters.GetArrayList(P("package_names"));
FilterSideEffects(/*out*/ relevantSideEffects, packagesList);
_.memory.Free(packagesList);
DisplaySideEffects(relevantSideEffects, arguments.options.HasKey(P("verbose")));
// Store new side effect list
storedSideEffectsList = _.collections.NewArrayList(relevantSideEffects);
displayedLists.SetItem(playerID, storedSideEffectsList);
_.memory.FreeMany(relevantSideEffects);
_.memory.Free(storedSideEffectsList);
} else {
ShowInfoFor(playerID, arguments.parameters.GetInt(P("side_effect_number")));
}
_.memory.Free(playerID);
}
private function FilterSideEffects(out array<SideEffect> sideEffects, ArrayList allowedPackages) {
local int i, j;
local int packagesLength;
local bool matchedPackage;
local Text nextSideEffectPackage, nextAllowedPackage;
if (allowedPackages == none) return;
if (allowedPackages.GetLength() <= 0) return;
packagesLength = allowedPackages.GetLength();
while (i < sideEffects.length) {
nextSideEffectPackage = sideEffects[i].GetPackage();
matchedPackage = false;
for (j = 0; j < packagesLength; j += 1) {
nextAllowedPackage = allowedPackages.GetText(j);
if (nextAllowedPackage.Compare(nextSideEffectPackage, SCASE_INSENSITIVE)) {
matchedPackage = true;
_.memory.Free(nextAllowedPackage);
break;
}
_.memory.Free(nextAllowedPackage);
}
if (!matchedPackage) {
sideEffects.Remove(i, 1);
} else {
i += 1;
}
_.memory.Free(nextSideEffectPackage);
}
}
private function DisplaySideEffects(array<SideEffect> toDisplay, bool verbose) {
local int i;
local MutableText nextPrefix;
if (toDisplay.length <= 0) {
callerConsole.Write(F("List of side effects is {$TextNeutral empty}."));
}
for (i = 0; i < toDisplay.length; i += 1) {
nextPrefix = _.text.FromIntM(i + 1);
nextPrefix.Append(P("."));
DisplaySideEffect(toDisplay[i], nextPrefix, verbose);
_.memory.Free(nextPrefix);
}
}
private function DisplaySideEffect(SideEffect toDisplay, BaseText prefix, bool verbose) {
local Text effectName, effectDescription, effectPackage, effectSource, effectStatus;
if (toDisplay == none) {
return;
}
if (prefix != none) {
callerConsole.Write(prefix);
callerConsole.Write(P(" "));
}
effectName = toDisplay.GetName();
effectPackage = toDisplay.GetPackage();
effectSource = toDisplay.GetSource();
effectStatus = toDisplay.GetStatus();
callerConsole.UseColor(_.color.TextEmphasis);
callerConsole.Write(P("["));
callerConsole.Write(effectPackage);
callerConsole.Write(P(" \\ "));
callerConsole.Write(effectSource);
callerConsole.Write(P("] "));
callerConsole.ResetColor();
callerConsole.Write(effectName);
callerConsole.Write(P(" {"));
callerConsole.Write(effectStatus);
callerConsole.WriteLine(P("}"));
if (verbose) {
effectDescription = toDisplay.GetDescription();
callerConsole.WriteBlock(effectDescription);
}
_.memory.Free5(effectName, effectDescription, effectPackage, effectSource, effectStatus);
}
private function ShowInfoFor(UserID playerID, int sideEffectIndex) {
local SideEffect toDisplay;
local ArrayList sideEffectList;
if (playerID == none) {
return;
}
if (sideEffectIndex <= 0) {
callerConsole.WriteLine(F("Specified side effect index {$TextNegative isn't positive}!"));
return;
}
sideEffectList = displayedLists.GetArrayList(playerID);
if (sideEffectList == none) {
callerConsole.WriteLine(F("{$TextNegative Cannot display} side effect by index without"
@ "first listing them. Call {$TextEmphasis sideeffects} command without"
@ "{$TextEmphasis show} subcommand first."));
return;
}
if (sideEffectIndex > sideEffectList.GetLength()) {
callerConsole.WriteLine(F("Specified side effect index is {$TextNegative out of bounds}."));
_.memory.Free(sideEffectList);
return;
}
// Above we checked that `sideEffectIndex` lies within `[0; sideEffectList.GetLength()]` segment
// This means that `sideEffectIndex - 1` points at non-`none` value
toDisplay = SideEffect(sideEffectList.GetItem(sideEffectIndex - 1));
if (!_.sideEffects.IsRegistered(toDisplay)) {
callerConsole.UseColorOnce(_.color.TextWarning);
callerConsole.WriteLine(P("Selected side effect is no longer active!"));
}
DisplaySideEffect(toDisplay, none, true);
_.memory.Free2(toDisplay, sideEffectList);
}
defaultproperties {
preferredName = "sideeffects"
}

220
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc

@ -0,0 +1,220 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandVote extends Command
dependson(CommandAPI)
dependson(VotingModel);
var private CommandDataBuilder dataBuilder;
protected function Constructor() {
ResetVotingInfo();
_.commands.OnVotingAdded(self).connect = AddVotingInfo;
_.commands.OnVotingRemoved(self).connect = HandleRemovedVoting;
_.chat.OnVoiceMessage(self).connect = VoteWithVoice;
}
protected function Finalizer() {
super.Finalizer();
_.memory.Free(dataBuilder);
dataBuilder = none;
_.commands.OnVotingAdded(self).Disconnect();
_.commands.OnVotingRemoved(self).Disconnect();
_.chat.OnVoiceMessage(self).Disconnect();
}
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("core"));
builder.Summary(P("Allows players to initiate any available voting."
@ "Voting options themselves are specified as sub-commands."));
builder.Describe(P("Default command simply displaces information about current vote."));
dataBuilder.SubCommand(P("yes"));
builder.Describe(P("Vote `yes` on the current vote."));
dataBuilder.SubCommand(P("no"));
builder.Describe(P("Vote `no` on the current vote."));
builder.Option(P("force"));
builder.Describe(P("Tries to force voting to end immediately with the desired result."));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local bool forcingVoting;
local VotingModel.ForceEndingType forceType;
local Voting currentVoting;
forcingVoting = arguments.options.HasKey(P("force"));
currentVoting = _.commands.GetCurrentVoting();
if (arguments.subCommandName.IsEmpty()) {
DisplayInfoAboutVoting(instigator, currentVoting);
} else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) {
CastVote(currentVoting, instigator, true);
forceType = FET_Success;
} else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) {
CastVote(currentVoting, instigator, false);
forceType = FET_Failure;
} else if (StartVoting(arguments, currentVoting, instigator)) {
_.memory.Free(currentVoting);
currentVoting = _.commands.GetCurrentVoting();
forceType = FET_Success;
} else {
forcingVoting = false;
}
if (currentVoting != none && !currentVoting.HasEnded() && forcingVoting) {
if (currentVoting.ForceEnding(instigator, forceType) == FEO_Forbidden) {
callerConsole
.WriteLine(F("You {$TextNegative aren't allowed} to forcibly end current voting"));
}
}
_.memory.Free(currentVoting);
}
private final function VoteWithVoice(EPlayer sender, ChatApi.BuiltInVoiceMessage message) {
local Voting currentVoting;
currentVoting = _.commands.GetCurrentVoting();
if (message == BIVM_AckYes) {
CastVote(currentVoting, sender, true);
}
if (message == BIVM_AckNo) {
CastVote(currentVoting, sender, false);
}
_.memory.Free(currentVoting);
}
/// Adds sub-command information about given voting with a given name.
public final function AddVotingInfo(class<Voting> processClass, Text processName) {
if (processName == none) return;
if (processClass == none) return;
if (dataBuilder == none) return;
dataBuilder.SubCommand(processName);
processClass.static.AddInfo(dataBuilder);
commandData = dataBuilder.BorrowData();
}
public final function HandleRemovedVoting(class<Voting> votingClass) {
local int i;
local array<Text> votingsNames;
ResetVotingInfo();
// Rebuild the whole voting data
votingsNames = _.commands.GetAllVotingsNames();
for (i = 0; i < votingsNames.length; i += 1) {
AddVotingInfo(_.commands.GetVotingClass(votingsNames[i]), votingsNames[i]);
}
_.memory.FreeMany(votingsNames);
}
/// Clears all sub-command information added from [`Voting`]s.
public final function ResetVotingInfo() {
_.memory.Free(dataBuilder);
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
BuildData(dataBuilder);
commandData = dataBuilder.BorrowData();
}
private final function DisplayInfoAboutVoting(EPlayer instigator, Voting currentVoting) {
if (currentVoting == none) {
callerConsole.WriteLine(P("No voting is active right now."));
} else {
currentVoting.PrintVotingInfoFor(instigator);
}
}
private final function CastVote(Voting currentVoting, EPlayer voter, bool voteForSuccess) {
if (currentVoting != none) {
currentVoting.CastVote(voter, voteForSuccess);
} else {
callerConsole.UseColor(_.color.TextWarning).WriteLine(P("No voting is active right now."));
}
}
// Assumes all arguments aren't `none`.
private final function bool StartVoting(
CallData arguments,
Voting currentVoting,
EPlayer instigator
) {
local Voting newVoting;
local User callerUser;
local CommandAPI.VotingConfigInfo pair;
local CommandAPI.StartVotingResult result;
callerUser = instigator.GetIdentity();
pair = _.commands.ResolveVotingForUser(arguments.subCommandName, callerUser);
_.memory.Free(callerUser);
if (pair.votingClass == none) {
callerConsole
.UseColor(_.color.TextFailure)
.Write(P("Unknown voting option \""))
.Write(arguments.subCommandName)
.WriteLine(P("\""));
return false;
}
if (pair.usageForbidden) {
callerConsole
.UseColor(_.color.TextFailure)
.Write(P("You aren't allowed to start \""))
.Write(arguments.subCommandName)
.WriteLine(P("\" voting"));
return false;
}
result = _.commands.StartVoting(pair, arguments.parameters);
Log("Result:" @ result);
// Handle errors.
// `SVR_UnknownVoting` is impossible, since we've already checked that
// `pair.votingClass != none`)
if (result == SVR_AlreadyInProgress) {
callerConsole
.UseColor(_.color.TextWarning)
.WriteLine(P("Another voting is already in progress!"));
return false;
}
if (result == SVR_NoVoters) {
callerConsole
.UseColor(_.color.TextWarning)
.WriteLine(P("There are no players eligible for that voting."));
return false;
}
// Cast a vote from instigator
newVoting = _.commands.GetCurrentVoting();
if (newVoting != none) {
newVoting.CastVote(instigator, true);
} else {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("Voting should be available, but it isn't."
@ "This is unexpected, something broke terribly."));
_.memory.Free(newVoting);
return false;
}
_.memory.Free(newVoting);
return true;
}
defaultproperties {
preferredName = "vote"
}

805
sources/BaseAPI/API/Commands/Command.uc

@ -0,0 +1,805 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Command extends AcediaObject
dependson(BaseText);
//! This class is meant to represent a command type.
//!
//! Command class provides an automated way to add a command to a server through
//! AcediaCore's features. It takes care of:
//!
//! 1. Verifying that player has passed correct (expected parameters);
//! 2. Parsing these parameters into usable values (both standard, built-in
//! types like `bool`, `int`, `float`, etc. and more advanced types such
//! as players lists and JSON values);
//! 3. Allowing you to easily specify a set of players you are targeting by
//! supporting several ways to refer to them, such as *by name*, *by id*
//! and *by selector* (@ and @self refer to caller player, @all refers
//! to all players).
//! 4. It can be registered inside AcediaCore's commands feature and be
//! automatically called through the unified system that supports *chat*
//! and *mutate* inputs (as well as allowing you to hook in any other
//! input source);
//! 5. Will also automatically provide a help page through built-in "help"
//! command;
//! 6. Subcommand support - when one command can have several distinct
//! functions, depending on how its called (e.g. "inventory add" vs
//! "inventory remove"). These subcommands have a special treatment in
//! help pages, which makes them more preferable, compared to simply
//! matching first `Text` argument;
//! 7. Add support for "options" - additional flags that can modify commands
//! behavior and behave like usual command options "--force"/"-f".
//! Their short versions can even be combined:
//! "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af".
//! And they can have their own parameters: "give@all --list sharp".
//!
//! # Implementation
//!
//! The idea of `Command`'s implementation is simple: command is basically the
//! `Command.Data` struct that is filled via `CommandDataBuilder`.
//! Whenever command is called it uses `CommandParser` to parse user's input
//! based on its `Command.Data` and either report error (in case of failure) or
//! pass make `Executed()`/`ExecutedFor()` calls (in case of success).
//!
//! When command is called is decided by `Commands_Feature` that tracks possible
//! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for
//! adding custom command inputs). That feature basically parses first part of
//! the command: its name (not the subcommand's names) and target players
//! (using `PlayersParser`, but only if command is targeted).
//!
//! Majority of the command-related code either serves to build `Command.Data`
//! or to parse command input by using it (`CommandParser`).
/// Possible errors that can arise when parsing command parameters from user
/// input
enum ErrorType {
/// No error
CET_None,
/// Bad parser was provided to parse user input (this should not be possible)
CET_BadParser,
/// Sub-command name was not specified or was incorrect
/// (this should not be possible)
CET_NoSubCommands,
/// Specified sub-command does not exist
/// (only relevant when it is enforced for parser, e.g. by an alias)
CET_BadSubCommand,
/// Required param for command / option was not specified
CET_NoRequiredParam,
CET_NoRequiredParamForOption,
/// Unknown option key was specified
CET_UnknownOption,
/// Unknown short option key was specified
CET_UnknownShortOption,
/// Same option appeared twice in one command call
CET_RepeatedOption,
/// Part of user's input could not be interpreted as a part of
/// command's call
CET_UnusedCommandParameters,
/// In one short option specification (e.g. '-lah') several options require
/// parameters: this introduces ambiguity and is not allowed
CET_MultipleOptionsWithParams,
/// Targets are specified incorrectly (for targeted commands only)
CET_IncorrectTargetList,
// No targets are specified (for targeted commands only)
CET_EmptyTargetList
};
/// Structure that contains all the information about how `Command` was called.
struct CallData {
/// Targeted players (if applicable)
var public array<EPlayer> targetPlayers;
/// Specified sub-command and parameters/options
var public Text subCommandName;
/// Provided parameters and specified options
var public HashTable parameters;
var public HashTable options;
/// Errors that occurred during command call processing are described by
/// error type.
var public ErrorType parsingError;
/// Optional error textual name of the object (parameter, option, etc.)
/// that caused it.
var public Text errorCause;
};
/// Possible types of parameters.
enum ParameterType {
/// Parses into `BoolBox`
CPT_Boolean,
/// Parses into `IntBox`
CPT_Integer,
/// Parses into `FloatBox`
CPT_Number,
/// Parses into `Text`
CPT_Text,
/// Special parameter that consumes the rest of the input into `Text`
CPT_Remainder,
/// Parses into `HashTable`
CPT_Object,
/// Parses into `ArrayList`
CPT_Array,
/// Parses into any JSON value
CPT_JSON,
/// Parses into an array of specified players
CPT_Players
};
/// Possible forms a boolean variable can be used as.
/// Boolean parameter can define it's preferred format, which will be used for
/// help page generation.
enum PreferredBooleanFormat {
PBF_TrueFalse,
PBF_EnableDisable,
PBF_OnOff,
PBF_YesNo
};
// Defines a singular command parameter
struct Parameter {
/// Display name (for the needs of help page displaying)
var Text displayName;
/// Type of value this parameter would store
var ParameterType type;
/// Does it take only a singular value or can it contain several of them,
/// written in a list
var bool allowsList;
/// Variable name that will be used as a key to store parameter's value
var Text variableName;
/// (For `CPT_Boolean` type variables only) - preferred boolean format,
/// used in help pages
var PreferredBooleanFormat booleanFormat;
/// `CPT_Text` can be attempted to be auto-resolved as an alias from some
/// source during parsing.
/// For command to attempt that, this field must be not-`none` and contain
/// the name of the alias source (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name).
///
/// Only relevant when given value is prefixed with "$" character.
var Text aliasSourceName;
};
/// Defines a sub-command of a this command
/// (specified as "<command> <sub_command>").
///
/// Using sub-command is not optional, but if none defined
/// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
/// one is automatically created / used.
struct SubCommand {
/// Name of the sub command. Cannot be `none`.
var Text name;
/// Human-readable description of the subcommand. Can be `none`.
var Text description;
/// List of required parameters of this [`Command`].
var array<Parameter> required;
/// List of optional parameters of this [`Command`].
var array<Parameter> optional;
};
/// Defines command's option (options are specified by "--long" or "-l").
/// Options are independent from sub-commands.
struct Option {
/// [`Option`]'s short name, i.e. a single letter "f" that can be specified
/// in, e.g. "-laf" type option listings
var BaseText.Character shortName;
/// [`Option`]'s full name, e.g. "--force".
var Text longName;
/// Human-readable description of the option. Can be `none`.
var Text description;
/// List of required parameters of this [`Command::Option`].
var array<Parameter> required;
/// List of required parameters of this [`Command::Option`].
var array<Parameter> optional;
};
/// Structure that defines what sub-commands and options command has
/// (and what parameters they take)
struct Data {
/// Command group this command belongs to
var protected Text group;
/// Short summary of what command does (recommended to
/// keep it to 80 characters)
var protected Text summary;
/// Available subcommands.
var protected array<SubCommand> subCommands;
/// Available options, common to all subcommands.
var protected array<Option> options;
/// `true` iff related [`Command`] targets players.
var protected bool requiresTarget;
};
var protected Data commandData;
/// Setting variable that defines a name that will be chosen for command by
/// default.
var protected const string preferredName;
/// Name that was used to register this command.
var protected Text usedName;
/// Settings variable that defines a class to be used for this [`Command`]'s
/// permissions config
var protected const class<CommandPermissions> permissionsConfigClass;
// We do not really ever need to create more than one instance of each class
// of `Command`, so we will simply store and reuse one created instance.
var private Command mainInstance;
/// When command is being executed we create several instances of
/// `ConsoleWriter` that can be used for command output.
/// They will also be automatically deallocated once command is executed.
///
/// DO NOT modify them or deallocate any of them manually.
///
/// This should make output more convenient and standardized.
///
/// 1. `publicConsole` - sends messages to all present players;
/// 2. `callerConsole` - sends messages to the player that called the command;
/// 3. `targetConsole` - sends messages to the player that is currently being
/// targeted (different each call of `ExecutedFor()` and `none` during
/// `Executed()` call);
/// 4. `othersConsole` - sends messaged to every player that is neither
/// "caller" or "target".
var protected ConsoleWriter publicConsole, othersConsole;
var protected ConsoleWriter callerConsole, targetConsole;
protected function Constructor() {
local CommandDataBuilder dataBuilder;
if (permissionsConfigClass != none) {
permissionsConfigClass.static.Initialize();
}
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
// Let user fill-in the rest
BuildData(dataBuilder);
commandData = dataBuilder.BorrowData();
dataBuilder.FreeSelf();
dataBuilder = none;
}
protected function Finalizer() {
local int i;
local array<SubCommand> subCommands;
local array<Option> options;
DeallocateConsoles();
_.memory.Free(usedName);
_.memory.Free(commandData.summary);
usedName = none;
commandData.summary = none;
subCommands = commandData.subCommands;
for (i = 0; i < options.length; i += 1) {
_.memory.Free(subCommands[i].name);
_.memory.Free(subCommands[i].description);
CleanParameters(subCommands[i].required);
CleanParameters(subCommands[i].optional);
subCommands[i].required.length = 0;
subCommands[i].optional.length = 0;
}
commandData.subCommands.length = 0;
options = commandData.options;
for (i = 0; i < options.length; i += 1) {
_.memory.Free(options[i].longName);
_.memory.Free(options[i].description);
CleanParameters(options[i].required);
CleanParameters(options[i].optional);
options[i].required.length = 0;
options[i].optional.length = 0;
}
commandData.options.length = 0;
}
/// Initializes command, providing it with a specific name.
///
/// Argument cannot be `none`, otherwise initialization fails.
/// [`Command`] can only be successfully initialized once.
public final function bool Initialize(BaseText commandName) {
if (commandName == none) return false;
if (usedName != none) return false;
usedName = commandName.LowerCopy();
return true;
}
/// Overload this method to use `builder` to define parameters and options for
/// your command.
protected function BuildData(CommandDataBuilder builder){}
/// Overload this method to perform required actions when your command is
/// called.
///
/// [`arguments`] is a `struct` filled with parameters that your command has
/// been called with. Guaranteed to not be in error state.
/// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call.
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions) {}
/// Overload this method to perform required actions when your command is called
/// with a given player as a target.
///
/// If several players have been specified - this method will be called once
/// for each.
///
/// If your command does not require a target - this method will not be called.
///
/// [`target`] is a player that this command must perform an action on.
/// [`arguments`] is a `struct` filled with parameters that your command has
/// been called with. Guaranteed to not be in error state.
/// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call.
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions) {}
/// Returns an instance of command (of particular class) that is stored
/// "as a singleton" in command's class itself. Do not deallocate it.
public final static function Command GetInstance() {
if (default.mainInstance == none) {
default.mainInstance = Command(__().memory.Allocate(default.class));
}
return default.mainInstance;
}
/// Forces command to process (parse) player's input, producing a structure with
/// parsed data in Acedia's format instead.
///
/// Use `Execute()` for actually performing command's actions.
///
/// [`subCommandName`] can be optionally specified to use as sub-command.
/// If this argument's value is `none` - sub-command name will be parsed from
/// the `parser`'s data.
///
/// Returns `CallData` structure that contains all the information about
/// parameters specified in `parser`'s contents.
/// Returned structure contains objects that must be deallocated, which can
/// easily be done by the auxiliary `DeallocateCallData()` method.
public final function CallData ParseInputWith(
Parser parser,
EPlayer callerPlayer,
optional BaseText subCommandName
) {
local array<EPlayer> targetPlayers;
local CommandParser commandParser;
local CallData callData;
if (parser == none || !parser.Ok()) {
callData.parsingError = CET_BadParser;
return callData;
}
// Parse targets and handle errors that can arise here
if (commandData.requiresTarget) {
targetPlayers = ParseTargets(parser, callerPlayer);
if (!parser.Ok()) {
callData.parsingError = CET_IncorrectTargetList;
return callData;
}
if (targetPlayers.length <= 0) {
callData.parsingError = CET_EmptyTargetList;
return callData;
}
}
// Parse parameters themselves
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
callData = commandParser.ParseWith(
parser,
commandData,
callerPlayer,
subCommandName);
callData.targetPlayers = targetPlayers;
commandParser.FreeSelf();
return callData;
}
/// Executes caller `Command` with data provided by `callData` if it is in
/// a correct state and reports error to `callerPlayer` if `callData` is
/// invalid.
///
/// Returns `true` if command was successfully executed and `false` otherwise.
/// Execution is considered successful if `Execute()` call was made, regardless
/// of whether `Command` can actually perform required action.
/// For example, giving a weapon to a player can fail because he does not have
/// enough space in his inventory, but it will still be considered a successful
/// execution as far as return value is concerned.
///
/// [`permissions`] argument is supposed to specify permissions with which this
/// command runs.
/// If [`permissionsConfigClass`] is `none`, it must always be `none`.
/// If [`permissionsConfigClass`] is not `none`, then [`permissions`] argument
/// being `none` should mean running with minimal priviledges.
public final function bool Execute(
CallData callData,
EPlayer callerPlayer,
CommandPermissions permissions
) {
local int i;
local array<EPlayer> targetPlayers;
if (callerPlayer == none) return false;
if (!callerPlayer.IsExistent()) return false;
// Report or execute
if (callData.parsingError != CET_None) {
ReportError(callData, callerPlayer);
return false;
}
targetPlayers = callData.targetPlayers;
publicConsole = _.console.ForAll();
callerConsole = _.console.For(callerPlayer);
callerConsole
.Write(P("Executing command `"))
.Write(usedName)
.Say(P("`"));
// `othersConsole` should also exist in time for `Executed()` call
othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
Executed(callData, callerPlayer, permissions);
_.memory.Free(othersConsole);
if (commandData.requiresTarget) {
for (i = 0; i < targetPlayers.length; i += 1) {
targetConsole = _.console.For(targetPlayers[i]);
othersConsole = _.console
.ForAll()
.ButPlayer(callerPlayer)
.ButPlayer(targetPlayers[i]);
ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions);
_.memory.Free(othersConsole);
_.memory.Free(targetConsole);
}
}
othersConsole = none;
targetConsole = none;
DeallocateConsoles();
return true;
}
/// Auxiliary method that cleans up all data and deallocates all objects inside provided structure.
public final static function DeallocateCallData(/* take */ CallData callData) {
__().memory.Free(callData.subCommandName);
__().memory.Free(callData.parameters);
__().memory.Free(callData.options);
__().memory.Free(callData.errorCause);
__().memory.FreeMany(callData.targetPlayers);
if (callData.targetPlayers.length > 0) {
callData.targetPlayers.length = 0;
}
}
private final function CleanParameters(array<Parameter> parameters) {
local int i;
for (i = 0; i < parameters.length; i += 1) {
_.memory.Free(parameters[i].displayName);
_.memory.Free(parameters[i].variableName);
_.memory.Free(parameters[i].aliasSourceName);
}
}
/// Returns name (in lower case) of the caller command class.
public final static function Text GetPreferredName() {
return __().text.FromString(Locs(default.preferredName));
}
/// Returns name (in lower case) of the caller command class.
public final static function string GetPreferredName_S() {
return Locs(default.preferredName);
}
/// Returns name (in lower case) of the caller command class.
public final function Text GetName() {
if (usedName == none) {
return P("").Copy();
}
return usedName.LowerCopy();
}
/// Returns name (in lower case) of the caller command class.
public final function string GetName_S() {
if (usedName == none) {
return "";
}
return _.text.IntoString(/*take*/ usedName.LowerCopy());
}
/// Returns group name (in lower case) of the caller command class.
public final function Text GetGroupName() {
if (commandData.group == none) {
return P("").Copy();
}
return commandData.group.LowerCopy();
}
/// Returns group name (in lower case) of the caller command class.
public final function string GetGroupName_S() {
if (commandData.group == none) {
return "";
}
return _.text.IntoString(/*take*/ commandData.group.LowerCopy());
}
/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig(BaseText configName) {
if (configName == none) return none;
if (default.permissionsConfigClass == none) return none;
// This creates default config if it is missing
default.permissionsConfigClass.static.NewConfig(configName);
return CommandPermissions(default.permissionsConfigClass.static
.GetConfigInstance(configName));
}
/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig_S(string configName) {
local MutableText wrapper;
local CommandPermissions result;
wrapper = __().text.FromStringM(configName);
result = LoadConfig(wrapper);
__().memory.Free(wrapper);
return result;
}
/// Returns subcommands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<Text> GetSubCommands(optional CommandPermissions permissions) {
local int i, j;
local bool addSubCommand;
local array<string> forbiddenCommands;
local array<Text> result;
forbiddenCommands = permissions.forbiddenSubCommands;
if (permissions != none) {
forbiddenCommands = permissions.forbiddenSubCommands;
}
for (i = 0; i < commandData.subCommands.length; i += 1) {
addSubCommand = true;
for (j = 0; j < forbiddenCommands.length; j += 1) {
if (commandData.subCommands[i].name.ToString() ~= forbiddenCommands[j]) {
addSubCommand = false;
break;
}
}
if (addSubCommand) {
result[result.length] = commandData.subCommands[i].name.LowerCopy();
}
}
return result;
}
/// Returns sub commands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<string> GetSubCommands_S(optional CommandPermissions permissions) {
return _.text.IntoStrings(GetSubCommands(permissions));
}
/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed as either argument, returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing subcommand is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed(
BaseText subCommand,
CommandPermissions permissions
) {
if (subCommand == none) return true;
if (permissions == none) return true;
return IsSubCommandAllowed_S(subCommand.ToString(), permissions);
}
/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed for permissions, always returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing sub command is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed_S(
string subCommand,
CommandPermissions permissions
) {
local int i;
local array<string> forbiddenCommands;
if (permissions == none) {
return true;
}
forbiddenCommands = permissions.forbiddenSubCommands;
for (i = 0; i < forbiddenCommands.length; i += 1) {
if (subCommand ~= forbiddenCommands[i]) {
return false;
}
}
return true;
}
/// Returns `Command.Data` struct that describes caller `Command`.
///
/// Returned struct contains `Text` references that are used internally by
/// the `Command` and not their copies.
///
/// Generally this is undesired approach and leaves `Command` more vulnerable to
/// modification, but copying all the data inside would not only introduce
/// a largely pointless computational overhead, but also would require some
/// cumbersome logic.
/// This might change in the future, so deallocating any objects in the returned
/// `struct` would lead to undefined behavior.
public final function Data BorrowData() {
return commandData;
}
private final function DeallocateConsoles() {
if (publicConsole != none && publicConsole.IsAllocated()) {
_.memory.Free(publicConsole);
}
if (callerConsole != none && callerConsole.IsAllocated()) {
_.memory.Free(callerConsole);
}
if (targetConsole != none && targetConsole.IsAllocated()) {
_.memory.Free(targetConsole);
}
if (othersConsole != none && othersConsole.IsAllocated()) {
_.memory.Free(othersConsole);
}
publicConsole = none;
callerConsole = none;
targetConsole = none;
othersConsole = none;
}
/// Reports given error to the `callerPlayer`, appropriately picking
/// message color
private final function ReportError(CallData callData, EPlayer callerPlayer) {
local Text errorMessage;
local ConsoleWriter console;
if (callerPlayer == none) return;
if (!callerPlayer.IsExistent()) return;
// Setup console color
console = callerPlayer.BorrowConsole();
if (callData.parsingError == CET_EmptyTargetList) {
console.UseColor(_.color.textWarning);
} else {
console.UseColor(_.color.textFailure);
}
// Send message
errorMessage = PrintErrorMessage(callData);
console.Say(errorMessage);
errorMessage.FreeSelf();
// Restore console color
console.ResetColor().Flush();
}
private final function Text PrintErrorMessage(CallData callData) {
local Text result;
local MutableText builder;
builder = _.text.Empty();
switch (callData.parsingError) {
case CET_BadParser:
builder.Append(P("Internal error occurred: invalid parser"));
break;
case CET_NoSubCommands:
builder.Append(P("Ill defined command: no subcommands"));
break;
case CET_BadSubCommand:
builder
.Append(P("Ill defined sub-command: "))
.Append(callData.errorCause);
break;
case CET_NoRequiredParam:
builder
.Append(P("Missing required parameter: "))
.Append(callData.errorCause);
break;
case CET_NoRequiredParamForOption:
builder
.Append(P("Missing required parameter for option: "))
.Append(callData.errorCause);
break;
case CET_UnknownOption:
builder
.Append(P("Invalid option specified: "))
.Append(callData.errorCause);
break;
case CET_UnknownShortOption:
builder.Append(P("Invalid short option specified"));
break;
case CET_RepeatedOption:
builder
.Append(P("Option specified several times: "))
.Append(callData.errorCause);
break;
case CET_UnusedCommandParameters:
builder.Append(P("Part of command could not be parsed: "))
.Append(callData.errorCause);
break;
case CET_MultipleOptionsWithParams:
builder
.Append(P("Multiple short options in one declarations require parameters: "))
.Append(callData.errorCause);
break;
case CET_IncorrectTargetList:
builder
.Append(P("Target players are incorrectly specified."))
.Append(callData.errorCause);
break;
case CET_EmptyTargetList:
builder
.Append(P("List of target players is empty"))
.Append(callData.errorCause);
break;
default:
}
result = builder.Copy();
builder.FreeSelf();
return result;
}
// Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state.
// If parsing failed, guaranteed to return an empty array.
private final function array<EPlayer> ParseTargets(Parser parser, EPlayer callerPlayer) {
local array<EPlayer> targetPlayers;
local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser);
if (parser.Ok()) {
targetPlayers = targetsParser.GetPlayers();
}
targetsParser.FreeSelf();
return targetPlayers;
}
defaultproperties {
preferredName = ""
permissionsConfigClass = none
}

1578
sources/BaseAPI/API/Commands/CommandAPI.uc

File diff suppressed because it is too large Load Diff

939
sources/BaseAPI/API/Commands/CommandDataBuilder.uc

@ -0,0 +1,939 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandDataBuilder extends AcediaObject
dependson(Command);
//! This is an auxiliary class for convenient creation of [`Command::Data`]
//! using a builder pattern.
//!
//! ## Implementation
//!
//! We will store all defined data in two ways:
//!
//! 1. Selected data: data about parameters for subcommand/option that is
//! currently being filled;
//! 2. Prepared data: data that was already filled as "selected data" then
//! stored in these records. Whenever we want to switch to filling another
//! subcommand/option or return already prepared data we must dump
//! "selected data" into "prepared data" first and then return the latter.
//!
//! Builder object is automatically created when new `Command` instance is
//! allocated and doesn't normally need to be allocated by hand.
// "Prepared data"
var private Text commandName, commandGroup;
var private Text commandSummary;
var private array<Command.SubCommand> subcommands;
var private array<Command.Option> options;
var private bool requiresTarget;
// Auxiliary arrays signifying that we've started adding optional parameters
// into appropriate `subcommands` and `options`.
//
// All optional parameters must follow strictly after required parameters and
// so, after user have started adding optional parameters to subcommand/option,
// we prevent them from adding required ones (to that particular
// command/option).
var private array<byte> subcommandsIsOptional;
var private array<byte> optionsIsOptional;
// "Selected data"
// `false` means we have selected sub-command, `true` - option
var private bool selectedItemIsOption;
// `name` for sub-commands, `longName` for options
var private Text selectedItemName;
// Description of selected sub-command/option
var private Text selectedDescription;
// Are we filling optional parameters (`true`)? Or required ones (`false`)?
var private bool selectionIsOptional;
// Array of parameters we are currently filling (either required or optional)
var private array<Command.Parameter> selectedParameterArray;
var private LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong;
var private LoggerAPI.Definition warnSameLongName, warnSameShortName;
protected function Constructor() {
// Fill empty subcommand (no special key word) by default
SubCommand(P(""));
}
protected function Finalizer() {
subcommands.length = 0;
subcommandsIsOptional.length = 0;
options.length = 0;
optionsIsOptional.length = 0;
selectedParameterArray.length = 0;
commandName = none;
commandGroup = none;
commandSummary = none;
selectedItemName = none;
selectedDescription = none;
requiresTarget = false;
selectedItemIsOption = false;
selectionIsOptional = false;
}
/// Method that starts defining a new sub-command.
///
/// Creates new sub-command with a given name (if it's missing) and then selects
/// sub-command with a given name to add parameters to.
///
/// [`name`] defines name of the sub-command user wants, case-sensitive.
/// If `none` is passed, this method will do nothing.
public final function SubCommand(BaseText name) {
local int subcommandIndex;
if (name == none) {
return;
}
if (!selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(name)) {
return;
}
RecordSelection();
subcommandIndex = FindSubCommandIndex(name);
if (subcommandIndex < 0) {
MakeEmptySelection(name, false);
return;
}
// Load appropriate prepared data, if it exists for
// sub-command with name `name`
selectedItemIsOption = false;
selectedItemName = subcommands[subcommandIndex].name;
selectedDescription = subcommands[subcommandIndex].description;
selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0;
if (selectionIsOptional) {
selectedParameterArray = subcommands[subcommandIndex].optional;
} else {
selectedParameterArray = subcommands[subcommandIndex].required;
}
}
/// Method that starts defining a new option.
///
/// This method checks if some of the recorded options are in conflict with
/// given `longName` and `shortName` (already using one and only one of them).
/// In case there is no conflict, it creates new option with specified long and
/// short names (if such option is missing) and selects option with a long name
/// `longName` to add parameters to.
///
/// [`longName`] defines long name of the option, case-sensitive (for using
/// an option in form "--..."). Must be at least two characters long.
/// [`shortName`] defines short name of the option, case-sensitive (for using
/// an option in form "-..."). Must be exactly one character.
///
/// # Errors
///
/// Errors will be logged in case either of arguments are `none`, have
/// inappropriate length or are in conflict with each other.
public final function Option(BaseText longName, optional BaseText shortName) {
local int optionIndex;
local BaseText.Character shortNameAsCharacter;
// Unlike for `SubCommand()`, we need to ensure that option naming is
// correct and does not conflict with existing options
// (user might attempt to add two options with same long names and
// different short ones).
shortNameAsCharacter = GetValidShortName(longName, shortName);
if ( !_.text.IsValidCharacter(shortNameAsCharacter)
|| VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) {
// ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()`
// are responsible for logging warnings/errors
return;
}
SelectOption(longName);
// Set short name for new options
optionIndex = FindOptionIndex(longName);
if (optionIndex < 0) {
// We can only be here if option was created for the first time
RecordSelection();
// So now it cannot fail
optionIndex = FindOptionIndex(longName);
options[optionIndex].shortName = shortNameAsCharacter;
}
}
/// Adds description to the selected sub-command / option.
///
/// Highlights parts of the description in-between "`" characters.
///
/// Does nothing if nothing is yet selected.
public final function Describe(BaseText description) {
local int fromIndex, toIndex;
local BaseText.Formatting keyWordFormatting;
local bool lookingForEnd;
local MutableText coloredDescription;
if (description == none) {
return;
}
keyWordFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
coloredDescription = description.MutableCopy();
while (true) {
if (lookingForEnd) {
toIndex = coloredDescription.IndexOf(P("`"), fromIndex + 1);
} else {
fromIndex = coloredDescription.IndexOf(P("`"), toIndex + 1);
}
if (toIndex < 0 || fromIndex < 0) {
break;
}
if (lookingForEnd) {
coloredDescription.ChangeFormatting(
keyWordFormatting,
fromIndex,
toIndex - fromIndex + 1);
lookingForEnd = false;
} else {
lookingForEnd = true;
}
}
coloredDescription.Replace(P("`"), P(""));
if (lookingForEnd) {
coloredDescription.ChangeFormatting(keyWordFormatting, fromIndex);
}
_.memory.Free(selectedDescription);
selectedDescription = coloredDescription.IntoText();
}
/// Sets new group of `Command.Data` under construction.
///
/// Group name is meant to be shared among several commands, allowing user to
/// filter or fetch commands of a certain group.
/// Group name is case-insensitive.
public final function Group(BaseText newName) {
if (newName != none && newName == commandGroup) {
return;
}
_.memory.Free(commandGroup);
if (newName != none) {
commandGroup = newName.Copy();
} else {
commandGroup = none;
}
}
/// Sets new summary of `Command.Data` under construction.
///
/// Summary gives a short description of the command on the whole that will
/// be displayed when "help" command is listing available command
public final function Summary(BaseText newSummary) {
if (newSummary != none && newSummary == commandSummary) {
return;
}
_.memory.Free(commandSummary);
if (newSummary != none) {
commandSummary = newSummary.Copy();
} else {
commandSummary = none;
}
}
/// Makes caller builder to mark `Command.Data` under construction to require
/// a player target.
public final function RequireTarget() {
requiresTarget = true;
}
/// Any parameters added to currently selected sub-command / option after
/// calling this method will be marked as optional.
///
/// Further calls when the same sub-command / option is selected will do
/// nothing.
public final function OptionalParams()
{
if (selectionIsOptional) {
return;
}
// Record all required parameters first, otherwise there would be no way
// to distinguish between them and optional parameters
RecordSelection();
selectionIsOptional = true;
selectedParameterArray.length = 0;
}
/// Returns data that has been constructed so far by the caller
/// [`CommandDataBuilder`].
///
/// Does not reset progress.
public final function Command.Data BorrowData() {
local Command.Data newData;
// TODO: is this copying needed?
RecordSelection();
newData.group = commandGroup;
newData.summary = commandSummary;
newData.subcommands = subcommands;
newData.options = options;
newData.requiresTarget = requiresTarget;
return newData;
}
// Adds new parameter to selected sub-command / option
private final function PushParameter(Command.Parameter newParameter) {
selectedParameterArray[selectedParameterArray.length] = newParameter;
}
// Fills `Command.ParameterType` struct with given values
// (except boolean format).
// Assumes `displayName != none`.
private final function Command.Parameter NewParameter(
BaseText displayName,
Command.ParameterType parameterType,
bool isListParameter,
optional BaseText variableName
) {
local Command.Parameter newParameter;
newParameter.displayName = displayName.Copy();
newParameter.type = parameterType;
newParameter.allowsList = isListParameter;
if (variableName != none) {
newParameter.variableName = variableName.Copy();
} else {
newParameter.variableName = displayName.Copy();
}
return newParameter;
}
/// Adds new boolean parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`format`] defines preferred format of boolean values.
/// Command parser will still accept boolean values in any form, this setting
/// only affects how parameter will be displayed in generated help.
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command
/// input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamBoolean(
BaseText name,
optional Command.PreferredBooleanFormat format,
optional BaseText variableName
) {
local Command.Parameter newParam;
if (name != none) {
newParam = NewParameter(name, CPT_Boolean, false, variableName);
newParam.booleanFormat = format;
PushParameter(newParam);
}
}
/// Adds new integer list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`format`] defines preferred format of boolean values.
/// Command parser will still accept boolean values in any form, this setting
/// only affects how
/// parameter will be displayed in generated help.
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamBooleanList(
BaseText name,
optional Command.PreferredBooleanFormat format,
optional BaseText variableName
) {
local Command.Parameter newParam;
if (name != none) {
newParam = NewParameter(name, CPT_Boolean, true, variableName);
newParam.booleanFormat = format;
PushParameter(newParam);
}
}
/// Adds new integer parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamInteger(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Integer, false, variableName));
}
}
/// Adds new integer list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in
/// the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamIntegerList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Integer, true, variableName));
}
}
/// Adds new numeric parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamNumber(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Number, false, variableName));
}
}
/// Adds new numeric list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamNumberList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Number, true, variableName));
}
}
/// Adds new text parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
///
/// [`aliasSourceName`] defines name of the alias source that must be used to
/// auto-resolve this parameter's value. `none` means that parameter will be
/// recorded as-is, any other value (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name) will make values prefixed
/// with "$" to be resolved as custom aliases.
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
/// two fields: "alias" - value provided by user and (in case "$" prefix was
/// used) "value" - actual resolved value of an alias.
/// If alias has failed to be resolved, `none` will be stored as a value.
public final function ParamText(
BaseText name,
optional BaseText variableName,
optional BaseText aliasSourceName
) {
local Command.Parameter newParameterValue;
if (name == none) {
return;
}
newParameterValue = NewParameter(name, CPT_Text, false, variableName);
if (aliasSourceName != none) {
newParameterValue.aliasSourceName = aliasSourceName.Copy();
}
PushParameter(newParameterValue);
}
/// Adds new text list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed. If left `none`, - will coincide with
/// `name` parameter.
///
/// [`aliasSourceName`] defines name of the alias source that must be used to
/// auto-resolve this parameter's value. `none` means that parameter will be
/// recorded as-is, any other value (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name) will make values prefixed
/// with "$" to be resolved as custom aliases.
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
/// two fields: "alias" - value provided by user and (in case "$" prefix was
/// used) "value" - actual resolved value of an alias.
/// If alias has failed to be resolved, `none` will be stored as a value.
public final function ParamTextList(
BaseText name,
optional BaseText variableName,
optional BaseText aliasSourceName
) {
local Command.Parameter newParameterValue;
if (name == none) {
return;
}
newParameterValue = NewParameter(name, CPT_Text, true, variableName);
if (aliasSourceName != none) {
newParameterValue.aliasSourceName = aliasSourceName.Copy();
}
PushParameter(newParameterValue);
}
/// Adds new remainder parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// Remainder parameter is a special parameter that will simply consume all
/// remaining command's input as-is.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamRemainder(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Remainder, false, variableName));
}
}
/// Adds new JSON object parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamObject(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Object, false, variableName));
}
}
/// Adds new JSON object list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamObjectList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Object, true, variableName));
}
}
/// Adds new JSON array parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamArray(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Array, false, variableName));
}
}
/// Adds new JSON array list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamArrayList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Array, true, variableName));
}
}
/// Adds new JSON value parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamJSON(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_JSON, false, variableName));
}
}
/// Adds new JSON value list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamJSONList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_JSON, true, variableName));
}
}
/// Adds new parameter that defines a set of players (required or optional
/// depends on whether `OptionalParams()` call happened) to the currently
/// selected sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamPlayers(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName));
}
}
/// Adds new parameter that defines a list of sets of players (required or
/// optional depends on whether `OptionalParams()` call happened) to
/// the currently selected sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamPlayersList(BaseText name,optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName));
}
}
// Find index of sub-command with a given name `name` in `subcommands`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindSubCommandIndex(BaseText name) {
local int i;
if (name == none) {
return -1;
}
for (i = 0; i < subcommands.length; i += 1) {
if (name.Compare(subcommands[i].name)) {
return i;
}
}
return -1;
}
// Find index of option with a given name `name` in `options`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindOptionIndex(BaseText longName) {
local int i;
if (longName == none) {
return -1;
}
for (i = 0; i < options.length; i += 1) {
if (longName.Compare(options[i].longName)) {
return i;
}
}
return -1;
}
// Creates an empty selection record for subcommand or option with name (long name) `name`.
// Doe not check whether subcommand/option with that name already exists.
// Copies passed `name`, assumes that it is not `none`.
private final function MakeEmptySelection(BaseText name, bool selectedOption) {
selectedItemIsOption = selectedOption;
selectedItemName = name.Copy();
selectedDescription = none;
selectedParameterArray.length = 0;
selectionIsOptional = false;
}
// Select option with a given long name `longName` from `options`.
// If there is no option with specified `longName` in prepared data - creates new record in
// selection, otherwise copies previously saved data.
// Automatically saves previously selected data into prepared data.
// Copies `name` if it has to create new record.
private final function SelectOption(BaseText longName) {
local int optionIndex;
if (longName == none) {
return;
}
if (selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(longName)) {
return;
}
RecordSelection();
optionIndex = FindOptionIndex(longName);
if (optionIndex < 0) {
MakeEmptySelection(longName, true);
return;
}
// Load appropriate prepared data, if it exists for
// option with long name `longName`
selectedItemIsOption = true;
selectedItemName = options[optionIndex].longName;
selectedDescription = options[optionIndex].description;
selectionIsOptional = optionsIsOptional[optionIndex] > 0;
if (selectionIsOptional) {
selectedParameterArray = options[optionIndex].optional;
} else {
selectedParameterArray = options[optionIndex].required;
}
}
// Saves currently selected data into prepared data.
private final function RecordSelection() {
if (selectedItemName == none) {
return;
}
if (selectedItemIsOption) {
RecordSelectedOption();
} else {
RecordSelectedSubCommand();
}
}
// Saves selected sub-command into prepared records.
// Assumes that command and not an option is selected.
private final function RecordSelectedSubCommand() {
local int selectedSubCommandIndex;
local Command.SubCommand newSubcommand;
if (selectedItemName == none) {
return;
}
selectedSubCommandIndex = FindSubCommandIndex(selectedItemName);
if (selectedSubCommandIndex < 0) {
selectedSubCommandIndex = subcommands.length;
subcommands[selectedSubCommandIndex] = newSubcommand;
}
subcommands[selectedSubCommandIndex].name = selectedItemName;
subcommands[selectedSubCommandIndex].description = selectedDescription;
if (selectionIsOptional) {
subcommands[selectedSubCommandIndex].optional = selectedParameterArray;
subcommandsIsOptional[selectedSubCommandIndex] = 1;
} else {
subcommands[selectedSubCommandIndex].required = selectedParameterArray;
subcommandsIsOptional[selectedSubCommandIndex] = 0;
}
}
// Saves currently selected option into prepared records.
// Assumes that option and not an command is selected.
private final function RecordSelectedOption() {
local int selectedOptionIndex;
local Command.Option newOption;
if (selectedItemName == none) {
return;
}
selectedOptionIndex = FindOptionIndex(selectedItemName);
if (selectedOptionIndex < 0) {
selectedOptionIndex = options.length;
options[selectedOptionIndex] = newOption;
}
options[selectedOptionIndex].longName = selectedItemName;
options[selectedOptionIndex].description = selectedDescription;
if (selectionIsOptional) {
options[selectedOptionIndex].optional = selectedParameterArray;
optionsIsOptional[selectedOptionIndex] = 1;
} else {
options[selectedOptionIndex].required = selectedParameterArray;
optionsIsOptional[selectedOptionIndex] = 0;
}
}
// Validates names (printing errors in case of failure) for the option.
// Long name must be at least 2 characters long.
// Short name must be either:
// 1. exactly one character long;
// 2. `none`, which leads to deriving `shortName` from `longName`
// as a first character.
// Anything else will result in logging a failure and rejection of
// the option altogether.
// Returns `none` if validation failed and chosen short name otherwise
// (if `shortName` was used for it - it's value will be copied).
private final function BaseText.Character GetValidShortName(
BaseText longName,
BaseText shortName
) {
// Validate `longName`
if (longName == none) {
return _.text.GetInvalidCharacter();
}
if (longName.GetLength() < 2) {
_.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy());
return _.text.GetInvalidCharacter();
}
// Validate `shortName`,
// deriving if from `longName` if necessary & possible
if (shortName == none) {
return longName.GetCharacter(0);
}
if (shortName.IsEmpty() || shortName.GetLength() > 1) {
_.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy());
return _.text.GetInvalidCharacter();
}
return shortName.GetCharacter(0);
}
// Checks that if any option record has a long/short name from a given pair of
// names (`longName`, `shortName`), then it also has another one.
//
// i.e. we cannot have several options with identical names:
// (--silent, -s) and (--sick, -s).
private final function bool VerifyNoOptionNamingConflict(
BaseText longName,
BaseText.Character shortName
) {
local int i;
local bool sameShortNames, sameLongNames;
// To make sure we will search through the up-to-date `options`,
// record selection into prepared records.
RecordSelection();
for (i = 0; i < options.length; i += 1) {
sameShortNames = _.text.AreEqual(shortName, options[i].shortName);
sameLongNames = longName.Compare(options[i].longName);
if (sameLongNames && !sameShortNames) {
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(longName.Copy());
return true;
}
if (!sameLongNames && sameShortNames) {
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName));
return true;
}
}
return false;
}
defaultproperties
{
errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2")
errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2")
warnSameLongName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.")
warnSameShortName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.")
}

249
sources/BaseAPI/API/Commands/CommandList.uc

@ -0,0 +1,249 @@
/**
* Config class for storing map lists.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandList extends AcediaConfig
perObjectConfig
config(AcediaCommands);
//! `CommandList` describes a set of commands and votings that can be made
//! available to users inside Commands feature
//!
//! Optionally, permission configs can be specified for commands and votings,
//! allowing server admins to create command lists for different groups player
//! with the same commands, but different permissions.
// For storing `class<Command>` - `string` pairs in the config
struct CommandConfigStoragePair {
var public class<Command> cmd;
var public string config;
};
// For storing `class` - `string` pairs in the config
struct VotingConfigStoragePair {
var public class<Voting> vtn;
var public string config;
};
// For returning `class` - `Text` pairs into other Acedia classes
struct EntityConfigPair {
var public class<AcediaObject> class;
var public Text config;
};
/// Allows to specify if this list should only be added when server is running
/// in debug mode.
/// `true` means yes, `false` means that list will always be available.
var public config bool debugOnly;
/// Adds a command of specified class with a "default" permissions config.
var public config array< class<Command> > command;
/// Adds a command of specified class with specified permissions config
var public config array<CommandConfigStoragePair> commandWith;
/// Adds a voting of specified class with a "default" permissions config
var public config array< class<Voting> > voting;
/// Adds a voting of specified class with specified permissions config
var public config array<VotingConfigStoragePair> votingWith;
public final function array<EntityConfigPair> GetCommandData() {
local int i;
local EntityConfigPair nextPair;
local array<EntityConfigPair> result;
for (i = 0; i < command.length; i += 1) {
if (command[i] != none) {
nextPair.class = command[i];
result[result.length] = nextPair;
}
}
for (i = 0; i < commandWith.length; i += 1) {
if (commandWith[i].cmd != none) {
nextPair.class = commandWith[i].cmd;
if (commandWith[i].config != "") {
nextPair.config = _.text.FromString(commandWith[i].config);
}
result[result.length] = nextPair;
// Moved into the `result`
nextPair.config = none;
}
}
return result;
}
public final function array<EntityConfigPair> GetVotingData() {
local int i;
local EntityConfigPair nextPair;
local array<EntityConfigPair> result;
for (i = 0; i < voting.length; i += 1) {
if (voting[i] != none) {
nextPair.class = voting[i];
result[result.length] = nextPair;
}
}
for (i = 0; i < votingWith.length; i += 1) {
if (votingWith[i].vtn != none) {
nextPair.class = votingWith[i].vtn;
if (votingWith[i].config != "") {
nextPair.config = _.text.FromString(votingWith[i].config);
}
result[result.length] = nextPair;
// Moved into the `result`
nextPair.config = none;
}
}
return result;
}
protected function HashTable ToData() {
local int i;
local ArrayList entityArray;
local HashTable result, innerPair;
result = _.collections.EmptyHashTable();
result.SetBool(P("debugOnly"), debugOnly);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < command.length; i += 1) {
entityArray.AddString(string(command[i]));
}
result.SetItem(P("commands"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < voting.length; i += 1) {
entityArray.AddString(string(voting[i]));
}
result.SetItem(P("votings"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < commandWith.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("command"), string(commandWith[i].cmd));
innerPair.SetString(P("config"), commandWith[i].config);
entityArray.AddItem(innerPair);
_.memory.Free(innerPair);
}
result.SetItem(P("commandsWithConfig"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < votingWith.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("voting"), string(votingWith[i].vtn));
innerPair.SetString(P("config"), votingWith[i].config);
entityArray.AddItem(innerPair);
_.memory.Free(innerPair);
}
result.SetItem(P("votingsWithConfig"), entityArray);
_.memory.Free(entityArray);
return result;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList entityArray;
local HashTable innerPair;
local class<Command> nextCommandClass;
local class<Voting> nextVotingClass;
local CommandConfigStoragePair nextCommandPair;
local VotingConfigStoragePair nextVotingPair;
if (source == none) {
return;
}
debugOnly = source.GetBool(P("debugOnly"));
command.length = 0;
entityArray = source.GetArrayList(P("commands"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
nextCommandClass = class<Command>(_.memory.LoadClass_S(entityArray.GetString(i)));
if (nextCommandClass != none) {
command[command.length] = nextCommandClass;
}
}
}
_.memory.Free(entityArray);
voting.length = 0;
entityArray = source.GetArrayList(P("votings"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
nextVotingClass = class<Voting>(_.memory.LoadClass_S(entityArray.GetString(i)));
if (nextVotingClass != none) {
voting[voting.length] = nextVotingClass;
}
}
}
_.memory.Free(entityArray);
commandWith.length = 0;
entityArray = source.GetArrayList(P("commandsWithConfig"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
innerPair = entityArray.GetHashTable(i);
if (innerPair == none) {
continue;
}
nextCommandPair.cmd =
class<Command>(_.memory.LoadClass_S(innerPair.GetString(P("command"))));
nextCommandPair.config = innerPair.GetString(P("config"));
_.memory.Free(innerPair);
if (nextCommandPair.cmd != none) {
commandWith[commandWith.length] = nextCommandPair;
}
}
}
_.memory.Free(entityArray);
votingWith.length = 0;
entityArray = source.GetArrayList(P("votingsWithConfig"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
innerPair = entityArray.GetHashTable(i);
if (innerPair == none) {
continue;
}
nextVotingPair.vtn =
class<Voting>(_.memory.LoadClass_S(innerPair.GetString(P("voting"))));
nextVotingPair.config = innerPair.GetString(P("config"));
_.memory.Free(innerPair);
if (nextVotingPair.vtn != none) {
votingWith[votingWith.length] = nextVotingPair;
}
}
}
_.memory.Free(entityArray);
}
protected function DefaultIt() {
debugOnly = false;
command.length = 0;
commandWith.length = 0;
voting.length = 0;
votingWith.length = 0;
command[0] = class'ACommandHelp';
command[1] = class'ACommandVote';
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
debugOnly = false
command(0) = class'ACommandHelp'
command(1) = class'ACommandVote'
}

983
sources/BaseAPI/API/Commands/CommandParser.uc

@ -0,0 +1,983 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandParser extends AcediaObject
dependson(Command);
//! Class specialized for parsing user input of the command's call into
//![ `Command.CallData`] structure with the information about all parsed
//! arguments.
//!
//! [`CommandParser`] is not made to parse the whole input:
//!
//! * Command's name needs to be parsed and resolved as an alias before using
//! this parser - it won't do this hob for you;
//! * List of targeted players must also be parsed using [`PlayersParser`] -
//! [`CommandParser`] won't do this for you;
//! * Optionally one can also decide on the referred subcommand and pass it into
//! [`ParseWith()`] method. If subcommand's name is not passed -
//! [`CommandParser`] will try to parse it itself.
//! This feature is used to add support for subcommand aliases.
//!
//! However, above steps are handled by [`Commands_Feature`] and one only needs to
//! call that feature's [`HandleInput()`] methods to pass user input with command
//! call line there.
//!
//! # Usage
//!
//! Allocate [`CommandParser`] and call [`ParseWith()`] method, providing it with:
//!
//! 1. [`Parser`], filled with command call input;
//! 2. Command's data that describes subcommands, options and their parameters
//! for the command, which call we are parsing;
//! 3. (Optionally) [`EPlayer`] reference to the player that initiated
//! the command call;
//! 4. (Optionally) Subcommand to be used - this will prevent [`CommandParser`]
//! from parsing subcommand name itself. Used for implementing aliases that
//! refer to a particular subcommand.
//!
//! # Implementation
//!
//! [`CommandParser`] stores both its state and command data, relevant to parsing,
//! as its member variables during the whole parsing process, instead of passing
//! that data around in every single method.
//!
//! We will give a brief overview of how around 20 parsing methods below are
//! interconnected.
//!
//! The only public method [`ParseWith()`] is used to start parsing and it uses
//! [`PickSubCommand()`] to first try and figure out what sub command is
//! intended by user's input.
//!
//! Main bulk of the work is done by [`ParseParameterArrays()`] method, for
//! simplicity broken into two [`ParseRequiredParameterArray()`] and
//! [`ParseOptionalParameterArray()`] methods that can parse
//! parameters for both command itself and it's options.
//!
//! They go through arrays of required and optional parameters, calling
//! [`ParseParameter()`] for each parameters, which in turn can make several
//! calls of [`ParseSingleValue()`] to parse parameters' values: it is called
//! once for single-valued parameters, but possibly several times for list
//! parameters that can contain several values.
//!
//! So main parsing method looks something like:
//!
//! ```
//! ParseParameterArrays() {
//! loop ParseParameter() {
//! loop ParseSingleValue()
//! }
//! }
//! ```
//!
//! [`ParseSingleValue()`] is essentially that redirects it's method call to
//! another, more specific, parsing method based on the parameter type.
//!
//! Finally, to allow users to specify options at any point in command, we call
//! [`TryParsingOptions()`] at the beginning of every [`ParseSingleValue()`]
//! (the only parameter that has higher priority than options is
//! [`CPT_Remainder`]), since option definition can appear at any place between
//! parameters. We also call `TryParsingOptions()` *after* we've parsed all
//! command's parameters, since that case won't be detected by parsing them
//! *before* every parameter.
//!
//! [`TryParsingOptions()`] itself simply tries to detect "-" and "--" prefixes
//! (filtering out negative numeric values) and then redirect the call to either
//! of more specialized methods: [`ParseLongOption()`] or
//! [`ParseShortOption()`], that can in turn make another
//! [`ParseParameterArrays()`] call, if specified option has parameters.
//!
//! NOTE: [`ParseParameterArrays()`] can only nest in itself once, since option
//! declaration always interrupts previous option's parameter list.
//! Rest of the methods perform simple auxiliary functions.
// Describes which parameters we are currently parsing, classifying them
// as either "necessary" or "extra".
//
// E.g. if last require parameter is a list of integers,
// then after parsing first integer we are:
//
// * Still parsing required *parameter* "integer list";
// * But no more integers are *necessary* for successful parsing.
//
// Therefore we consider parameter "necessary" if the lack of it will
// result in failed parsing and "extra" otherwise.
enum ParsingTarget {
// We are in the process of parsing required parameters, that must all
// be present.
// This case does not include parsing last required parameter: it needs
// to be treated differently to track when we change from "necessary" to
// "extra" parameters.
CPT_NecessaryParameter,
// We are parsing last necessary parameter.
CPT_LastNecessaryParameter,
// We are not parsing extra parameters that can be safely omitted.
CPT_ExtraParameter,
};
// Parser filled with user input.
var private Parser commandParser;
// Data for sub-command specified by both command we are parsing
// and user's input; determined early during parsing.
var private Command.SubCommand pickedSubCommand;
// Options available for the command we are parsing.
var private array<Command.Option> availableOptions;
// Result variable we are filling during the parsing process,
// should be `none` outside of [`self.ParseWith()`] method call.
var private Command.CallData nextResult;
// Parser for player parameters, setup with a caller for current parsing
var private PlayersParser currentPlayersParser;
// Current [`ParsingTarget`], see it's enum description for more details
var private ParsingTarget currentTarget;
// `true` means we are parsing parameters for a command's option and
// `false` means we are parsing command's own parameters
var private bool currentTargetIsOption;
// If we are parsing parameters for an option (`currentTargetIsOption == true`)
// this variable will store that option's data.
var private Command.Option targetOption;
// Last successful state of [`commandParser`].
var Parser.ParserState confirmedState;
// Options we have so far encountered during parsing, necessary since we want
// to forbid specifying th same option more than once.
var private array<Command.Option> usedOptions;
// Literals that can be used as boolean values
var private array<string> booleanTrueEquivalents;
var private array<string> booleanFalseEquivalents;
var LoggerAPI.Definition errNoSubCommands;
protected function Finalizer() {
Reset();
}
/// Parses user's input given in [`parser`] using command's information given by
/// [`commandData`].
///
/// Optionally, sub-command can be specified for the [`CommandParser`] to use
/// via [`specifiedSubCommand`] argument.
/// If this argument's value is `none` - it will be parsed from [`parser`]'s
/// data instead.
///
/// Returns results of parsing, described by [`Command.CallData`].
/// Returned object is guaranteed to be not `none`.
public final function Command.CallData ParseWith(
Parser parser,
Command.Data commandData,
EPlayer callerPlayer,
optional BaseText specifiedSubCommand
) {
local HashTable commandParameters;
// Temporary object to return `nextResult` while setting variable to `none`
local Command.CallData toReturn;
nextResult.parameters = _.collections.EmptyHashTable();
nextResult.options = _.collections.EmptyHashTable();
if (commandData.subCommands.length == 0) {
DeclareError(CET_NoSubCommands, none);
toReturn = nextResult;
Reset();
return toReturn;
}
if (parser == none || !parser.Ok()) {
DeclareError(CET_BadParser, none);
toReturn = nextResult;
Reset();
return toReturn;
}
commandParser = parser;
availableOptions = commandData.options;
currentPlayersParser =
PlayersParser(_.memory.Allocate(class'PlayersParser'));
currentPlayersParser.SetSelf(callerPlayer);
// (subcommand) (parameters, possibly with options) and nothing else!
PickSubCommand(commandData, specifiedSubCommand);
nextResult.subCommandName = pickedSubCommand.name.Copy();
commandParameters = ParseParameterArrays(pickedSubCommand.required, pickedSubCommand.optional);
AssertNoTrailingInput(); // make sure there is nothing else
if (commandParser.Ok()) {
nextResult.parameters = commandParameters;
} else {
_.memory.Free(commandParameters);
}
// Clean up
toReturn = nextResult;
Reset();
return toReturn;
}
// Zero important variables
private final function Reset() {
local Command.CallData blankCallData;
_.memory.Free(currentPlayersParser);
currentPlayersParser = none;
// We didn't create this one and are not meant to free it either
commandParser = none;
nextResult = blankCallData;
currentTarget = CPT_NecessaryParameter;
currentTargetIsOption = false;
usedOptions.length = 0;
}
// Auxiliary method for recording errors
private final function DeclareError(Command.ErrorType type, optional BaseText cause) {
nextResult.parsingError = type;
if (cause != none) {
nextResult.errorCause = cause.Copy();
}
if (commandParser != none) {
commandParser.Fail();
}
}
// Assumes `commandParser != none`, is in successful state.
//
// Picks a sub command based on it's contents (parser's pointer must be before
// where subcommand's name is specified).
//
// If [`specifiedSubCommand`] is not `none` - will always use that value instead
// of parsing it from [`commandParser`].
private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) {
local int i;
local MutableText candidateSubCommandName;
local Command.SubCommand emptySubCommand;
local array<Command.SubCommand> allSubCommands;
allSubCommands = commandData.subCommands;
if (allSubcommands.length == 0) {
_.logger.Auto(errNoSubCommands).ArgClass(class);
pickedSubCommand = emptySubCommand;
return;
}
// Get candidate name
confirmedState = commandParser.GetCurrentState();
if (specifiedSubCommand != none) {
candidateSubCommandName = specifiedSubCommand.MutableCopy();
} else {
commandParser.Skip().MUntil(candidateSubCommandName,, true);
}
// Try matching it to sub commands
pickedSubCommand = allSubcommands[0];
if (candidateSubCommandName.IsEmpty()) {
candidateSubCommandName.FreeSelf();
return;
}
for (i = 0; i < allSubcommands.length; i += 1) {
if (candidateSubCommandName.Compare(allSubcommands[i].name)) {
candidateSubCommandName.FreeSelf();
pickedSubCommand = allSubcommands[i];
return;
}
}
// We will only reach here if we did not match any sub commands,
// meaning that whatever consumed by[ `candidateSubCommandName`] probably
// has a different meaning.
commandParser.RestoreState(confirmedState);
}
// Assumes `commandParser` is not `none`
// Declares an error if `commandParser` still has any input left
private final function AssertNoTrailingInput() {
local Text remainder;
if (!commandParser.Ok()) return;
if (commandParser.Skip().GetRemainingLength() <= 0) return;
remainder = commandParser.GetRemainder();
DeclareError(CET_UnusedCommandParameters, remainder);
remainder.FreeSelf();
}
// Assumes `commandParser` is not `none`.
// Parses given required and optional parameters along with any possible option
// declarations.
// Returns `HashTable` filled with (variable, parsed value) pairs.
// Failure is equal to `commandParser` entering into a failed state.
private final function HashTable ParseParameterArrays(
array<Command.Parameter> requiredParameters,
array<Command.Parameter> optionalParameters
) {
local HashTable parsedParameters;
if (!commandParser.Ok()) {
return none;
}
parsedParameters = _.collections.EmptyHashTable();
// Parse parameters
ParseRequiredParameterArray(parsedParameters, requiredParameters);
ParseOptionalParameterArray(parsedParameters, optionalParameters);
// Parse trailing options
while (TryParsingOptions());
return parsedParameters;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses given required parameters along with any possible option declarations into given
// `parsedParameters` `HashTable`.
private final function ParseRequiredParameterArray(
HashTable parsedParameters,
array<Command.Parameter> requiredParameters
) {
local int i;
if (!commandParser.Ok()) {
return;
}
currentTarget = CPT_NecessaryParameter;
while (i < requiredParameters.length) {
if (i == requiredParameters.length - 1) {
currentTarget = CPT_LastNecessaryParameter;
}
// Parse parameters one-by-one, reporting appropriate errors
if (!ParseParameter(parsedParameters, requiredParameters[i])) {
// Any failure to parse required parameter leads to error
if (currentTargetIsOption) {
DeclareError( CET_NoRequiredParamForOption,
targetOption.longName);
} else {
DeclareError( CET_NoRequiredParam,
requiredParameters[i].displayName);
}
return;
}
i += 1;
}
currentTarget = CPT_ExtraParameter;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses given optional parameters along with any possible option declarations
// into given `parsedParameters` hash table.
private final function ParseOptionalParameterArray(
HashTable parsedParameters,
array<Command.Parameter> optionalParameters
) {
local int i;
if (!commandParser.Ok()) {
return;
}
while (i < optionalParameters.length) {
confirmedState = commandParser.GetCurrentState();
// Parse parameters one-by-one, reporting appropriate errors
if (!ParseParameter(parsedParameters, optionalParameters[i])) {
// Propagate errors
if (nextResult.parsingError != CET_None) {
return;
}
// Failure to parse optional parameter is fine if
// it is caused by that parameters simply missing
commandParser.RestoreState(confirmedState);
break;
}
i += 1;
}
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses one given parameter along with any possible option declarations into
// given `parsedParameters` `HashTable`.
//
// Returns `true` if we've successfully parsed given parameter without any
// errors.
private final function bool ParseParameter(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local bool parsedEnough;
confirmedState = commandParser.GetCurrentState();
while (ParseSingleValue(parsedParameters, expectedParameter)) {
if (currentTarget == CPT_LastNecessaryParameter) {
currentTarget = CPT_ExtraParameter;
}
parsedEnough = true;
// We are done if there is either no more input or we only needed
// to parse a single value
if (!expectedParameter.allowsList) {
return true;
}
if (commandParser.Skip().HasFinished()) {
return true;
}
confirmedState = commandParser.GetCurrentState();
}
// We only succeeded in parsing if we've parsed enough for
// a given parameter and did not encounter any errors
if (parsedEnough && nextResult.parsingError == CET_None) {
commandParser.RestoreState(confirmedState);
return true;
}
// Clean up any values `ParseSingleValue` might have recorded
parsedParameters.RemoveItem(expectedParameter.variableName);
return false;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single value for a given parameter (e.g. one integer for integer or
// integer list parameter types) along with any possible option declarations
// into given `parsedParameters`.
//
// Returns `true` if we've successfully parsed a single value without
// any errors.
private final function bool ParseSingleValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
// Before parsing any other value we need to check if user has specified any options instead.
//
// However this might lead to errors if we are already parsing necessary parameters of another
// option: we must handle such situation and report an error.
if (currentTargetIsOption) {
// There is no problem is option's parameter is remainder
if (expectedParameter.type == CPT_Remainder) {
return ParseRemainderValue(parsedParameters, expectedParameter);
}
if (currentTarget != CPT_ExtraParameter && TryParsingOptions()) {
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
return false;
}
}
while (TryParsingOptions());
// First we try `CPT_Remainder` parameter, since it is a special case that
// consumes all further input
if (expectedParameter.type == CPT_Remainder) {
return ParseRemainderValue(parsedParameters, expectedParameter);
}
// Propagate errors after parsing options
if (nextResult.parsingError != CET_None) {
return false;
}
// Try parsing one of the variable types
if (expectedParameter.type == CPT_Boolean) {
return ParseBooleanValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Integer) {
return ParseIntegerValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Number) {
return ParseNumberValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Text) {
return ParseTextValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Remainder) {
return ParseRemainderValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Object) {
return ParseObjectValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Array) {
return ParseArrayValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_JSON) {
return ParseJSONValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Players) {
return ParsePlayersValue(parsedParameters, expectedParameter);
}
return false;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single boolean value into given `parsedParameters` hash table.
private final function bool ParseBooleanValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local int i;
local bool isValidBooleanLiteral;
local bool booleanValue;
local MutableText parsedLiteral;
commandParser.Skip().MUntil(parsedLiteral,, true);
if (!commandParser.Ok()) {
_.memory.Free(parsedLiteral);
return false;
}
// Try to match parsed literal to any recognizable boolean literals
for (i = 0; i < booleanTrueEquivalents.length; i += 1) {
if (parsedLiteral.CompareToString(booleanTrueEquivalents[i], SCASE_INSENSITIVE)) {
isValidBooleanLiteral = true;
booleanValue = true;
break;
}
}
for (i = 0; i < booleanFalseEquivalents.length; i += 1) {
if (isValidBooleanLiteral) {
break;
}
if (parsedLiteral.CompareToString(booleanFalseEquivalents[i], SCASE_INSENSITIVE)) {
isValidBooleanLiteral = true;
booleanValue = false;
}
}
parsedLiteral.FreeSelf();
if (!isValidBooleanLiteral) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, _.box.bool(booleanValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single integer value into given `parsedParameters` hash table.
private final function bool ParseIntegerValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local int integerValue;
commandParser.Skip().MInteger(integerValue);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, _.box.int(integerValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single number (float) value into given `parsedParameters`
// hash table.
private final function bool ParseNumberValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local float numberValue;
commandParser.Skip().MNumber(numberValue);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, _.box.float(numberValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single `Text` value into given `parsedParameters`
// hash table.
private final function bool ParseTextValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local bool failedParsing;
local MutableText textValue;
local Parser.ParserState initialState;
local HashTable resolvedPair;
// (needs some work for reading formatting `string`s from `Text` objects)
initialState = commandParser.Skip().GetCurrentState();
// Try manually parsing as a string literal first, since then we will
// allow empty `textValue` as a result
commandParser.MStringLiteral(textValue);
failedParsing = !commandParser.Ok();
// Otherwise - empty values are not allowed
if (failedParsing) {
_.memory.Free(textValue);
commandParser.RestoreState(initialState).MString(textValue);
failedParsing = (!commandParser.Ok() || textValue.IsEmpty());
}
if (failedParsing) {
_.memory.Free(textValue);
commandParser.Fail();
return false;
}
resolvedPair = AutoResolveAlias(textValue, expectedParameter.aliasSourceName);
if (resolvedPair != none) {
RecordParameter(parsedParameters, expectedParameter, resolvedPair);
_.memory.Free(textValue);
} else {
RecordParameter(parsedParameters, expectedParameter, textValue.IntoText());
}
return true;
}
// Resolves alias and returns it, along with the resolved value, if parameter
// was specified to be auto-resolved.
// Returns `none` otherwise.
private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) {
local HashTable result;
local Text resolvedValue, immutableValue;
if (textValue == none) return none;
if (aliasSourceName == none) return none;
// Always create `HashTable` with at least "alias" key
result = _.collections.EmptyHashTable();
immutableValue = textValue.Copy();
result.SetItem(P("alias"), immutableValue);
_.memory.Free(immutableValue);
// Add "value" key only after we've checked for "$" prefix
if (!textValue.StartsWithS("$")) {
result.SetItem(P("value"), immutableValue);
return result;
}
if (aliasSourceName.Compare(P("weapon"))) {
resolvedValue = _.alias.ResolveWeapon(textValue, true);
} else if (aliasSourceName.Compare(P("color"))) {
resolvedValue = _.alias.ResolveColor(textValue, true);
} else if (aliasSourceName.Compare(P("feature"))) {
resolvedValue = _.alias.ResolveFeature(textValue, true);
} else if (aliasSourceName.Compare(P("entity"))) {
resolvedValue = _.alias.ResolveEntity(textValue, true);
} else {
resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true);
}
result.SetItem(P("value"), resolvedValue);
_.memory.Free(resolvedValue);
return result;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single `Text` value into given `parsedParameters` hash table,
// consuming all remaining contents.
private final function bool ParseRemainderValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local MutableText value;
commandParser.Skip().MUntil(value);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, value.IntoText());
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single JSON object into given `parsedParameters` hash table.
private final function bool ParseObjectValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local HashTable objectValue;
objectValue = _.json.ParseHashTableWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, objectValue);
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON array into given `parsedParameters` hash table.
private final function bool ParseArrayValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local ArrayList arrayValue;
arrayValue = _.json.ParseArrayListWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, arrayValue);
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON value into given `parsedParameters`
// hash table.
private final function bool ParseJSONValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local AcediaObject jsonValue;
jsonValue = _.json.ParseWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, jsonValue);
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON value into given `parsedParameters` hash table.
private final function bool ParsePlayersValue(
HashTable parsedParameters,
Command.Parameter expectedParameter)
{
local ArrayList resultPlayerList;
local array<EPlayer> targetPlayers;
currentPlayersParser.ParseWith(commandParser);
if (commandParser.Ok()) {
targetPlayers = currentPlayersParser.GetPlayers();
} else {
return false;
}
resultPlayerList = _.collections.NewArrayList(targetPlayers);
_.memory.FreeMany(targetPlayers);
RecordParameter(parsedParameters, expectedParameter, resultPlayerList);
return true;
}
// Assumes `parsedParameters` is not `none`.
//
// Records `value` for a given `parameter` into a given `parametersArray`.
// If parameter is not a list type - simply records `value` as value under
// `parameter.variableName` key.
// If parameter is a list type - pushed value at the end of an array, recorded at
// `parameter.variableName` key (creating it if missing).
//
// All recorded values are managed by `parametersArray`.
private final function RecordParameter(
HashTable parametersArray,
Command.Parameter parameter,
/*take*/ AcediaObject value
) {
local ArrayList parameterVariable;
if (!parameter.allowsList) {
parametersArray.SetItem(parameter.variableName, value);
_.memory.Free(value);
return;
}
parameterVariable = ArrayList(parametersArray.GetItem(parameter.variableName));
if (parameterVariable == none) {
parameterVariable = _.collections.EmptyArrayList();
}
parameterVariable.AddItem(value);
_.memory.Free(value);
parametersArray.SetItem(parameter.variableName, parameterVariable);
_.memory.Free(parameterVariable);
}
// Assumes `commandParser` is not `none`.
//
// Tries to parse an option declaration (along with all of it's parameters) with
// `commandParser`.
//
// Returns `true` on success and `false` otherwise.
//
// In case of failure to detect option declaration also reverts state of
// `commandParser` to that before `TryParsingOptions()` call.
// However, if option declaration was present, but invalid (or had invalid
// parameters) parser will be left in a failed state.
private final function bool TryParsingOptions() {
local int temporaryInt;
if (!commandParser.Ok()) {
return false;
}
confirmedState = commandParser.GetCurrentState();
// Long options
commandParser.Skip().Match(P("--"));
if (commandParser.Ok()) {
return ParseLongOption();
}
// Filter out negative numbers that start similarly to short options:
// -3, -5.7, -.9
commandParser
.RestoreState(confirmedState)
.Skip()
.Match(P("-"))
.MUnsignedInteger(temporaryInt, 10, 1);
if (commandParser.Ok()) {
commandParser.RestoreState(confirmedState);
return false;
}
commandParser.RestoreState(confirmedState).Skip().Match(P("-."));
if (commandParser.Ok()) {
commandParser.RestoreState(confirmedState);
return false;
}
// Short options
commandParser.RestoreState(confirmedState).Skip().Match(P("-"));
if (commandParser.Ok()) {
return ParseShortOption();
}
commandParser.RestoreState(confirmedState);
return false;
}
// Assumes `commandParser` is not `none`.
//
// Tries to parse a long option name along with all of it's possible parameters
// with `commandParser`.
//
// Returns `true` on success and `false` otherwise. At the point this method is
// called, option declaration is already assumed to be detected and any failure
// implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseLongOption() {
local int i, optionIndex;
local MutableText optionName;
commandParser.MUntil(optionName,, true);
if (!commandParser.Ok()) {
return false;
}
while (optionIndex < availableOptions.length) {
if (optionName.Compare(availableOptions[optionIndex].longName)) break;
optionIndex += 1;
}
if (optionIndex >= availableOptions.length) {
DeclareError(CET_UnknownOption, optionName);
optionName.FreeSelf();
return false;
}
for (i = 0; i < usedOptions.length; i += 1) {
if (optionName.Compare(usedOptions[i].longName)) {
DeclareError(CET_RepeatedOption, optionName);
optionName.FreeSelf();
return false;
}
}
//usedOptions[usedOptions.length] = availableOptions[optionIndex];
optionName.FreeSelf();
return ParseOptionParameters(availableOptions[optionIndex]);
}
// Assumes `commandParser` and `nextResult` are not `none`.
//
// Tries to parse a short option name along with all of it's possible parameters
// with `commandParser`.
//
// Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseShortOption() {
local int i;
local bool pickedOptionWithParameters;
local MutableText optionsList;
commandParser.MUntil(optionsList,, true);
if (!commandParser.Ok()) {
optionsList.FreeSelf();
return false;
}
for (i = 0; i < optionsList.GetLength(); i += 1) {
if (nextResult.parsingError != CET_None) break;
pickedOptionWithParameters =
AddOptionByCharacter(
optionsList.GetCharacter(i),
optionsList,
pickedOptionWithParameters)
|| pickedOptionWithParameters;
}
optionsList.FreeSelf();
return (nextResult.parsingError == CET_None);
}
// Assumes `commandParser` and `nextResult` are not `none`.
//
// Auxiliary method that adds option by it's short version's character
// `optionCharacter`.
//
// It also accepts `optionSourceList` that describes short option expression
// (e.g. "-rtV") from
// which it originated for error reporting and `forbidOptionWithParameters`
// that, when set to `true`, forces this method to cause the
// `CET_MultipleOptionsWithParams` error if new option has non-empty parameters.
//
// Method returns `true` if added option had non-empty parameters and `false`
// otherwise.
//
// Any parsing failure inside this method always causes
// `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()`
// to check if method has failed.
private final function bool AddOptionByCharacter(
BaseText.Character optionCharacter,
BaseText optionSourceList,
bool forbidOptionWithParameters
) {
local int i;
local bool optionHasParameters;
// Prevent same option appearing twice
for (i = 0; i < usedOptions.length; i += 1) {
if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName)) {
DeclareError(CET_RepeatedOption, usedOptions[i].longName);
return false;
}
}
// If it's a new option - look it up in all available options
for (i = 0; i < availableOptions.length; i += 1) {
if (!_.text.AreEqual(optionCharacter, availableOptions[i].shortName)) {
continue;
}
usedOptions[usedOptions.length] = availableOptions[i];
optionHasParameters = (availableOptions[i].required.length > 0
|| availableOptions[i].optional.length > 0);
// Enforce `forbidOptionWithParameters` flag restriction
if (optionHasParameters && forbidOptionWithParameters) {
DeclareError(CET_MultipleOptionsWithParams, optionSourceList);
return optionHasParameters;
}
// Parse parameters (even if they are empty) and bail
commandParser.Skip();
ParseOptionParameters(availableOptions[i]);
break;
}
if (i >= availableOptions.length) {
DeclareError(CET_UnknownShortOption);
}
return optionHasParameters;
}
// Auxiliary method for parsing option's parameters (including empty ones).
// Automatically fills `nextResult` with parsed parameters (or `none` if option
// has no parameters).
// Assumes `commandParser` and `nextResult` are not `none`.
private final function bool ParseOptionParameters(Command.Option pickedOption) {
local HashTable optionParameters;
// If we are already parsing other option's parameters and did not finish
// parsing all required ones - we cannot start another option
if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) {
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
return false;
}
if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) {
nextResult.options.SetItem(pickedOption.longName, none);
return true;
}
currentTargetIsOption = true;
targetOption = pickedOption;
optionParameters = ParseParameterArrays(
pickedOption.required,
pickedOption.optional);
currentTargetIsOption = false;
if (commandParser.Ok()) {
nextResult.options.SetItem(pickedOption.longName, optionParameters);
_.memory.Free(optionParameters);
return true;
}
_.memory.Free(optionParameters);
return false;
}
defaultproperties {
booleanTrueEquivalents(0) = "true"
booleanTrueEquivalents(1) = "enable"
booleanTrueEquivalents(2) = "on"
booleanTrueEquivalents(3) = "yes"
booleanFalseEquivalents(0) = "false"
booleanFalseEquivalents(1) = "disable"
booleanFalseEquivalents(2) = "off"
booleanFalseEquivalents(3) = "no"
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.")
}

66
sources/BaseAPI/API/Commands/CommandPermissions.uc

@ -0,0 +1,66 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandPermissions extends AcediaConfig
perobjectconfig
config(AcediaCommands)
abstract;
var public config array<string> forbiddenSubCommands;
protected function HashTable ToData() {
local int i;
local HashTable data;
local ArrayList forbiddenList;
data = _.collections.EmptyHashTable();
forbiddenList = _.collections.EmptyArrayList();
for (i = 0; i < forbiddenSubCommands.length; i += 1) {
forbiddenList.AddString(Locs(forbiddenSubCommands[i]));
}
data.SetItem(P("forbiddenSubCommands"), forbiddenList);
_.memory.Free(forbiddenList);
return data;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList forbiddenList;
if (source == none) return;
forbiddenList = source.GetArrayList(P("forbiddenSubCommands"));
if (forbiddenList == none) return;
forbiddenSubCommands.length = 0;
for (i = 0; i < forbiddenList.GetLength(); i += 1) {
forbiddenSubCommands[i] = forbiddenList.GetString(i);
}
_.memory.Free(forbiddenList);
}
protected function DefaultIt() {
forbiddenSubCommands.length = 0;
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
}

81
sources/BaseAPI/API/Commands/CommandRegistrationJob.uc

@ -0,0 +1,81 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandRegistrationJob extends SchedulerJob
dependson(CommandAPI);
var private CommandAPI.AsyncTask nextItem;
// Expecting 300 units of work, this gives us registering 20 commands per tick
const ADDING_COMMAND_COST = 15;
// Adding voting option is approximately the same as adding a command's
// single sub-command - we'll estimate it as 1/3rd of the full value
const ADDING_VOTING_COST = 5;
// Authorizing is relatively cheap, whether it's commands or voting
const AUTHORIZING_COST = 1;
protected function Constructor() {
nextItem = _.commands._popPending();
}
protected function Finalizer() {
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
nextItem.entityClass = none;
nextItem.entityName = none;
nextItem.userGroup = none;
nextItem.configName = none;
}
public function bool IsCompleted() {
return (nextItem.entityName == none);
}
public function DoWork(int allottedWorkUnits) {
while (allottedWorkUnits > 0 && nextItem.entityName != none) {
if (nextItem.type == CAJT_AddCommand) {
allottedWorkUnits -= ADDING_COMMAND_COST;
_.commands.AddCommand(class<Command>(nextItem.entityClass), nextItem.entityName);
_.memory.Free(nextItem.entityName);
} else if (nextItem.type == CAJT_AddVoting) {
allottedWorkUnits -= ADDING_VOTING_COST;
_.commands.AddVoting(class<Voting>(nextItem.entityClass), nextItem.entityName);
_.memory.Free(nextItem.entityName);
} else if (nextItem.type == CAJT_AuthorizeCommand) {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeCommandUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
} else /*if (nextItem.type == CAJT_AuthorizeVoting)*/ {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeVotingUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
}
nextItem = _.commands._popPending();
}
}
defaultproperties {
}

230
sources/BaseAPI/API/Commands/Commands.uc

@ -0,0 +1,230 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Commands extends FeatureConfig
perobjectconfig
config(AcediaCommands);
/// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandSetGroupPair {
/// Name of the command set to add
var public string name;
/// Name of the group, for which to add this set
var public string for;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> rename;
/// Name to use for that class
var public string to;
};
/// Setting this to `true` enables players to input commands with "mutate"
/// console command.
/// Default is `true`.
var public config bool useMutateInput;
/// Setting this to `true` enables players to input commands right in the chat
/// by prepending them with [`chatCommandPrefix`].
/// Default is `true`.
var public config bool useChatInput;
/// Chat messages, prepended by this prefix will be treated as commands.
/// Default is "!". Empty values are also treated as "!".
var public config string chatCommandPrefix;
/// Allows to specify which user groups are used in determining command/votings
/// permission.
/// They must be specified in the order of importance: from the group with
/// highest level of permissions to the lowest. When determining player's
/// permission to use a certain command/voting, his group with the highest
/// available permissions will be used.
var public config array<string> commandGroup;
/// Add a specified `CommandList` to the specified user group
var public config array<CommandSetGroupPair> addCommandList;
/// Allows to specify a name for a certain command class
///
/// NOTE:By default command choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between commands from different packages.
var public config array<RenamingRulePair> renamingRule;
/// Allows to specify a name for a certain voting class
///
/// NOTE:By default voting choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between votings from different packages.
var public config array<RenamingRulePair> votingRenamingRule;
protected function HashTable ToData() {
local int i;
local HashTable data;
local ArrayList innerList;
local HashTable innerPair;
data = __().collections.EmptyHashTable();
data.SetBool(P("useChatInput"), useChatInput, true);
data.SetBool(P("useMutateInput"), useMutateInput, true);
data.SetString(P("chatCommandPrefix"), chatCommandPrefix);
// Serialize `commandGroup`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < commandGroup.length; i += 1) {
innerList.AddString(commandGroup[i]);
}
data.SetItem(P("commandGroups"), innerList);
_.memory.Free(innerList);
// Serialize `addCommandSet`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("name"), addCommandList[i].name);
innerPair.SetString(P("for"), addCommandList[i].for);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("commandSets"), innerList);
_.memory.Free(innerList);
// Serialize `renamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(renamingRule[i].rename));
innerPair.SetString(P("to"), renamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("renamingRules"), innerList);
_.memory.Free(innerList);
// Serialize `votingRenamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(votingRenamingRule[i].rename));
innerPair.SetString(P("to"), votingRenamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("votingRenamingRules"), innerList);
_.memory.Free(innerList);
return data;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList innerList;
local HashTable innerPair;
local CommandSetGroupPair nextCommandSetGroupPair;
local RenamingRulePair nextRenamingRule;
local class<AcediaObject> nextClass;
if (source == none) {
return;
}
useChatInput = source.GetBool(P("useChatInput"));
useMutateInput = source.GetBool(P("useMutateInput"));
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!");
// De-serialize `commandGroup`
commandGroup.length = 0;
innerList = source.GetArrayList(P("commandGroups"));
if (innerList != none) {
for (i = 0; i < commandGroup.length; i += 1) {
commandGroup[i] = innerList.GetString(i);
}
_.memory.Free(innerList);
}
// De-serialize `addCommandSet`
addCommandList.length = 0;
innerList = source.GetArrayList(P("commandSets"));
if (innerList != none) {
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextCommandSetGroupPair.name = innerPair.GetString(P("name"));
nextCommandSetGroupPair.for = innerPair.GetString(P("for"));
addCommandList[addCommandList.length] = nextCommandSetGroupPair;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `renamingRule`
renamingRule.length = 0;
innerList = source.GetArrayList(P("renamingRules"));
if (innerList != none) {
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
renamingRule[renamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `votingRenamingRule`
votingRenamingRule.length = 0;
innerList = source.GetArrayList(P("votingRenamingRules"));
if (innerList != none) {
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
votingRenamingRule[votingRenamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
}
protected function DefaultIt() {
local CommandSetGroupPair defaultPair;
useChatInput = true;
useMutateInput = true;
chatCommandPrefix = "!";
commandGroup[0] = "admin";
commandGroup[1] = "moderator";
commandGroup[2] = "trusted";
addCommandList.length = 0;
defaultPair.name = "default";
defaultPair.for = "all";
addCommandList[0] = defaultPair;
renamingRule.length = 0;
}
defaultproperties {
configName = "AcediaCommands"
}

708
sources/BaseAPI/API/Commands/Commands_Feature.uc

@ -0,0 +1,708 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Commands_Feature extends Feature
dependson(CommandAPI)
dependson(Commands);
//! This feature manages commands that automatically parse their arguments into standard Acedia
//! collections.
//!
//! # Implementation
//!
//! Implementation is simple: calling a method `RegisterCommand()` adds
//! command into two caches `registeredCommands` for obtaining registered
//! commands by name and `groupedCommands` for obtaining arrays of commands by
//! their group name. These arrays are used for providing methods for fetching
//! arrays of commands and obtaining pre-allocated `Command` instances by their
//! name.
//! Depending on settings, this feature also connects to corresponding
//! signals for catching "mutate"/chat input, then it checks user-specified name
//! for being an alias and picks correct command from `registeredCommands`.
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
//! enforces connecting to the "mutate" input.
/// Auxiliary struct for passing name of the command to call with pre-specified
/// sub-command name.
///
/// Normally sub-command name is parsed by the command itself, however command
/// aliases can try to enforce one.
struct CommandCallPair {
var MutableText commandName;
/// Not `none` in case it is enforced by an alias
var MutableText subCommandName;
};
/// Auxiliary struct that stores all the information needed to load
/// a certain command
struct EntityLoadInfo {
/// Command class to load.
var public class<AcediaObject> entityClass;
/// Name to load that command class under.
var public Text name;
/// Groups that are authorized to use that command.
var public array<Text> authorizedGroups;
/// Groups that are authorized to use that command.
var public array<Text> groupsConfig;
};
/// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandListGroupPair {
/// Name of the command set to add
var public Text commandListName;
/// Name of the group, for which to add this set
var public Text permissionGroup;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> class;
/// Name to use for that class
var public Text newName;
};
/// Tools that provide functionality of managing registered commands and votings
var private CommandAPI.CommandFeatureTools tools;
/// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters;
/// When this flag is set to true, mutate input becomes available despite
/// [`useMutateInput`] flag to allow to unlock server in case of an error
var private bool emergencyEnabledMutate;
var private /*config*/ bool useChatInput;
var private /*config*/ bool useMutateInput;
var private /*config*/ Text chatCommandPrefix;
var public /*config*/ array<string> commandGroup;
var public /*config*/ array<Commands.CommandSetGroupPair> addCommandSet;
var public /*config*/ array<Commands.RenamingRulePair> renamingRule;
var public /*config*/ array<Commands.RenamingRulePair> votingRenamingRule;
// Converted version of `commandGroup`
var private array<Text> permissionGroupOrder;
/// Converted version of `addCommandSet`
var private array<CommandListGroupPair> usedCommandLists;
/// Converted version of `renamingRule` and `votingRenamingRule`
var private array<RenamingRulePair> commandRenamingRules;
var private array<RenamingRulePair> votingRenamingRules;
// Name, under which `ACommandHelp` is registered
var private Text helpCommandName;
var LoggerAPI.Definition errServerAPIUnavailable, warnDuplicateRenaming, warnNoCommandList;
var LoggerAPI.Definition infoCommandAdded, infoVotingAdded;
protected function OnEnabled() {
helpCommandName = P("help");
// Macro selector
commandDelimiters[0] = _.text.FromString("@");
// Key selector
commandDelimiters[1] = _.text.FromString("#");
// Player array (possibly JSON array)
commandDelimiters[2] = _.text.FromString("[");
// Negation of the selector
// NOT the same thing as default command prefix in chat
commandDelimiters[3] = _.text.FromString("!");
if (useChatInput) {
_.chat.OnMessage(self).connect = HandleCommands;
}
else {
_.chat.OnMessage(self).Disconnect();
}
if (useMutateInput || emergencyEnabledMutate) {
if (__server() != none) {
__server().unreal.mutator.OnMutate(self).connect = HandleMutate;
} else {
_.logger.Auto(errServerAPIUnavailable);
}
}
LoadConfigArrays();
// `SetPermissionGroupOrder()` must be called *after* loading configs
tools.commands = CommandsTool(_.memory.Allocate(class'CommandsTool'));
tools.votings = VotingsTool(_.memory.Allocate(class'VotingsTool'));
tools.commands.SetPermissionGroupOrder(permissionGroupOrder);
tools.votings.SetPermissionGroupOrder(permissionGroupOrder);
_.commands._reloadFeature();
// Uses `CommandAPI`, so must be done after `_reloadFeature()` call
LoadCommands();
LoadVotings();
}
protected function OnDisabled() {
if (useChatInput) {
_.chat.OnMessage(self).Disconnect();
}
if (useMutateInput && __server() != none) {
__server().unreal.mutator.OnMutate(self).Disconnect();
}
useChatInput = false;
useMutateInput = false;
_.memory.Free3(tools.commands, tools.votings, chatCommandPrefix);
tools.commands = none;
tools.votings = none;
chatCommandPrefix = none;
_.memory.FreeMany(commandDelimiters);
commandDelimiters.length = 0;
_.memory.FreeMany(permissionGroupOrder);
permissionGroupOrder.length = 0;
FreeUsedCommandSets();
FreeRenamingRules();
_.commands._reloadFeature();
}
protected function SwapConfig(FeatureConfig config) {
local Commands newConfig;
newConfig = Commands(config);
if (newConfig == none) {
return;
}
_.memory.Free(chatCommandPrefix);
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
useChatInput = newConfig.useChatInput;
useMutateInput = newConfig.useMutateInput;
commandGroup = newConfig.commandGroup;
addCommandSet = newConfig.addCommandList;
renamingRule = newConfig.renamingRule;
votingRenamingRule = newConfig.votingRenamingRule;
}
/// This method allows to forcefully enable `Command_Feature` along with
/// "mutate" input in case something goes wrong.
///
/// `Command_Feature` is a critical command to have running on your server and,
/// if disabled by accident, there will be no way of starting it again without
/// restarting the level or even editing configs.
public final static function EmergencyEnable() {
local bool noWayToInputCommands;
local Text autoConfig;
local Commands_Feature feature;
if (!IsEnabled()) {
autoConfig = GetAutoEnabledConfig();
EnableMe(autoConfig);
__().memory.Free(autoConfig);
}
feature = Commands_Feature(GetEnabledInstance());
noWayToInputCommands = !feature.emergencyEnabledMutate
&& !feature.IsUsingMutateInput()
&& !feature.IsUsingChatInput();
if (noWayToInputCommands) {
default.emergencyEnabledMutate = true;
feature.emergencyEnabledMutate = true;
if (__server() != none) {
__server().unreal.mutator.OnMutate(feature).connect = HandleMutate;
} else {
__().logger.Auto(default.errServerAPIUnavailable);
}
}
}
/// Checks if `Commands_Feature` currently uses chat as input.
///
/// If `Commands_Feature` is not enabled, then it does not use anything
/// as input.
public final static function bool IsUsingChatInput() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none) {
return instance.useChatInput;
}
return false;
}
/// Checks if `Commands_Feature` currently uses mutate command as input.
///
/// If `Commands_Feature` is not enabled, then it does not use anything
/// as input.
public final static function bool IsUsingMutateInput() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none) {
return instance.useMutateInput;
}
return false;
}
/// Returns prefix that will indicate that chat message is intended to be
/// a command. By default "!".
///
/// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetChatPrefix() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none && instance.chatCommandPrefix != none) {
return instance.chatCommandPrefix.Copy();
}
return none;
}
/// Returns name, under which [`ACommandHelp`] is registered.
///
/// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetHelpCommandName() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none && instance.helpCommandName != none) {
return instance.helpCommandName.Copy();
}
return none;
}
/// Executes command based on the input.
///
/// Takes [`commandLine`] as input with command's call, finds appropriate
/// registered command instance and executes it with parameters specified in
/// the [`commandLine`].
///
/// [`callerPlayer`] has to be specified and represents instigator of this
/// command that will receive appropriate result/error messages.
///
/// Returns `true` iff command was successfully executed.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to the [`callerPlayer`]
public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
local bool result;
local Parser wrapper;
if (input == none) {
return false;
}
wrapper = input.Parse();
result = HandleInputWith(wrapper, callerPlayer);
wrapper.FreeSelf();
return result;
}
/// Executes command based on the input.
///
/// Takes [`commandLine`] as input with command's call, finds appropriate
/// registered command instance and executes it with parameters specified in
/// the [`commandLine`].
///
/// [`callerPlayer`] has to be specified and represents instigator of this
/// command that will receive appropriate result/error messages.
///
/// Returns `true` iff command was successfully executed.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to the [`callerPlayer`]
public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) {
local bool errorOccured;
local User identity;
local CommandAPI.CommandConfigInfo commandPair;
local Command.CallData callData;
local CommandCallPair callPair;
if (parser == none) return false;
if (callerPlayer == none) return false;
if (!parser.Ok()) return false;
identity = callerPlayer.GetIdentity();
callPair = ParseCommandCallPairWith(parser);
commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity);
if (commandPair.instance == none || commandPair.usageForbidden) {
if (callerPlayer != none && callerPlayer.IsExistent()) {
callerPlayer
.BorrowConsole()
.Flush()
.Say(F("{$TextFailure Command not found!}"));
}
}
if (parser.Ok() && commandPair.instance != none && !commandPair.usageForbidden) {
callData =
commandPair.instance.ParseInputWith(parser, callerPlayer, callPair.subCommandName);
errorOccured = commandPair.instance.Execute(callData, callerPlayer, commandPair.config);
commandPair.instance.DeallocateCallData(callData);
}
_.memory.Free4(commandPair.instance, callPair.commandName, callPair.subCommandName, identity);
return errorOccured;
}
// Parses command's name into `CommandCallPair` - sub-command is filled in case
// specified name is an alias with specified sub-command name.
private final function CommandCallPair ParseCommandCallPairWith(Parser parser) {
local Text resolvedValue;
local MutableText userSpecifiedName;
local CommandCallPair result;
local Text.Character dotCharacter;
if (parser == none) return result;
if (!parser.Ok()) return result;
parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true);
resolvedValue = _.alias.ResolveCommand(userSpecifiedName);
// This isn't an alias
if (resolvedValue == none) {
result.commandName = userSpecifiedName;
return result;
}
// It is an alias - parse it
dotCharacter = _.text.GetCharacter(".");
resolvedValue.Parse()
.MUntil(result.commandName, dotCharacter)
.MatchS(".")
.MUntil(result.subCommandName, dotCharacter)
.FreeSelf();
if (result.subCommandName.IsEmpty()) {
result.subCommandName.FreeSelf();
result.subCommandName = none;
}
resolvedValue.FreeSelf();
return result;
}
private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) {
local Parser parser;
// We are only interested in messages that start with `chatCommandPrefix`
parser = _.text.Parse(message);
if (!parser.Match(chatCommandPrefix).Ok()) {
parser.FreeSelf();
return true;
}
// Pass input to command feature
HandleInputWith(parser, sender);
parser.FreeSelf();
return false;
}
private function HandleMutate(string command, PlayerController sendingPlayer) {
local Parser parser;
local EPlayer sender;
// A lot of other mutators use these commands
if (command ~= "help") return;
if (command ~= "version") return;
if (command ~= "status") return;
if (command ~= "credits") return;
parser = _.text.ParseString(command);
sender = _.players.FromController(sendingPlayer);
HandleInputWith(parser, sender);
sender.FreeSelf();
parser.FreeSelf();
}
private final function LoadConfigArrays() {
local int i;
local CommandListGroupPair nextCommandSetGroupPair;
for (i = 0; i < commandGroup.length; i += 1) {
permissionGroupOrder[i] = _.text.FromString(commandGroup[i]);
}
for (i = 0; i < addCommandSet.length; i += 1) {
nextCommandSetGroupPair.commandListName = _.text.FromString(addCommandSet[i].name);
nextCommandSetGroupPair.permissionGroup = _.text.FromString(addCommandSet[i].for);
usedCommandLists[i] = nextCommandSetGroupPair;
}
FreeRenamingRules();
commandRenamingRules = LoadRenamingRules(renamingRule);
votingRenamingRules = LoadRenamingRules(votingRenamingRule);
}
private final function array<RenamingRulePair> LoadRenamingRules(
array<Commands.RenamingRulePair> inputRules) {
local int i, j;
local RenamingRulePair nextRule;
local array<RenamingRulePair> result;
// Clear away duplicates
for (i = 0; i < inputRules.length; i += 1) {
j = i + 1;
while (j < inputRules.length) {
if (inputRules[i].rename == inputRules[j].rename) {
_.logger.Auto(warnDuplicateRenaming)
.ArgClass(inputRules[i].rename)
.Arg(_.text.FromString(inputRules[i].to))
.Arg(_.text.FromString(inputRules[j].to));
inputRules.Remove(j, 1);
} else {
j += 1;
}
}
}
// Translate rules
for (i = 0; i < inputRules.length; i += 1) {
nextRule.class = inputRules[i].rename;
nextRule.newName = _.text.FromString(inputRules[i].to);
if (nextRule.class == class'ACommandHelp') {
_.memory.Free(helpCommandName);
helpCommandName = nextRule.newName.Copy();
}
result[result.length] = nextRule;
}
return result;
}
private final function LoadCommands() {
local int i, j;
local Text nextName;
local array<EntityLoadInfo> commandClassesToLoad;
commandClassesToLoad = CollectAllCommandClasses();
// Load command names to use, according to preferred names and name rules
for (i = 0; i < commandClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < commandRenamingRules.length; j += 1) {
if (commandClassesToLoad[i].entityClass == commandRenamingRules[j].class) {
nextName = commandRenamingRules[j].newName.Copy();
break;
}
}
if (nextName == none) {
nextName = class<Command>(commandClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
commandClassesToLoad[i].name = nextName;
}
// Actually load commands
for (i = 0; i < commandClassesToLoad.length; i += 1) {
_.commands.AddCommandAsync(
class<Command>(commandClassesToLoad[i].entityClass),
commandClassesToLoad[i].name);
for (j = 0; j < commandClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeCommandUsageAsync(
commandClassesToLoad[i].name,
commandClassesToLoad[i].authorizedGroups[j],
commandClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoCommandAdded)
.ArgClass(commandClassesToLoad[i].entityClass)
.Arg(/*take*/ commandClassesToLoad[i].name);
}
for (i = 0; i < commandClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(commandClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(commandClassesToLoad[i].groupsConfig);
}
}
private final function LoadVotings() {
local int i, j;
local Text nextName;
local array<EntityLoadInfo> votingClassesToLoad;
votingClassesToLoad = CollectAllVotingClasses();
// Load voting names to use, according to preferred names and name rules
for (i = 0; i < votingClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < votingRenamingRules.length; j += 1) {
if (votingClassesToLoad[i].entityClass == votingRenamingRules[j].class) {
nextName = votingRenamingRules[j].newName.Copy();
break;
}
}
if (nextName == none) {
nextName = class<Voting>(votingClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
votingClassesToLoad[i].name = nextName;
}
// Actually load votings
for (i = 0; i < votingClassesToLoad.length; i += 1) {
_.commands.AddVotingAsync(
class<Voting>(votingClassesToLoad[i].entityClass),
votingClassesToLoad[i].name);
for (j = 0; j < votingClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeVotingUsageAsync(
votingClassesToLoad[i].name,
votingClassesToLoad[i].authorizedGroups[j],
votingClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoVotingAdded)
.ArgClass(votingClassesToLoad[i].entityClass)
.Arg(/*take*/ votingClassesToLoad[i].name);
}
for (i = 0; i < votingClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(votingClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(votingClassesToLoad[i].groupsConfig);
}
}
// Guaranteed to not return `none` items in the array
private final function array<EntityLoadInfo> CollectAllCommandClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetCommandData(),
usedCommandLists[i].permissionGroup);
} else {
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
}
}
return result;
}
// Guaranteed to not return `none` items in the array
private final function array<EntityLoadInfo> CollectAllVotingClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetVotingData(),
usedCommandLists[i].permissionGroup);
} else {
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
}
}
return result;
}
// Adds `newCommands` into `infoArray`, adding `commandsGroup` to
// their array `authorizedGroups`
//
// Assumes that all arguments aren't `none`.
//
// Guaranteed to not add `none` commands from `newCommands`.
//
// Assumes that items from `infoArray` all have `name` field set to `none`,
// will also leave them as `none`.
private final function MergeEntityClassArrays(
out array<EntityLoadInfo> infoArray,
/*take*/ array<CommandList.EntityConfigPair> newCommands,
Text commandsGroup
) {
local int i, infoToEditIndex;
local EntityLoadInfo infoToEdit;
local array<Text> editedArray;
for (i = 0; i < newCommands.length; i += 1) {
if (newCommands[i].class == none) {
continue;
}
// Search for an existing record of class `newCommands[i]` in
// `infoArray`. If found, copy to `infoToEdit` and index into
// `infoToEditIndex`, else `infoToEditIndex` will hold the next unused
// index in `infoArray`.
infoToEditIndex = 0;
while (infoToEditIndex < infoArray.length) {
if (infoArray[infoToEditIndex].entityClass == newCommands[i].class) {
infoToEdit = infoArray[infoToEditIndex];
break;
}
infoToEditIndex += 1;
}
// Update data inside `infoToEdit`.
infoToEdit.entityClass = newCommands[i].class;
editedArray = infoToEdit.authorizedGroups;
editedArray[editedArray.length] = commandsGroup.Copy();
infoToEdit.authorizedGroups = editedArray;
editedArray = infoToEdit.groupsConfig;
editedArray[editedArray.length] = newCommands[i].config; // moving here
infoToEdit.groupsConfig = editedArray;
// Update `infoArray` with the modified record.
infoArray[infoToEditIndex] = infoToEdit;
// Forget about data `authorizedGroups` and `groupsConfig`:
//
// 1. Their references have already been moved into `infoArray` and
// don't need to be released;
// 2. If we don't find corresponding struct inside `infoArray` on
// the next iteration, we'll override `commandClass`,
// but not `authorizedGroups`/`groupsConfig`, so we'll just reset them
// to empty now.
// (`name` field is expected to be `none` during this method)
infoToEdit.authorizedGroups.length = 0;
infoToEdit.groupsConfig.length = 0;
}
}
private final function FreeUsedCommandSets() {
local int i;
for (i = 0; i < usedCommandLists.length; i += 1) {
_.memory.Free(usedCommandLists[i].commandListName);
_.memory.Free(usedCommandLists[i].permissionGroup);
}
usedCommandLists.length = 0;
}
private final function FreeRenamingRules() {
local int i;
for (i = 0; i < commandRenamingRules.length; i += 1) {
_.memory.Free(commandRenamingRules[i].newName);
}
commandRenamingRules.length = 0;
for (i = 0; i < votingRenamingRules.length; i += 1) {
_.memory.Free(votingRenamingRules[i].newName);
}
votingRenamingRules.length = 0;
}
public final function CommandAPI.CommandFeatureTools _borrowTools() {
return tools;
}
defaultproperties {
configClass = class'Commands'
errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.")
warnDuplicateRenaming = (l=LOG_Warning,m="Class `%1` has duplicate renaming rules: \"%2\" and \"%3\". Latter will be discarded. It is recommended that you fix your `AcediaCommands` configuration file.")
warnNoCommandList = (l=LOG_Warning,m="Command list \"%1\" not found.")
infoCommandAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load command `%1` as \"%2\".")
infoVotingAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load voting `%1` as \"%2\".")
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Signal.uc

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandAdded_Signal extends Signal;
public final function bool Emit(class<Command> addedClass, Text usedName) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnCommandAdded_Slot(nextSlot).connect(addedClass, usedName);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnCommandAdded_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Slot.uc

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandAdded_Slot extends Slot;
delegate connect(class<Command> addedClass, Text usedName) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Signal.uc

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandRemoved_Signal extends Signal;
public final function bool Emit(class<Command> removedClass) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnCommandRemoved_Slot(nextSlot).connect(removedClass);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnCommandRemoved_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Slot.uc

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandRemoved_Slot extends Slot;
delegate connect(class<Command> removedClass) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Signal.uc

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingAdded_Signal extends Signal;
public final function bool Emit(class<Voting> addedClass, Text usedName) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingAdded_Slot(nextSlot).connect(addedClass, usedName);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingAdded_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Slot.uc

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingAdded_Slot extends Slot;
delegate connect(class<Voting> addedClass, Text usedName) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Signal.uc

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingEnded_Signal extends Signal;
public final function bool Emit(bool success, HashTable arguments) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingEnded_Slot(nextSlot).connect(success, arguments);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingEnded_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Slot.uc

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingEnded_Slot extends Slot;
delegate connect(bool success, HashTable arguments) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Signal.uc

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingRemoved_Signal extends Signal;
public final function bool Emit(class<Voting> removedClass) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingRemoved_Slot(nextSlot).connect(removedClass);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingRemoved_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Slot.uc

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingRemoved_Slot extends Slot;
delegate connect(class<Voting> removedClass) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

494
sources/BaseAPI/API/Commands/PlayersParser.uc

@ -0,0 +1,494 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*\
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class PlayersParser extends AcediaObject
dependson(Parser);
//! This parser is supposed to parse player set definitions as they
//! are used in commands.
//!
//! Basic use is to specify one of the selectors:
//! 1. Key selector: "#<integer>" (examples: "#1", "#5").
//! This one is used to specify players by their key, assigned to them when
//! they enter the game.
//! This type of selectors can be used when players have hard to type names.
//! 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@".
//! "@", "@me", and "@self" are identical and can be used to specify player
//! that called the command.
//! "@admin" can be used to specify all admins in the game at once.
//! "@all" specifies all current players.
//! In future it is planned to make macros extendable by allowing to bind
//! more names to specific groups of players.
//! 3. Name selectors: quoted strings and any other types of string that do not
//! start with either "#" or "@".
//! These specify name prefixes: any player with specified prefix will be considered to match
//! such selector.
//!
//! Negated selectors: "!<selector>". Specifying "!" in front of selector will
//! select all players that do not match it instead.
//!
//! Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']".
//! Specified selectors are process in order: from left to right.
//! First selector works as usual and selects a set of players.
//! All the following selectors either expand that list (additive ones, without
//! "!" prefix) or remove specific players from the list (the ones with "!"
//! prefix). Examples of that:
//!
//! * "[@admin, !@self]" - selects all admins, except the one who called
//! the command (whether he is admin or not).
//! * "[dkanus, 'mate']" - will select players "dkanus" and "mate".
//! Order also matters, since:
//! - "[@admin, !@admin]" - won't select anyone, since it will first add all
//! the admins and then remove them.
//! - "[!@admin, @admin]" - will select everyone, since it will first select
//! everyone who is not an admin and then adds everyone else.
//!
//! # Usage
//!
//! 1. Allocate `PlayerParser`;
//! 2. Set caller player through `SetSelf()` method to make "@" and "@me"
//! selectors usable;
//! 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that
//! starts with proper players selector;
//! 4. Call `GetPlayers()` to obtain selected players array.
//!
//! # Implementation
//!
//! When created, `PlayersParser` takes a snapshot (array) of current players on
//! the server. Then `currentSelection` is decided based on whether first
//! selector is positive (initial selection is taken as empty array) or negative
//! (initial selection is taken as full snapshot).
//!
//! After that `PlayersParser` simply goes through specified selectors
//! (in case more than one is specified) and adds or removes appropriate players
//! in `currentSelection`, assuming that `playersSnapshot` is a current full
//! array of players.
/// Player for which "@", "@me", and "@self" macros will refer
var private EPlayer selfPlayer;
/// Copy of the list of current players at the moment of allocation of
/// this `PlayersParser`.
var private array<EPlayer> playersSnapshot;
/// Players, selected according to selectors we have parsed so far
var private array<EPlayer> currentSelection;
/// Have we parsed our first selector?
/// We need this to know whether to start with the list of
/// all players (if first selector removes them) or
/// with empty list (if first selector adds them).
var private bool parsedFirstSelector;
/// Will be equal to a single-element array [","], used for parsing
var private array<Text> selectorDelimiters;
var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA;
var const int TOPEN_BRACKET, TCLOSE_BRACKET;
protected function Finalizer() {
// No need to deallocate `currentSelection`,
// since it has `EPlayer`s from `playersSnapshot` or `selfPlayer`
_.memory.Free(selfPlayer);
_.memory.FreeMany(playersSnapshot);
selfPlayer = none;
parsedFirstSelector = false;
playersSnapshot.length = 0;
currentSelection.length = 0;
}
/// Set a player who will be referred to by "@", "@me" and "@self" macros.
///
/// Passing `none` will make it so no one is referred by these macros.
public final function SetSelf(EPlayer newSelfPlayer) {
_.memory.Free(selfPlayer);
selfPlayer = none;
if (newSelfPlayer != none) {
selfPlayer = EPlayer(newSelfPlayer.Copy());
}
}
/// Returns players parsed by the last `ParseWith()` or `Parse()` call.
///
/// If neither were yet called - returns an empty array.
public final function array<EPlayer> GetPlayers() {
local int i;
local array<EPlayer> result;
for (i = 0; i < currentSelection.length; i += 1) {
if (currentSelection[i].IsExistent()) {
result[result.length] = EPlayer(currentSelection[i].Copy());
}
}
return result;
}
/// Parses players from `parser` according to the currently present players.
///
/// Array of parsed players can be retrieved by `self.GetPlayers()` method.
///
/// Returns `true` if parsing was successful and `false` otherwise.
public final function bool ParseWith(Parser parser) {
local Parser.ParserState confirmedState;
if (parser == none) return false;
if (!parser.Ok()) return false;
if (parser.HasFinished()) return false;
Reset();
confirmedState = parser.Skip().GetCurrentState();
if (!parser.Match(T(TOPEN_BRACKET)).Ok()) {
ParseSelector(parser.RestoreState(confirmedState));
if (parser.Ok()) {
return true;
}
return false;
}
while (parser.Ok() && !parser.HasFinished()) {
confirmedState = parser.Skip().GetCurrentState();
if (parser.Match(T(TCLOSE_BRACKET)).Ok()) {
return true;
}
parser.RestoreState(confirmedState);
if (parsedFirstSelector) {
parser.Match(T(TCOMMA)).Skip();
}
ParseSelector(parser);
parser.Skip();
}
parser.Fail();
return false;
}
/// Parses players from according to the currently present players.
///
/// Array of parsed players can be retrieved by `self.GetPlayers()` method.
/// Returns `true` if parsing was successful and `false` otherwise.
public final function bool Parse(BaseText toParse) {
local bool wasSuccessful;
local Parser parser;
if (toParse == none) {
return false;
}
parser = _.text.Parse(toParse);
wasSuccessful = ParseWith(parser);
parser.FreeSelf();
return wasSuccessful;
}
// Insert a new player into currently selected list of players
// (`currentSelection`) such that there will be no duplicates.
//
// `none` values are auto-discarded.
private final function InsertPlayer(EPlayer toInsert) {
local int i;
if (toInsert == none) {
return;
}
for (i = 0; i < currentSelection.length; i += 1) {
if (currentSelection[i] == toInsert) {
return;
}
}
currentSelection[currentSelection.length] = toInsert;
}
// Adds all the players with specified key (`key`) to the current selection.
private final function AddByKey(int key) {
local int i;
for (i = 0; i < playersSnapshot.length; i += 1) {
if (playersSnapshot[i].GetIdentity().GetKey() == key) {
InsertPlayer(playersSnapshot[i]);
}
}
}
// Removes all the players with specified key (`key`) from
// the current selection.
private final function RemoveByKey(int key) {
local int i;
while (i < currentSelection.length) {
if (currentSelection[i].GetIdentity().GetKey() == key) {
currentSelection.Remove(i, 1);
} else {
i += 1;
}
}
}
// Adds all the players with specified name (`name`) to the current selection.
private final function AddByName(BaseText name) {
local int i;
local Text nextPlayerName;
if (name == none) {
return;
}
for (i = 0; i < playersSnapshot.length; i += 1) {
nextPlayerName = playersSnapshot[i].GetName();
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) {
InsertPlayer(playersSnapshot[i]);
}
nextPlayerName.FreeSelf();
}
}
// Removes all the players with specified name (`name`) from
// the current selection.
private final function RemoveByName(BaseText name) {
local int i;
local Text nextPlayerName;
while (i < currentSelection.length) {
nextPlayerName = currentSelection[i].GetName();
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) {
currentSelection.Remove(i, 1);
} else {
i += 1;
}
nextPlayerName.FreeSelf();
}
}
// Adds all the admins to the current selection.
private final function AddAdmins() {
local int i;
for (i = 0; i < playersSnapshot.length; i += 1) {
if (playersSnapshot[i].IsAdmin()) {
InsertPlayer(playersSnapshot[i]);
}
}
}
// Removes all the admins from the current selection.
private final function RemoveAdmins() {
local int i;
while (i < currentSelection.length) {
if (currentSelection[i].IsAdmin()) {
currentSelection.Remove(i, 1);
} else {
i += 1;
}
}
}
// Add all the players specified by `macroText` (from macro "@<macroText>").
// Does nothing if there is no such macro.
private final function AddByMacro(BaseText macroText) {
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) {
AddAdmins();
return;
}
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) {
currentSelection = playersSnapshot;
return;
}
if ( macroText.IsEmpty()
|| macroText.Compare(T(TSELF), SCASE_INSENSITIVE)
|| macroText.Compare(T(TME), SCASE_INSENSITIVE)) {
InsertPlayer(selfPlayer);
}
}
// Removes all the players specified by `macroText` (from macro "@<macroText>").
// Does nothing if there is no such macro.
private final function RemoveByMacro(BaseText macroText) {
local int i;
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) {
RemoveAdmins();
return;
}
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) {
currentSelection.length = 0;
return;
}
if (macroText.IsEmpty() || macroText.Compare(T(TSELF), SCASE_INSENSITIVE)) {
while (i < currentSelection.length) {
if (currentSelection[i] == selfPlayer) {
currentSelection.Remove(i, 1);
} else {
i += 1;
}
}
}
}
// Parses one selector from `parser`, while accordingly modifying current player
// selection list.
private final function ParseSelector(Parser parser) {
local bool additiveSelector;
local Parser.ParserState confirmedState;
if (parser == none) return;
if (!parser.Ok()) return;
confirmedState = parser.GetCurrentState();
if (!parser.Match(T(TNOT)).Ok()) {
additiveSelector = true;
parser.RestoreState(confirmedState);
}
// Determine whether we stars with empty or full player list
if (!parsedFirstSelector) {
parsedFirstSelector = true;
if (additiveSelector) {
currentSelection.length = 0;
}
else {
currentSelection = playersSnapshot;
}
}
// Try all selector types
confirmedState = parser.GetCurrentState();
if (parser.Match(T(TKEY)).Ok()) {
ParseKeySelector(parser, additiveSelector);
return;
}
parser.RestoreState(confirmedState);
if (parser.Match(T(TMACRO)).Ok()) {
ParseMacroSelector(parser, additiveSelector);
return;
}
parser.RestoreState(confirmedState);
ParseNameSelector(parser, additiveSelector);
}
// Parse key selector (assuming "#" is already consumed), while accordingly
// modifying current player selection list.
private final function ParseKeySelector(Parser parser, bool additiveSelector) {
local int key;
if (parser == none) return;
if (!parser.Ok()) return;
if (!parser.MInteger(key).Ok()) return;
if (additiveSelector) {
AddByKey(key);
} else {
RemoveByKey(key);
}
}
// Parse macro selector (assuming "@" is already consumed), while accordingly
// modifying current player selection list.
private final function ParseMacroSelector(Parser parser, bool additiveSelector) {
local MutableText macroName;
local Parser.ParserState confirmedState;
if (parser == none) return;
if (!parser.Ok()) return;
confirmedState = parser.GetCurrentState();
macroName = ParseLiteral(parser);
if (!parser.Ok()) {
_.memory.Free(macroName);
return;
}
if (additiveSelector) {
AddByMacro(macroName);
}
else {
RemoveByMacro(macroName);
}
_.memory.Free(macroName);
}
// Parse name selector, while accordingly modifying current player selection
// list.
private final function ParseNameSelector(Parser parser, bool additiveSelector) {
local MutableText playerName;
local Parser.ParserState confirmedState;
if (parser == none) return;
if (!parser.Ok()) return;
confirmedState = parser.GetCurrentState();
playerName = ParseLiteral(parser);
if (!parser.Ok() || playerName.IsEmpty()) {
_.memory.Free(playerName);
return;
}
if (additiveSelector) {
AddByName(playerName);
}
else {
RemoveByName(playerName);
}
_.memory.Free(playerName);
}
// Reads a string that can either be a body of name selector (some player's
// name prefix) or of a macro selector (what comes after "@").
//
// This is different from `parser.MString()` because it also uses "," as
// a separator.
private final function MutableText ParseLiteral(Parser parser) {
local MutableText literal;
local Parser.ParserState confirmedState;
if (parser == none) return none;
if (!parser.Ok()) return none;
confirmedState = parser.GetCurrentState();
if (!parser.MStringLiteral(literal).Ok()) {
parser.RestoreState(confirmedState);
parser.MUntilMany(literal, selectorDelimiters, true);
}
return literal;
}
// Resets this object to initial state before parsing and update
// `playersSnapshot` to contain current players.
private final function Reset() {
parsedFirstSelector = false;
currentSelection.length = 0;
_.memory.FreeMany(playersSnapshot);
playersSnapshot.length = 0;
playersSnapshot = _.players.GetAll();
selectorDelimiters.length = 0;
selectorDelimiters[0] = T(TCOMMA);
selectorDelimiters[1] = T(TCLOSE_BRACKET);
}
defaultproperties {
TSELF = 0
stringConstants(0) = "self"
TADMIN = 1
stringConstants(1) = "admin"
TALL = 2
stringConstants(2) = "all"
TNOT = 3
stringConstants(3) = "!"
TKEY = 4
stringConstants(4) = "#"
TMACRO = 5
stringConstants(5) = "@"
TCOMMA = 6
stringConstants(6) = ","
TOPEN_BRACKET = 7
stringConstants(7) = "["
TCLOSE_BRACKET = 8
stringConstants(8) = "]"
TME = 9
stringConstants(9) = "me"
}

21
sources/Commands/Tests/MockCommandA.uc → sources/BaseAPI/API/Commands/Tests/MockCommandA.uc

@ -21,16 +21,17 @@ class MockCommandA extends Command;
protected function BuildData(CommandDataBuilder builder)
{
builder.ParamObject(P("just_obj"))
.ParamArrayList(P("manyLists"))
.OptionalParams()
.ParamObject(P("last_obj"));
builder.SubCommand(P("simple"))
.ParamBooleanList(P("isItSimple?"))
.ParamInteger(P("integer variable"), P("int"))
.OptionalParams()
.ParamNumberList(P("numeric list"), P("list"))
.ParamTextList(P("another list"));
builder.ParamObject(P("just_obj"));
builder.ParamArrayList(P("manyLists"));
builder.OptionalParams();
builder.ParamObject(P("last_obj"));
builder.SubCommand(P("simple"));
builder.ParamBooleanList(P("isItSimple?"));
builder.ParamInteger(P("integer variable"), P("int"));
builder.OptionalParams();
builder.ParamNumberList(P("numeric list"), P("list"));
builder.ParamTextList(P("another list"));
}
defaultproperties

61
sources/BaseAPI/API/Commands/Tests/MockCommandB.uc

@ -0,0 +1,61 @@
/**
* Mock command class for testing.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockCommandB extends Command;
protected function BuildData(CommandDataBuilder builder)
{
builder.ParamArray(P("just_array"));
builder.ParamText(P("just_text"));
builder.Option(P("values"));
builder.ParamIntegerList(P("types"));
builder.Option(P("long"));
builder.ParamInteger(P("num"));
builder.ParamNumberList(P("text"));
builder.ParamBoolean(P("huh"));
builder.Option(P("type"), P("t"));
builder.ParamText(P("type"));
builder.Option(P("Test"));
builder.ParamText(P("to_test"));
builder.Option(P("silent"));
builder.Option(P("forced"));
builder.Option(P("verbose"), P("V"));
builder.Option(P("actual"));
builder.SubCommand(P("do"));
builder.OptionalParams();
builder.ParamNumberList(P("numeric list"), P("list"));
builder.ParamBoolean(P("maybe"));
builder.Option(P("remainder"));
builder.ParamRemainder(P("everything"));
builder.SubCommand(P("json"));
builder.ParamJSON(P("first_json"));
builder.ParamJSONList(P("other_json"));
}
defaultproperties
{
}

0
sources/Commands/Tests/TEST_Command.uc → sources/BaseAPI/API/Commands/Tests/TEST_Command.uc

24
sources/Commands/Tests/TEST_CommandDataBuilder.uc → sources/BaseAPI/API/Commands/Tests/TEST_CommandDataBuilder.uc

@ -24,27 +24,33 @@ class TEST_CommandDataBuilder extends TestCase
protected static function CommandDataBuilder PrepareBuilder()
{
local CommandDataBuilder builder;
builder =
CommandDataBuilder(__().memory.Allocate(class'CommandDataBuilder'));
builder.ParamNumber(P("var")).ParamText(P("str_var"), P("otherName"));
builder = CommandDataBuilder(__().memory.Allocate(class'CommandDataBuilder'));
builder.ParamNumber(P("var"));
builder.ParamText(P("str_var"), P("otherName"));
builder.OptionalParams();
builder.Describe(P("Simple command"));
builder.ParamBooleanList(P("list"), PBF_OnOff);
// Subcommands
builder.SubCommand(P("sub")).ParamArray(P("array_var"));
builder.SubCommand(P("sub"));
builder.ParamArray(P("array_var"));
builder.Describe(P("Alternative command!"));
builder.ParamIntegerList(P("int"));
builder.SubCommand(P("empty"));
builder.Describe(P("Empty one!"));
builder.SubCommand(P("huh")).ParamNumber(P("list"));
builder.SubCommand(P("sub")).ParamObjectList(P("one_more"), P("but"));
builder.SubCommand(P("huh"));
builder.ParamNumber(P("list"));
builder.SubCommand(P("sub"));
builder.ParamObjectList(P("one_more"), P("but"));
builder.Describe(P("Alternative command! Updated!"));
// Options
builder.Option(P("silent")).Describe(P("Just an option, I dunno."));
builder.Option(P("silent"));
builder.Describe(P("Just an option, I dunno."));
builder.Option(P("Params"), P("d"));
builder.ParamBoolean(P("www"), PBF_YesNo, P("random"));
builder.OptionalParams().ParamIntegerList(P("www2"));
return builder.RequireTarget();
builder.OptionalParams();
builder.ParamIntegerList(P("www2"));
builder.RequireTarget();
return builder;
}
protected static function Command.SubCommand GetSubCommand(

351
sources/BaseAPI/API/Commands/Tests/TEST_Voting.uc

@ -0,0 +1,351 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_Voting extends TestCase
abstract
dependsOn(VotingModel);
enum ExpectedOutcome {
TEST_EO_Continue,
TEST_EO_End,
};
protected static function VotingModel MakeVotingModel(bool drawMeansWin) {
local VotingModel model;
model = VotingModel(__().memory.Allocate(class'VotingModel'));
model.Start(drawMeansWin);
return model;
}
protected static function SetVoters(
VotingModel model,
optional string voterID0,
optional string voterID1,
optional string voterID2,
optional string voterID3,
optional string voterID4,
optional string voterID5,
optional string voterID6,
optional string voterID7,
optional string voterID8,
optional string voterID9
) {
local UserID nextID;
local array<UserID> voterIDs;
if (voterID0 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID0));
voterIDs[voterIDs.length] = nextID;
}
if (voterID1 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID1));
voterIDs[voterIDs.length] = nextID;
}
if (voterID2 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID2));
voterIDs[voterIDs.length] = nextID;
}
if (voterID3 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID3));
voterIDs[voterIDs.length] = nextID;
}
if (voterID4 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID4));
voterIDs[voterIDs.length] = nextID;
}
if (voterID5 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID5));
voterIDs[voterIDs.length] = nextID;
}
if (voterID6 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID6));
voterIDs[voterIDs.length] = nextID;
}
if (voterID7 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID7));
voterIDs[voterIDs.length] = nextID;
}
if (voterID8 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID8));
voterIDs[voterIDs.length] = nextID;
}
if (voterID9 != "") {
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(__().text.FromString(voterID9));
voterIDs[voterIDs.length] = nextID;
}
model.UpdatePotentialVoters(voterIDs);
}
protected static function MakeFaultyYesVote(
VotingModel model,
string voterID,
VotingModel.VotingResult expected) {
local UserID id;
id = UserID(__().memory.Allocate(class'UserID'));
id.Initialize(__().text.FromString(voterID));
Issue("Illegal vote had unexpected result.");
TEST_ExpectTrue(model.CastVote(id, true) == expected);
}
protected static function MakeFaultyNoVote(
VotingModel model,
string voterID,
VotingModel.VotingResult expected) {
local UserID id;
id = UserID(__().memory.Allocate(class'UserID'));
id.Initialize(__().text.FromString(voterID));
Issue("Illegal vote had unexpected result.");
TEST_ExpectTrue(model.CastVote(id, false) == expected);
}
protected static function VoteYes(VotingModel model, string voterID, ExpectedOutcome expected) {
local UserID id;
id = UserID(__().memory.Allocate(class'UserID'));
id.Initialize(__().text.FromString(voterID));
Issue("Failed to add legitimate vote.");
TEST_ExpectTrue(model.CastVote(id, true) == VFR_Success);
if (expected == TEST_EO_Continue) {
Issue("Vote, that shouldn't have ended voting, ended it.");
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
} else if (expected == TEST_EO_End) {
Issue("Vote, that should've ended voting with one side's victory, didn't do it.");
TEST_ExpectTrue(model.GetStatus() == VPM_Success);
}
}
protected static function VoteNo(VotingModel model, string voterID, ExpectedOutcome expected) {
local UserID id;
id = UserID(__().memory.Allocate(class'UserID'));
id.Initialize(__().text.FromString(voterID));
Issue("Failed to add legitimate vote.");
TEST_ExpectTrue(model.CastVote(id, false) == VFR_Success);
if (expected == TEST_EO_Continue) {
Issue("Vote, that shouldn't have ended voting, ended it.");
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
} else if (expected == TEST_EO_End) {
Issue("Vote, that should've ended voting with one side's victory, didn't do it.");
TEST_ExpectTrue(model.GetStatus() == VPM_Failure);
}
}
protected static function TESTS() {
Test_VotingModel();
}
protected static function Test_VotingModel() {
SubTest_YesVoting();
SubTest_NoVoting();
SubTest_FaultyVoting();
SubTest_DisconnectVoting_DrawMeansWin();
SubTest_DisconnectVoting_DrawMeansLoss();
SubTest_ReconnectVoting_DrawMeansWin();
SubTest_ReconnectVoting_DrawMeansLoss();
}
protected static function SubTest_YesVoting() {
local VotingModel model;
Context("Testing \"yes\" voting.");
model = MakeVotingModel(true);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteYes(model, "2", TEST_EO_Continue);
VoteYes(model, "4", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
VoteNo(model, "1", TEST_EO_Continue);
VoteYes(model, "6", TEST_EO_End);
model = MakeVotingModel(false);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteYes(model, "2", TEST_EO_Continue);
VoteYes(model, "4", TEST_EO_Continue);
VoteYes(model, "3", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
VoteNo(model, "1", TEST_EO_Continue);
VoteYes(model, "6", TEST_EO_End);
}
protected static function SubTest_NoVoting() {
local VotingModel model;
Context("Testing \"no\" voting.");
model = MakeVotingModel(true);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteNo(model, "1", TEST_EO_Continue);
VoteNo(model, "2", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteYes(model, "4", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_End);
model = MakeVotingModel(false);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteNo(model, "1", TEST_EO_Continue);
VoteNo(model, "2", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteYes(model, "4", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_End);
}
protected static function SubTest_FaultyVoting() {
local VotingModel model;
Context("Testing \"faulty\" voting.");
model = MakeVotingModel(false);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteYes(model, "3", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteYes(model, "2", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_Continue);
VoteYes(model, "1", TEST_EO_Continue);
VoteYes(model, "6", TEST_EO_End);
MakeFaultyYesVote(model, "4", VFR_VotingEnded); // voting already ended, so no more votes here
model = MakeVotingModel(true);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteYes(model, "3", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_Continue);
VoteYes(model, "1", TEST_EO_Continue);
VoteYes(model, "6", TEST_EO_End);
MakeFaultyYesVote(model, "4", VFR_VotingEnded); // voting already ended, so no more votes here
}
protected static function SubTest_DisconnectVoting_DrawMeansWin() {
local VotingModel model;
Context("Testing \"disconnect\" voting when draw means victory.");
model = MakeVotingModel(true);
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
VoteYes(model, "1", TEST_EO_Continue);
VoteYes(model, "2", TEST_EO_Continue);
VoteYes(model, "3", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_Continue);
VoteYes(model, "4", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 2 "yes" votes
MakeFaultyNoVote(model, "1", VFR_NotAllowed);
VoteNo(model, "8", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteNo(model, "9", TEST_EO_Continue);
// Here we're at 3 "no" votes, 3 "yes" votes out of 7 total;
// disconnect "6" and "9" for "yes" to win
SetVoters(model, "2", "4", "5", "8", "10");
Issue("Unexpected result after voting users disconnected.");
TEST_ExpectTrue(model.GetStatus() == VPM_Success);
}
protected static function SubTest_DisconnectVoting_DrawMeansLoss() {
local VotingModel model;
Context("Testing \"disconnect\" voting when draw means loss.");
model = MakeVotingModel(false);
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
VoteYes(model, "1", TEST_EO_Continue);
VoteYes(model, "2", TEST_EO_Continue);
VoteYes(model, "6", TEST_EO_Continue);
VoteYes(model, "3", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteNo(model, "5", TEST_EO_Continue);
VoteYes(model, "4", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
VoteYes(model, "7", TEST_EO_Continue);
SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 3 "yes" votes
MakeFaultyNoVote(model, "1", VFR_NotAllowed);
VoteNo(model, "8", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteNo(model, "9", TEST_EO_Continue);
// Here we're at 3 "no" votes, 3 "yes" votes out of 7 total;
// disconnect "6" and "9" for "yes" to win
SetVoters(model, "2", "4", "5", "8", "10");
Issue("Unexpected result after voting users disconnected.");
TEST_ExpectTrue(model.GetStatus() == VPM_Success);
}
protected static function SubTest_ReconnectVoting_DrawMeansWin() {
local VotingModel model;
Context("Testing \"reconnect\" voting when draw means victory.");
model = MakeVotingModel(true);
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
VoteYes(model, "1", TEST_EO_Continue);
VoteNo(model, "2", TEST_EO_Continue);
VoteYes(model, "3", TEST_EO_Continue);
VoteNo(model, "4", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
// Disconnect 1 3 "yes" voters
SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10");
VoteYes(model, "7", TEST_EO_Continue);
VoteNo(model, "8", TEST_EO_Continue);
VoteYes(model, "9", TEST_EO_Continue);
MakeFaultyNoVote(model, "1", VFR_NotAllowed);
MakeFaultyNoVote(model, "3", VFR_NotAllowed);
// Restore 3 "yes" voter
SetVoters(model, "2", "3", "4", "5", "6", "7", "8", "9", "10");
VoteNo(model, "3", TEST_EO_End);
}
protected static function SubTest_ReconnectVoting_DrawMeansLoss() {
local VotingModel model;
Context("Testing \"reconnect\" voting when draw means loss.");
model = MakeVotingModel(false);
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
VoteYes(model, "1", TEST_EO_Continue);
VoteNo(model, "2", TEST_EO_Continue);
VoteYes(model, "3", TEST_EO_Continue);
VoteNo(model, "4", TEST_EO_Continue);
VoteYes(model, "5", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
// Disconnect 1 3 "yes" voters
SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10");
VoteYes(model, "7", TEST_EO_Continue);
VoteYes(model, "9", TEST_EO_Continue);
MakeFaultyNoVote(model, "1", VFR_NotAllowed);
MakeFaultyNoVote(model, "3", VFR_NotAllowed);
// Restore 3 "yes" voter
SetVoters(model, "2", "3", "4", "5", "6", "7", "8", "9", "10");
VoteNo(model, "8", TEST_EO_Continue);
VoteNo(model, "3", TEST_EO_End);
}
defaultproperties {
caseGroup = "Commands"
caseName = "Voting model"
}

306
sources/BaseAPI/API/Commands/Tools/CmdItemsTool.uc

@ -0,0 +1,306 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CmdItemsTool extends AcediaObject
dependson(CommandAPI)
abstract;
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Command`] instances and [`Voting`] classes: they both have in common
//! the need to remember who was authorized to use them (i.e. which user group)
//! and with what permissions (i.e. name of the config that contains appropriate
//! permissions).
//!
//! Aside from trivial accessors to its data, it also provides a way to resolve
//! the best permissions available to the user by finding the most priviledged
//! group he belongs to.
//!
//! NOTE: child classes must implement `MakeCard()` method and can override
//! `DiscardCard()` method to catch events of removing items from storage.
/// Allows to specify a base class requirement for this tool - only classes
/// that were derived from it can be stored inside.
var protected const class<AcediaObject> ruleBaseClass;
/// Names of user groups that can decide permissions for items,
/// in order of importance: from most significant to the least significant.
/// This is used for resolving the best permissions for each user.
var private array<Text> permissionGroupOrder;
/// Maps item names to their [`ItemCards`] with information about which groups
/// are authorized to use this particular item.
var private HashTable registeredCards;
var LoggerAPI.Definition errItemInvalidName;
var LoggerAPI.Definition errItemDuplicate;
protected function Constructor() {
registeredCards = _.collections.EmptyHashTable();
}
protected function Finalizer() {
_.memory.Free(registeredCards);
_.memory.FreeMany(permissionGroupOrder);
registeredCards = none;
permissionGroupOrder.length = 0;
}
/// Registers given item class under the specified (case-insensitive) name.
///
/// If name parameter is omitted (specified as `none`) or is an invalid name
/// (according to [`BaseText::IsValidName()`] method), then item class will not
/// be registered.
///
/// Returns `true` if item was successfully registered and `false` otherwise`.
///
/// # Errors
///
/// If provided name that is invalid or already taken by a different item -
/// a warning will be logged and item class won't be registered.
public function bool AddItemClass(class<AcediaObject> itemClass, BaseText itemName) {
local Text itemKey;
local ItemCard newCard, existingCard;
if (itemClass == none) return false;
if (itemName == none) return false;
if (registeredCards == none) return false;
if (ruleBaseClass == none || !ClassIsChildOf(itemClass, ruleBaseClass)) {
return false;
}
// The item name is transformed into lowercase, immutable value.
// This facilitates the use of item names as keys in a [`HashTable`],
// enabling case-insensitive matching.
itemKey = itemName.LowerCopy();
if (itemKey == none || !itemKey.IsValidName()) {
_.logger.Auto(errItemInvalidName).ArgClass(itemClass).Arg(itemKey);
return false;
}
// Guaranteed to only store cards
existingCard = ItemCard(registeredCards.GetItem(itemName));
if (existingCard != none) {
_.logger.Auto(errItemDuplicate)
.ArgClass(existingCard.GetItemClass())
.Arg(itemKey)
.ArgClass(itemClass);
_.memory.Free(existingCard);
return false;
}
newCard = MakeCard(itemClass, itemName);
registeredCards.SetItem(itemKey, newCard);
_.memory.Free2(itemKey, newCard);
return true;
}
/// Removes item of given class from the list of registered items.
///
/// Removing once registered item is not an action that is expected to
/// be performed under normal circumstances and does not have an efficient
/// implementation (it is linear on the current amount of items).
///
/// Returns `true` if successfully removed registered item class and
/// `false` otherwise (either item wasn't registered or caller tool
/// initialized).
public function bool RemoveItemClass(class<AcediaObject> itemClass) {
local int i;
local CollectionIterator iter;
local ItemCard nextCard;
local array<Text> keysToRemove;
if (itemClass == none) return false;
if (registeredCards == none) return false;
// Removing items during iterator breaks an iterator, so first we find
// all the keys to remove
iter = registeredCards.Iterate();
iter.LeaveOnlyNotNone();
while (!iter.HasFinished()) {
// Guaranteed to only be `ItemCard`
nextCard = ItemCard(iter.Get());
if (nextCard.GetItemClass() == itemClass) {
keysToRemove[keysToRemove.length] = Text(iter.GetKey());
DiscardCard(nextCard);
}
_.memory.Free(nextCard);
iter.Next();
}
iter.FreeSelf();
// Actual clean up everything in `keysToRemove`
for (i = 0; i < keysToRemove.length; i += 1) {
registeredCards.RemoveItem(keysToRemove[i]);
}
_.memory.FreeMany(keysToRemove);
return (keysToRemove.length > 0);
}
/// Allows to specify the order of the user group in terms of privilege for
/// accessing stored items. Only specified groups will be used when resolving
/// appropriate permissions config name for a user.
public final function SetPermissionGroupOrder(array<Text> groupOrder) {
local int i;
_.memory.FreeMany(permissionGroupOrder);
permissionGroupOrder.length = 0;
for (i = 0; i < groupOrder.length; i += 1) {
if (groupOrder[i] != none) {
permissionGroupOrder[permissionGroupOrder.length] = groupOrder[i].Copy();
}
}
}
/// Specifies what permissions (given by the config name) given user group has
/// when using an item with a specified name.
///
/// Method must be called after item with a given name is added.
///
/// If this config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, only an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// registered.
///
/// Method will return `true` if group was successfully authorized and `false`
/// otherwise (either group already authorized or no item with specified name
/// was added in the caller tool so far).
///
/// # Errors
///
/// If specified group was already authorized to use card's item, then it
/// will log a warning message about it.
public function bool AuthorizeUsage(BaseText itemName, BaseText groupName, BaseText configName) {
local bool result;
local ItemCard relevantCard;
if (configName != none && !configName.IsValidName()) {
return false;
}
relevantCard = GetCard(itemName);
if (relevantCard != none) {
result = relevantCard.AuthorizeGroupWithConfig(groupName, configName);
_.memory.Free(relevantCard);
}
return result;
}
/// Returns struct with item class (+ instance, if one was stored) for a given
/// case in-sensitive item name and name of the config with best permissions
/// available to the player with provided ID.
///
/// Function only returns `none` for item class if item with a given name
/// wasn't found.
/// Config name being `none` with non-`none` item class in the result means
/// that user with provided ID doesn't have permissions for using the item at
/// all.
public final function CommandAPI.ItemConfigInfo ResolveItem(BaseText itemName, BaseText textID) {
local int i;
local ItemCard relevantCard;
local CommandAPI.ItemConfigInfo result;
relevantCard = GetCard(itemName);
if (relevantCard == none) {
// At this point contains `none` for all values -> indicates a failure
// to find item in storage
return result;
}
result.instance = relevantCard.GetItem();
result.class = relevantCard.GetItemClass();
if (textID == none) {
return result;
}
// Look through all `permissionGroupOrder` in order to find most priviledged
// group that user with `textID` belongs to
for (i = 0; i < permissionGroupOrder.length && result.configName == none; i += 1) {
if (_.users.IsSteamIDInGroup(textID, permissionGroupOrder[i])) {
result.configName = relevantCard.GetConfigNameForGroup(permissionGroupOrder[i]);
}
}
_.memory.Free(relevantCard);
return result;
}
/// Returns all item classes that are stored inside caller tool.
///
/// Doesn't check for duplicates (although with a normal usage, there shouldn't
/// be any).
public final function array< class<AcediaObject> > GetAllItemClasses() {
local array< class<AcediaObject> > result;
local ItemCard value;
local CollectionIterator iter;
for (iter = registeredCards.Iterate(); !iter.HasFinished(); iter.Next()) {
value = ItemCard(iter.Get());
if (value != none) {
result[result.length] = value.GetItemClass();
}
_.memory.Free(value);
}
iter.FreeSelf();
return result;
}
/// Returns array of names of all available items.
public final function array<Text> GetItemsNames() {
local array<Text> emptyResult;
if (registeredCards != none) {
return registeredCards.GetTextKeys();
}
return emptyResult;
}
/// Called each time a new card is to be created and stored.
///
/// Must be reimplemented by child classes.
protected function ItemCard MakeCard(class<AcediaObject> itemClass, BaseText itemName) {
return none;
}
/// Called each time a certain card is to be removed from storage.
///
/// Must be reimplemented by child classes
/// (reimplementations SHOULD NOT DEALLOCATE `toDiscard`).
protected function DiscardCard(ItemCard toDiscard) {
}
/// Find item card for the item that was stored with a specified
/// case-insensitive name
///
/// Function only returns `none` if item with a given name wasn't found
/// (or `none` was provided as an argument).
protected final function ItemCard GetCard(BaseText itemName) {
local Text itemKey;
local ItemCard relevantCard;
if (itemName == none) return none;
if (registeredCards == none) return none;
/// The item name is transformed into lowercase, immutable value.
/// This facilitates the use of item names as keys in a [`HashTable`],
/// enabling case-insensitive matching.
itemKey = itemName.LowerCopy();
relevantCard = ItemCard(registeredCards.GetItem(itemKey));
_.memory.Free(itemKey);
return relevantCard;
}
defaultproperties {
errItemInvalidName = (l=LOG_Error,m="Attempt at registering item with class `%1` under an invalid name \"%2\" will be ignored.")
errItemDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Attempt at registering command `%3` with the same name will be ignored.")
}

142
sources/BaseAPI/API/Commands/Tools/CommandsTool.uc

@ -0,0 +1,142 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsTool extends CmdItemsTool;
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Command`] instances.
//!
//! This storage class allows for efficient manipulation and retrieval of
//! [`Command`]s, along with information about what use groups were authorized
//! to use them.
//!
//! Additionally, this tool allows for efficient fetching of commands that
//! belong to a particular *command group*.
/// [`HashTable`] that maps a command group name to a set of command names that
/// belong to it.
var private HashTable groupedCommands;
protected function Constructor() {
super.Constructor();
groupedCommands = _.collections.EmptyHashTable();
}
protected function Finalizer() {
super.Finalizer();
_.memory.Free(groupedCommands);
groupedCommands = none;
}
/// Returns all known command groups' names.
public final function array<Text> GetGroupsNames() {
local array<Text> emptyResult;
if (groupedCommands != none) {
return groupedCommands.GetTextKeys();
}
return emptyResult;
}
/// Returns array of names of all available commands belonging to the specified
/// group.
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) {
local int i;
local ArrayList commandNamesArray;
local array<Text> result;
if (groupedCommands == none) return result;
commandNamesArray = groupedCommands.GetArrayList(groupName);
if (commandNamesArray == none) return result;
for (i = 0; i < commandNamesArray.GetLength(); i += 1) {
result[result.length] = commandNamesArray.GetText(i);
}
_.memory.Free(commandNamesArray);
return result;
}
protected function ItemCard MakeCard(class<AcediaObject> commandClass, BaseText itemName) {
local Command newCommandInstance;
local ItemCard newCard;
local Text commandGroup;
if (class<Command>(commandClass) != none) {
newCommandInstance = Command(_.memory.Allocate(commandClass, true));
newCommandInstance.Initialize(itemName);
newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
newCard.InitializeWithInstance(newCommandInstance);
// Guaranteed to be lower case (keys of [`HashTable`])
if (itemName != none) {
itemName = itemName.LowerCopy();
} else {
itemName = newCommandInstance.GetPreferredName();
}
commandGroup = newCommandInstance.GetGroupName();
AssociateGroupAndName(commandGroup, itemName);
_.memory.Free3(newCommandInstance, itemName, commandGroup);
}
return newCard;
}
protected function DiscardCard(ItemCard toDiscard) {
local Text groupKey, commandName;
local Command storedCommand;
local ArrayList listOfCommands;
if (toDiscard == none) return;
// Guaranteed to store a [`Command`]
storedCommand = Command(toDiscard.GetItem());
if (storedCommand == none) return;
// Guaranteed to be stored in a lower case
commandName = storedCommand.GetName();
listOfCommands = groupedCommands.GetArrayList(groupKey);
if (listOfCommands != none && commandName != none) {
listOfCommands.RemoveItem(commandName);
}
_.memory.Free2(commandName, storedCommand);
}
// Expect both arguments to be not `none`.
// Expect both arguments to be lower-case.
private final function AssociateGroupAndName(BaseText groupKey, BaseText commandName) {
local ArrayList listOfCommands;
if (groupedCommands != none) {
listOfCommands = groupedCommands.GetArrayList(groupKey);
if (listOfCommands == none) {
listOfCommands = _.collections.EmptyArrayList();
}
if (listOfCommands.Find(commandName) < 0) {
// `< 0` means not found
listOfCommands.AddItem(commandName);
}
// Set `listOfCommands` in case we've just created that array.
// Won't do anything if it is already recorded there.
groupedCommands.SetItem(groupKey, listOfCommands);
}
}
defaultproperties {
ruleBaseClass = class'Command';
}

177
sources/BaseAPI/API/Commands/Tools/ItemCard.uc

@ -0,0 +1,177 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ItemCard extends AcediaObject;
//! Utility class designed for storing either class of an object
//! (possibly also a specific instance) along with authorization information:
//! which user groups are allowed to use stored entity and with what level of
//! permissions (defined by the name of a config with permissions).
//!
//! [`ItemCard`] has to be initialized with either [`InitializeWithClass()`] or
//! [`InitializeWithInstance()`] before it can be used.
/// Class of object that this card describes.
var private class<AcediaObject> storedClass;
/// Instance of an object (can also *optionally* be stored in this card)
var private AcediaObject storedInstance;
/// This [`HashTable`] maps authorized groups to their respective config names.
///
/// Each key represents an authorized group, and its corresponding value
/// indicates the associated config name. If a key has a value of `none`,
/// the default config (named "default") should be used for that group.
var private HashTable groupToConfig;
var LoggerAPI.Definition errGroupAlreadyHasConfig;
protected function Finalizer() {
_.memory.Free2(storedInstance, groupToConfig);
storedInstance = none;
storedClass = none;
groupToConfig = none;
}
/// Initializes the caller [`ItemCard`] object with class to be stored.
///
/// Initialization can only be done once: once method returned `true`,
/// all future calls will fail.
///
/// Returns `false` if caller was already initialized or `none` is provided as
/// an argument. Otherwise succeeds and returns `true`.
public function bool InitializeWithClass(class<AcediaObject> toStore) {
if (storedClass != none) return false;
if (toStore == none) return false;
storedClass = toStore;
groupToConfig = _.collections.EmptyHashTable();
return true;
}
/// Initializes the caller [`ItemCard`] object with an object to be stored.
///
/// Initialization can only be done once: once method returned `true`,
/// all future calls will fail.
///
/// Returns `false` caller was already initialized or `none` is provided as
/// an argument. Otherwise succeeds and returns `true`.
public function bool InitializeWithInstance(AcediaObject toStore) {
if (storedClass != none) return false;
if (toStore == none) return false;
storedClass = toStore.class;
storedInstance = toStore;
storedInstance.NewRef();
groupToConfig = _.collections.EmptyHashTable();
return true;
}
/// Authorizes a new group to use the this card's item.
///
/// This function allows to specify the config name for a particular user group.
/// If this config name is skipped (specified as `none`), then "default" will be
/// used instead.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or caller [`ItemCard`]
/// isn't initialized).
///
/// # Errors
///
/// If specified group was already authorized to use card's item, then it
/// will log an error message about it.
public function bool AuthorizeGroupWithConfig(BaseText groupName, optional BaseText configName) {
local Text itemKey;
local Text storedConfigName;
if (storedClass == none) return false;
if (groupToConfig == none) return false;
if (groupName == none) return false;
if (groupName.IsEmpty()) return false;
/// Make group name immutable and have its characters have a uniform case to
/// be usable as case-insensitive keys for [`HashTable`].
itemKey = groupName.LowerCopy();
storedConfigName = groupToConfig.GetText(itemKey);
if (storedConfigName != none) {
_.logger.Auto(errGroupAlreadyHasConfig)
.ArgClass(storedClass)
.Arg(groupName.Copy())
.Arg(storedConfigName)
.Arg(configName.Copy());
_.memory.Free(itemKey);
return false;
}
// We don't actually record "default" value at this point, instead opting
// to return "default" in getter functions in case stored `configName`
// is `none`.
groupToConfig.SetItem(itemKey, configName);
_.memory.Free(itemKey);
return true;
}
/// Returns item instance for the caller [`ItemCard`].
///
/// Returns `none` iff this card wasn't initialized with an instance.
public function AcediaObject GetItem() {
if (storedInstance != none) {
storedInstance.NewRef();
}
return storedInstance;
}
/// Returns item class for the caller [`ItemCard`].
///
/// Returns `none` iff this card wasn't initialized.
public function class<AcediaObject> GetItemClass() {
return storedClass;
}
/// Returns the name of config that was authorized for the specified group.
///
/// Returns `none` if group wasn't authorized, otherwise guaranteed to
/// return non-`none` and non-empty `Text` value.
public function Text GetConfigNameForGroup(BaseText groupName) {
local Text groupNameAsKey, result;
if (storedClass == none) return none;
if (groupToConfig == none) return none;
if (groupName == none) return none;
/// Make group name immutable and have its characters a uniform case to
/// be usable as case-insensitive keys for [`HashTable`]
groupNameAsKey = groupName.LowerCopy();
if (groupToConfig.HasKey(groupNameAsKey)) {
result = groupToConfig.GetText(groupNameAsKey);
if (result == none) {
// If we do have specified group recorded as a key, then we must
// return non-`none` config name, defaulting to "default" value
// if none was provided
result = P("default").Copy();
}
}
_.memory.Free(groupNameAsKey);
return result;
}
defaultproperties {
errGroupAlreadyHasConfig = (l=LOG_Error,m="Item `%1` is already added to group '%2' with config '%3'. Attempt to add it with config '%4' is ignored.")
}

119
sources/BaseAPI/API/Commands/Tools/VotingsTool.uc

@ -0,0 +1,119 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class VotingsTool extends CmdItemsTool
dependson(CommandAPI);
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Voting`] classes.
//!
//! This storage class allows for efficient manipulation and retrieval of
//! [`Voting`] classes, along with information about what use groups were
//! authorized to use them.
//!
//! Additionally this tool is used to keep track of the currently ongoing
//! voting, preventing [`CommandsAPI`] from starting several votings at once.
/// Currently running voting process.
/// This tool doesn't actively track when voting ends, so reference can be
/// non-`none` even if voting has already ended. Instead `DropFinishedVoting()`
/// method is used as needed to figure out whether that voting has ended and
/// should be deallocated.
var private Voting currentVoting;
protected function Finalizer() {
super.Finalizer();
_.memory.Free(currentVoting);
currentVoting = none;
}
/// Starts a voting process with a given name, returning its result.
public final function CommandAPI.StartVotingResult StartVoting(
CommandAPI.VotingConfigInfo votingData,
HashTable arguments
) {
local CommandAPI.StartVotingResult result;
DropFinishedVoting();
if (currentVoting != none) {
return SVR_AlreadyInProgress;
}
if (votingData.votingClass == none) {
return SVR_UnknownVoting;
}
currentVoting = Voting(_.memory.Allocate(votingData.votingClass));
result = currentVoting.Start(votingData.config, arguments);
if (result != SVR_Success) {
_.memory.Free(currentVoting);
currentVoting = none;
}
return result;
}
/// Returns `true` iff some voting is currently active.
public final function bool IsVotingRunning() {
DropFinishedVoting();
return (currentVoting != none);
}
/// Returns instance of the active voting.
///
/// `none` iff no voting is currently active.
public final function Voting GetCurrentVoting() {
DropFinishedVoting();
if (currentVoting != none) {
currentVoting.NewRef();
}
return currentVoting;
}
protected function ItemCard MakeCard(class<AcediaObject> votingClass, BaseText itemName) {
local ItemCard newCard;
if (class<Voting>(votingClass) != none) {
newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
newCard.InitializeWithClass(votingClass);
}
return newCard;
}
private final function class<Voting> GetVoting(BaseText itemName) {
local ItemCard relevantCard;
local class<Voting> result;
relevantCard = GetCard(itemName);
if (relevantCard != none) {
result = class<Voting>(relevantCard.GetItemClass());
}
_.memory.Free(relevantCard);
return result;
}
// Clears `currentVoting` if it has already finished
private final function DropFinishedVoting() {
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
}
defaultproperties {
ruleBaseClass = class'Voting'
}

863
sources/BaseAPI/API/Commands/Voting/Voting.uc

@ -0,0 +1,863 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Voting extends AcediaObject
dependsOn(VotingModel)
dependson(CommandAPI);
//! Class that describes a single voting option.
//!
//! [`Voting`] is created to be heavily integrated with [`Commands_Feature`] and
//! shouldn't be used separately from it.
//! You shouldn't allocate its instances directly unless you're working on
//! the [`Commands_Feature`]'s or related code.
//!
//! Generally, [`Voting`] will only update whenever its methods are called.
//! The only exception is when a time limit was specified, then
//! `TryAnnounceTimer()` will be called every tick, voting emulating countdown.
//!
//! ## Usage
//!
//! This class takes care of the voting process by itself, one only needs to
//! call [`Start()`] method.
//! If you wish to prematurely end voting (e.g. forcing it to end), then call
//! [`ForceEnding()`] method.
//!
//! When implementing your own voting:
//!
//! 1. You normally would override [`Execute()`] method to perform any
//! actions you want upon the voting success.
//! 2. If you want your voting to take any arguments, you should also
//! overload [`AddInfo()`].
//! 3. You can also override [`HandleVotingStart()`] method to setup custom
//! voting's messages inside `currentAnnouncements` or to reject
//! starting this voting altogether.
/// Describes possible results of [`ForceEnding()`] method.
enum ForceEndingOutcome {
/// Voting forcing was successful.
FEO_Success,
/// User is not allowed to force voting.
FEO_Forbidden,
/// No voting to end at this moment.
FEO_NotApplicable
};
/*******************************************************************************
* Voting settings that should be specified for child classes.
******************************************************************************/
/// During its lifecycle voting outputs several messages to players about its
/// current state.
/// This struct contains all such messages.
struct VotingAnnouncementSet {
/// Message that is displayed when voting starts, inviting others to vote
/// (e.g. "Voting to end trader has started")
var public Text started;
/// Message that is displayed once voting succeeds
/// (e.g. "{$TextPositive Voting to end trader was successful}")
var public Text succeeded;
/// Message that is displayed once voting succeeds
/// (e.g. "{$TextNegative Voting to end trader has failed}")
var public Text failed;
/// Message that is displayed when voting info is displayed mid-voting.
/// (e.g. "Voting to end trader currently active.")
var public Text info;
};
/// Variable that contains current messages that voting will use to communicate
/// its status to the players.
///
/// It is auto-filled with `string` values [`votingStartedLine`],
/// [`votingSucceededLine`], [`votingFailedLine`], [`votingInfoLine`].
/// If you want to change/customize these values based on voting arguments,
/// then override [`HandleVotingStart()`] method and set values inside of this
/// variable directly (they will all be `none` at this point).
var protected VotingAnnouncementSet currentAnnouncements;
/// Preferred name of this voting. Actual name is decided by
/// server owner/mod author.
///
/// Has to satisfy limitations described in the `BaseText::IsValidName()`
var protected const string preferredName;
/// Text that should be displayed when voting starts.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "Voting to end trader has started" line, avoid adding formatting and
/// don't add comma/exclamation mark at the end.
var protected const string votingStartedLine;
/// Text that should be displayed when voting has ended with a success.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "{$TextPositive Voting to end trader was successful}" line, coloring
/// it in a positive color and adding comma/exclamation mark at the end.
var protected const string votingSucceededLine;
/// Text that should be displayed when voting has ended in a failure.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "{$TextNegative Voting to end trader has failed}" line, coloring it in
/// a negative color and adding comma/exclamation mark at the end.
var protected const string votingFailedLine;
/// Text that should be displayed when voting info is displayed mid-voting.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "Voting to end trader is currently active." line, avoid adding
/// formatting and don't add comma/exclamation mark at the end.
var protected const string votingInfoLine;
/// Settings variable that defines a class to be used for this [`Voting`]'s
/// permissions config.
var protected const class<VotingPermissions> permissionsConfigClass;
/*******************************************************************************
* Variables that describe current state of the voting.
******************************************************************************/
/// Underlying voting model that does actual vote calculations.
var private VotingModel model;
/// How much time remains in the voting.
/// Both negative and zero values mean that countdown either ended or wasn't
/// started to begin with.
/// This value can only be decreased inside [`TryAnnounceTimer()`] event method;
/// voting end due to countdown is also expected to be handled there.
var private float remainingVotingTime;
/// Tracks index of the next timing inside [`announcementTimings`] to announce.
var private int nextTimingToAnnounce;
/// Records whether end of the voting announcement was already made.
var private bool endingHandled;
/// Arguments that this voting was called with.
var private HashTable usedArguments;
var private array<Text> policyAllowedToVoteGroups;
var private array<Text> policyAllowedToSeeVotingGroups;
var private array<Text> policyAllowedToForceVoting;
var private bool policySpectatorsCanVote;
/// Fake voters that are only used in debug mode to allow for simpler vote
/// testing.
var private array<UserID> debugVoters;
// Timings at which to announce how much time is left for this voting
var private const array<int> announcementTimings;
/*******************************************************************************
* Auxiliary variables (`string`s + templates from them) used for producing
* output to the user.
******************************************************************************/
/// Text that serves as a template for announcing current vote counts.
var private const string voteSummaryTemplateString;
/// Text that serves as a template for announcing player making a new vote.
var private const string playerVotedTemplateString, playerVotedAnonymousTemplateString;
var private const string timeRemaningAnnounceTemplateString;
// [`TextTemplate`]s made from the above `string` templates.
var private TextTemplate voteSummaryTemplate, playerVotedTemplate, playerVotedAnonymousTemplate;
var private TextTemplate timeRemaningAnnounceTemplate;
/// Text that is used instead of how to vote hint for players not allowed
/// to vote
var private const string cannotVoteHint;
/*******************************************************************************
* Signals.
******************************************************************************/
var private CommandsAPI_OnVotingEnded_Signal onVotingEndedSignal;
protected function Constructor() {
nextTimingToAnnounce = 0;
voteSummaryTemplate = _.text.MakeTemplate_S(voteSummaryTemplateString);
playerVotedTemplate = _.text.MakeTemplate_S(playerVotedTemplateString);
playerVotedAnonymousTemplate = _.text.MakeTemplate_S(playerVotedAnonymousTemplateString);
timeRemaningAnnounceTemplate = _.text.MakeTemplate_S(timeRemaningAnnounceTemplateString);
onVotingEndedSignal = CommandsAPI_OnVotingEnded_Signal(
_.memory.Allocate(class'CommandsAPI_OnVotingEnded_Signal'));
}
protected function Finalizer() {
_.memory.Free(model);
model = none;
endingHandled = false;
policySpectatorsCanVote = false;
_.memory.Free2(usedArguments, onVotingEndedSignal);
usedArguments = none;
onVotingEndedSignal = none;
_server.unreal.OnTick(self).Disconnect();
_.memory.Free4(currentAnnouncements.started,
currentAnnouncements.succeeded,
currentAnnouncements.failed,
currentAnnouncements.info);
currentAnnouncements.started = none;
currentAnnouncements.succeeded = none;
currentAnnouncements.failed = none;
currentAnnouncements.info = none;
_.memory.Free4(voteSummaryTemplate,
playerVotedTemplate,
playerVotedAnonymousTemplate,
timeRemaningAnnounceTemplate);
voteSummaryTemplate = none;
playerVotedTemplate = none;
playerVotedAnonymousTemplate = none;
timeRemaningAnnounceTemplate = none;
_.memory.FreeMany(policyAllowedToVoteGroups);
_.memory.FreeMany(policyAllowedToSeeVotingGroups);
_.memory.FreeMany(policyAllowedToForceVoting);
policyAllowedToVoteGroups.length = 0;
policyAllowedToSeeVotingGroups.length = 0;
policyAllowedToForceVoting.length = 0;
}
/// Signal that will be emitted when voting ends.
///
/// # Slot description
///
/// bool <slot>(bool success, HashTable arguments)
///
/// ## Parameters
///
/// * [`success`]: `true` if voting ended successfully and `false` otherwise.
/// * [`arguments`]: Arguments with which voting was called.
public /*signal*/ function CommandsAPI_OnVotingEnded_Slot OnVotingEnded(AcediaObject receiver) {
return CommandsAPI_OnVotingEnded_Slot(onVotingEndedSignal.NewSlot(receiver));
}
/// Override this to specify arguments for your voting command.
///
/// This method is for adding arguments only.
/// DO NOT call [`CommandDataBuilder::SubCommand()`] or
/// [`CommandDataBuilder::Option()`] methods, otherwise you'll cause unexpected
/// behavior for your mod's users.
public static function AddInfo(CommandDataBuilder builder) {
}
/// Loads permissions config with a given name for the caller [`Voting`] class.
///
/// Permission configs describe allowed usage of the [`Voting`].
/// Basic settings are contained inside [`VotingPermissions`], but votings
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Voting`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function VotingPermissions LoadConfig(BaseText configName) {
if (configName == none) return none;
if (default.permissionsConfigClass == none) return none;
default.permissionsConfigClass.static.Initialize();
// This creates default config if it is missing
default.permissionsConfigClass.static.NewConfig(configName);
return VotingPermissions(default.permissionsConfigClass.static
.GetConfigInstance(configName));
}
/// Returns name of this voting in the lower case.
///
/// If voting class was configured incorrectly (with a `preferredName`
/// that doesn't satisfy limitations, described in `BaseText::IsValidName()`),
/// then this method will return `none`.
public final static function Text GetPreferredName() {
local Text result;
result = __().text.FromString(Locs(default.preferredName));
if (result.IsValidName()) {
return result;
}
__().memory.Free(result);
return none;
}
/// Forcibly ends the voting, deciding winner depending on the argument.
/// By default decides result by the votes that already have been cast.
///
/// Only does anything if voting is currently in progress
/// (in `VPM_InProgress` state).
public final function ForceEndingOutcome ForceEnding(
EPlayer instigator,
VotingModel.ForceEndingType type
) {
local int i;
local UserID id;
local bool canForce;
if (model == none) return FEO_NotApplicable;
if (instigator == none) return FEO_Forbidden;
id = instigator.GetUserID();
if (id == none) return FEO_Forbidden;
for (i = 0; i < policyAllowedToForceVoting.length; i += 1) {
if (_.users.IsUserIDInGroup(id, policyAllowedToForceVoting[i])) {
canForce = true;
break;
}
}
if (canForce) {
if (model.ForceEnding(type)) {
TryEnding(instigator);
return FEO_Success;
}
return FEO_NotApplicable;
}
_.memory.Free(id);
return FEO_Forbidden;
}
/// Starts caller [`Voting`] using policies, loaded from the given config.
///
/// Provided config instance must not be `none`, otherwise method is guaranteed
/// to fail with `SVR_InvalidState`.
/// Method will also fail if voting was already started (even if it already
/// ended), there is no one eligible to vote or [`Voting`] itself has decided to
/// reject being started at this moment, with given arguments.
public final function CommandAPI.StartVotingResult Start(
VotingPermissions config,
HashTable arguments
) {
local bool hasDebugVoters;
local array<EPlayer> voters;
if (model != none) return SVR_InvalidState;
if (config == none) return SVR_InvalidState;
// Check whether we even have enough voters
ReadConfigIntoPolicies(config); // we need to know permission policies
voters = FindAllVotingPlayers();
hasDebugVoters = _.environment.IsDebugging()
&& class'ACommandFakers'.static.BorrowDebugVoters().length > 0;
if (voters.length == 0 && !hasDebugVoters) {
return SVR_NoVoters;
}
// Check if voting even wants to start with these arguments
if (HandleVotingStart(config, arguments)) {
// ^ this was supposed to pre-fill `currentAnnouncements` struct if
// it wanted to change any messages, so now is the good time to fill
// the rest with defaults/fallback
FillAnnouncementGaps();
if (arguments != none) {
arguments.NewRef();
usedArguments = arguments;
}
} else {
_.memory.FreeMany(voters);
return SVR_Rejected;
}
// Actually start voting
model = VotingModel(_.memory.Allocate(class'VotingModel'));
model.Start(config.drawEqualsSuccess);
// Inform new voting about fake voters, in case we're debugging
if (hasDebugVoters) {
// This method will call also `UpdateVoters()`
SetDebugVoters(class'ACommandFakers'.static.BorrowDebugVoters());
} else {
UpdateVoters(voters);
}
SetupCountdownTimer(config);
AnnounceStart();
_.memory.FreeMany(voters);
return SVR_Success;
}
/// Checks if the [`Voting`] process has reached its conclusion.
///
/// Please note that this differs from determining whether voting is currently
// active. Even voting that hasn't started is not considered concluded.
public final function bool HasEnded() {
if (model == none) {
return false;
}
return model.HasEnded();
}
/// Retrieves the current voting status for the specified voter.
///
/// If the voter was previously eligible to vote, cast a vote, but later had
/// their voting rights revoked, their vote will not be counted, and this method
/// will return [`PVS_NoVote`].
///
/// In case the voter regains their voting rights while the voting process is
/// still ongoing, their previous vote will be automatically reinstated by
/// the caller [`Voting`].
public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) {
if (model != none) {
return model.GetVote(voter);
}
return PVS_NoVote;
}
/// Adds specified [`UserID`]s as additional voters in debug mode.
///
/// This method is intended for debugging purposes and only functions when
/// the game is running in debug mode.
public final function SetDebugVoters(array<UserID> newDebugVoters) {
local int i;
local array<EPlayer> realVoters;
if(!_.environment.IsDebugging()) {
return;
}
_.memory.FreeMany(debugVoters);
debugVoters.length = 0;
for (i = 0; i < newDebugVoters.length; i += 1) {
if (newDebugVoters[i] != none) {
debugVoters[debugVoters.length] = newDebugVoters[i];
newDebugVoters[i].NewRef();
}
}
realVoters = FindAllVotingPlayers();
UpdateVoters(realVoters);
_.memory.FreeMany(realVoters);
TryEnding();
}
/// Adds a new vote by a given [`UserID`].
///
/// NOTE: this method is intended for use only in debug mode, and is will not do
/// anything otherwise. This method silently adds a vote using the provided
/// [`UserID`], without any prompt or notification of updated voting status.
/// It was added to facilitate testing with fake [`UserID`]s, and is limited
/// to debug mode to prevent misuse and unintended behavior in production code.
public final function VotingModel.VotingResult CastVoteByID(UserID voter, bool voteForSuccess) {
local array<EPlayer> realVoters;
local VotingModel.VotingResult result;
if (model == none) return VFR_NotAllowed;
if (voter == none) return VFR_NotAllowed;
if (!_.environment.IsDebugging()) return VFR_NotAllowed;
realVoters = FindAllVotingPlayers();
UpdateVoters(realVoters);
result = model.CastVote(voter, voteForSuccess);
if (result == VFR_Success) {
AnnounceNewVote(none, voteForSuccess);
}
TryEnding();
_.memory.FreeMany(realVoters);
return result;
}
/// Registers a vote on behalf of the specified player.
///
/// This method updates the voting status for the specified player and may
/// initiate the conclusion of the voting process.
/// After a vote is registered, the updated voting status is broadcast to all
/// players.
public final function VotingModel.VotingResult CastVote(EPlayer voter, bool voteForSuccess) {
local UserID voterID;
local array<EPlayer> realVoters;
local VotingModel.VotingResult result;
if (model == none) return VFR_NotAllowed;
if (voter == none) return VFR_NotAllowed;
voterID = voter.GetUserID();
realVoters = FindAllVotingPlayers();
UpdateVoters(realVoters);
result = model.CastVote(voterID, voteForSuccess);
switch (result) {
case VFR_Success:
AnnounceNewVote(voter, voteForSuccess);
break;
case VFR_NotAllowed:
voter
.BorrowConsole()
.WriteLine(F("You are {$TextNegative not allowed} to vote right now."));
break;
case VFR_CannotChangeVote:
voter.BorrowConsole().WriteLine(F("Changing vote is {$TextNegative forbidden}."));
break;
case VFR_VotingEnded:
voter.BorrowConsole().WriteLine(F("Voting has already {$TextNegative ended}!"));
break;
default:
}
TryEnding();
_.memory.Free(voterID);
_.memory.FreeMany(realVoters);
return result;
}
/// Prints information about caller [`Voting`] to the given player.
public final function PrintVotingInfoFor(EPlayer requester) {
local ConsoleWriter writer;
local MutableText summaryPart, timeRemaining;
if (requester == none) {
return;
}
voteSummaryTemplate.Reset();
voteSummaryTemplate.ArgInt(model.GetVotesFor());
voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
summaryPart = voteSummaryTemplate.CollectFormattedM();
writer = requester.BorrowConsole();
writer.Write(currentAnnouncements.info);
writer.Write(P(". "));
writer.Write(summaryPart);
writer.WriteLine(P("."));
if (remainingVotingTime > 0) {
timeRemaining = _.text.FromIntM(int(Ceil(remainingVotingTime)));
writer.Write(P("Time remaining: "));
writer.Write(timeRemaining);
writer.WriteLine(P(" seconds."));
_.memory.Free(timeRemaining);
}
_.memory.Free(summaryPart);
}
/// Override this to perform necessary logic after voting has succeeded.
protected function Execute(HashTable arguments) {}
/// Override this method to:
///
/// 1. Specify any of the messages inside `currentAnnouncements` to fit passed
/// [`arguments`].
/// 2. Optionally reject starting this voting altogether by returning `false`
/// (returning `true` will allow voting to proceed).
protected function bool HandleVotingStart(VotingPermissions config, HashTable arguments) {
return true;
}
// Assembles "Say {$TextPositive !yes} or {$TextNegative !no} to vote" hint.
// Replaces "!yes"/"!no" with "!vote yes"/"!vote no" if corresponding aliases
// aren't properly setup.
private final function MutableText MakeHowToVoteHint() {
local Text resolvedAlias;
local MutableText result;
result = P("Say ").MutableCopy();
resolvedAlias = _.alias.ResolveCommand(P("yes"));
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.yes"), SCASE_SENSITIVE)) {
result.Append(P("!yes"), _.text.FormattingFromColor(_.color.TextPositive));
} else {
result.Append(P("!vote yes"), _.text.FormattingFromColor(_.color.TextPositive));
}
_.memory.Free(resolvedAlias);
result.Append(P(" or "));
resolvedAlias = _.alias.ResolveCommand(P("no"));
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.no"), SCASE_SENSITIVE)) {
result.Append(P("!no"), _.text.FormattingFromColor(_.color.TextNegative));
} else {
result.Append(P("!vote no"), _.text.FormattingFromColor(_.color.TextNegative));
}
_.memory.Free(resolvedAlias);
result.Append(P(" to vote"));
return result;
}
private final function ReadConfigIntoPolicies(VotingPermissions config) {
local int i;
if (config != none) {
policySpectatorsCanVote = config.allowSpectatorVoting;
for (i = 0; i < config.allowedToVoteGroup.length; i += 1) {
policyAllowedToVoteGroups[i] = _.text.FromString(config.allowedToVoteGroup[i]);
}
for (i = 0; i < config.allowedToSeeVotesGroup.length; i += 1) {
policyAllowedToSeeVotingGroups[i] = _.text.FromString(config.allowedToSeeVotesGroup[i]);
}
for (i = 0; i < config.allowedToForceGroup.length; i += 1) {
policyAllowedToForceVoting[i] = _.text.FromString(config.allowedToForceGroup[i]);
}
}
}
private final function SetupCountdownTimer(VotingPermissions config) {
if (config != none && config.votingTime > 0) {
remainingVotingTime = config.votingTime;
_server.unreal.OnTick(self).connect = TryAnnounceTimer;
nextTimingToAnnounce = 0;
while (nextTimingToAnnounce < announcementTimings.length) {
if (announcementTimings[nextTimingToAnnounce] <= remainingVotingTime) {
break;
}
nextTimingToAnnounce += 1;
}
}
}
private function TryAnnounceTimer(float delta, float dilationCoefficient) {
local MutableText message;
local ConsoleWriter writer;
if (remainingVotingTime <= 0) {
return;
}
remainingVotingTime -= delta / dilationCoefficient;
if (remainingVotingTime <= 0) {
model.ForceEnding();
TryEnding();
return;
}
if (nextTimingToAnnounce >= announcementTimings.length) {
return;
}
if (announcementTimings[nextTimingToAnnounce] > int(remainingVotingTime)) {
writer = _.console.ForAll();
timeRemaningAnnounceTemplate.Reset();
timeRemaningAnnounceTemplate.ArgInt(announcementTimings[nextTimingToAnnounce]);
message = timeRemaningAnnounceTemplate.CollectFormattedM();
writer.WriteLine(message);
_.memory.Free(writer);
nextTimingToAnnounce += 1;
}
}
/// Outputs message about new vote being submitted to all relevant voters.
private final function AnnounceNewVote(EPlayer voter, bool voteForSuccess) {
local int i, j;
local bool playerAllowedToSee;
local Text voterName;
local array<EPlayer> allPlayers;
local UserID nextID;
local MutableText playerVotedPart, playerVotedAnonymousPart, summaryPart;
voteSummaryTemplate.Reset();
voteSummaryTemplate.ArgInt(model.GetVotesFor());
voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
summaryPart = voteSummaryTemplate.CollectFormattedM();
playerVotedTemplate.Reset();
playerVotedAnonymousTemplate.Reset();
if (voter != none) {
voterName = voter.GetName();
} else {
voterName = P("DEBUG:FAKER").Copy();
}
playerVotedTemplate.TextArg(P("player_name"), voterName, true);
_.memory.Free(voterName);
if (voteForSuccess) {
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextPositive for}"));
playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextPositive for}"));
} else {
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextNegative against}"));
playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextNegative against}"));
}
playerVotedPart = playerVotedTemplate.CollectFormattedM();
playerVotedAnonymousPart = playerVotedAnonymousTemplate.CollectFormattedM();
allPlayers = _.players.GetAll();
for (i = 0; i < allPlayers.length; i += 1) {
nextID = allPlayers[i].GetUserID();
playerAllowedToSee = false;
for (j = 0; j < policyAllowedToSeeVotingGroups.length; j += 1) {
if (_.users.IsUserIDInGroup(nextID, policyAllowedToSeeVotingGroups[j])) {
playerAllowedToSee = true;
break;
}
}
if (playerAllowedToSee) {
allPlayers[i].BorrowConsole().Write(playerVotedPart);
} else {
allPlayers[i].BorrowConsole().Write(playerVotedAnonymousPart);
}
allPlayers[i].BorrowConsole().Write(P(". ")).Write(summaryPart).WriteLine(P("."));
_.memory.Free(nextID);
}
_.memory.Free3(playerVotedPart, playerVotedAnonymousPart, summaryPart);
_.memory.FreeMany(allPlayers);
}
/// Tries to end voting.
///
/// Returns `true` iff this method was called for the first time after
/// the voting concluded.
private final function bool TryEnding(optional EPlayer forcedBy) {
local Text outcomeMessage;
if (model == none) return false;
if (endingHandled) return false;
if (!HasEnded()) return false;
endingHandled = true;
if (model.GetStatus() == VPM_Success) {
outcomeMessage = currentAnnouncements.succeeded;
} else {
outcomeMessage = currentAnnouncements.failed;
}
onVotingEndedSignal.Emit(model.GetStatus() == VPM_Success, usedArguments);
AnnounceOutcome(outcomeMessage, forcedBy);
if (model.GetStatus() == VPM_Success) {
Execute(usedArguments);
}
_server.unreal.OnTick(self).Disconnect();
return true;
}
private final function FillAnnouncementGaps() {
if (currentAnnouncements.started == none) {
currentAnnouncements.started = _.text.FromFormattedString(votingStartedLine);
}
if (currentAnnouncements.succeeded == none) {
currentAnnouncements.succeeded = _.text.FromFormattedString(votingSucceededLine);
}
if (currentAnnouncements.failed == none) {
currentAnnouncements.failed = _.text.FromFormattedString(votingFailedLine);
}
if (currentAnnouncements.info == none) {
currentAnnouncements.info = _.text.FromFormattedString(votingInfoLine);
}
}
private final function array<EPlayer> FindAllVotingPlayers() {
local int i, j;
local bool userAllowedToVote;
local UserID nextID;
local array<EPlayer> currentPlayers, voterPlayers;
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
if (!policySpectatorsCanVote && currentPlayers[i].IsSpectator()) {
continue;
}
nextID = currentPlayers[i].GetUserID();
userAllowedToVote = false;
for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) {
if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) {
userAllowedToVote = true;
break;
}
}
if (userAllowedToVote) {
currentPlayers[i].NewRef();
voterPlayers[voterPlayers.length] = currentPlayers[i];
}
_.memory.Free(nextID);
}
_.memory.FreeMany(currentPlayers);
return voterPlayers;
}
/// Updates the inner voting model with current list of players allowed to vote.
/// Also returns said list.
private final function UpdateVoters(array<EPlayer> votingPlayers) {
local int i;
local array<UserID> votersIDs;
if (model == none) {
return;
}
for (i = 0; i < votingPlayers.length; i += 1) {
votersIDs[votersIDs.length] = votingPlayers[i].GetUserID();
}
for (i = 0; i < debugVoters.length; i += 1) {
debugVoters[i].NewRef();
votersIDs[votersIDs.length] = debugVoters[i];
}
model.UpdatePotentialVoters(votersIDs);
_.memory.FreeMany(votersIDs);
}
/// Prints given voting outcome message in console and publishes it as
/// a notification.
private final function AnnounceStart() {
local int i, j;
local bool playerAllowedToSee;
local UserID nextID;
local MutableText howToVoteHint;
local array<EPlayer> currentPlayers;
howToVoteHint = MakeHowToVoteHint();
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
nextID = currentPlayers[i].GetUserID();
playerAllowedToSee = false;
for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) {
if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) {
playerAllowedToSee = true;
break;
}
}
_.memory.Free(nextID);
if (playerAllowedToSee) {
currentPlayers[i].Notify(currentAnnouncements.started, howToVoteHint,, P("voting"));
currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started);
currentPlayers[i].BorrowConsole().WriteLine(howToVoteHint);
} else {
currentPlayers[i].Notify(currentAnnouncements.started, F(cannotVoteHint),, P("voting"));
currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started);
}
}
_.memory.Free(howToVoteHint);
_.memory.FreeMany(currentPlayers);
}
/// Prints given voting outcome message in console and publishes it as
/// a notification.
private final function AnnounceOutcome(BaseText outcomeMessage, optional EPlayer forcedBy) {
local int i;
local Text playerName;
local MutableText editedOutcomeMessage, summaryLine;
local ConsoleWriter writer;
local array<EPlayer> currentPlayers;
if (model == none) {
return;
}
if (outcomeMessage != none) {
editedOutcomeMessage = outcomeMessage.MutableCopy();
}
if (editedOutcomeMessage != none && forcedBy != none) {
editedOutcomeMessage.Append(F(" {$TextEmphasis (forced by }"));
playerName = forcedBy.GetName();
editedOutcomeMessage.Append(playerName, _.text.FormattingFromColor(_.color.Gray));
_.memory.Free(playerName);
editedOutcomeMessage.Append(F("{$TextEmphasis )}"));
}
voteSummaryTemplate.Reset();
voteSummaryTemplate.ArgInt(model.GetVotesFor());
voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
summaryLine = voteSummaryTemplate.CollectFormattedM();
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
writer = currentPlayers[i].BorrowConsole();
writer.Write(editedOutcomeMessage);
writer.Write(P(" / "));
writer.WriteLine(summaryLine);
currentPlayers[i].Notify(editedOutcomeMessage, summaryLine,, P("voting"));
}
_.memory.FreeMany(currentPlayers);
_.memory.Free(summaryLine);
}
defaultproperties {
// You can override these
preferredName = "test"
votingInfoLine = "Debug voting is running"
votingStartedLine = "Test voting has started"
votingSucceededLine = "{$TextPositive Test voting passed}"
votingFailedLine = "{$TextNegative Test voting has failed}"
permissionsConfigClass = class'VotingPermissions'
// You cannot override these
voteSummaryTemplateString = "Vote tally: {$TextPositive %1} vs {$TextNegative %2}"
playerVotedTemplateString = "Player {$TextSubtle %%player_name%%} has voted %%vote_type%% passing test voting"
playerVotedAnonymousTemplateString = "Someone has voted %%vote_type%% passing test voting"
timeRemaningAnnounceTemplateString = "Time remaining for voting: %1 seconds"
cannotVoteHint = "{$TextNegative You aren't allowed to vote :(}"
announcementTimings(0) = 60
announcementTimings(1) = 30
announcementTimings(2) = 15
announcementTimings(3) = 10
announcementTimings(4) = 5
announcementTimings(5) = 4
announcementTimings(6) = 3
announcementTimings(7) = 2
announcementTimings(8) = 1
}

440
sources/BaseAPI/API/Commands/Voting/VotingModel.uc

@ -0,0 +1,440 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class VotingModel extends AcediaObject
dependsOn(MathApi);
//! This class counts votes according to the configured voting policies.
//!
//! Its main purpose is to separate the voting logic from the voting interface,
//! making the implementation simpler and the logic easier to test.
//!
//! # Usage
//!
//! 1. Allocate an instance of the [`VotingModel`] class.
//! 2. Call [`Start()`] to start voting with required policies.
//! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote.
//! You can change this set at any time before the voting has concluded.
//! The method used to recount the votes will depend on the policies set
//! during the previous [`Initialize()`] call.
//! 4. Use [`CastVote()`] to add a vote from a user.
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`],
//! check [`GetStatus()`] to see if the voting has concluded.
//! Once voting has concluded, the result cannot be changed, so you can
//! release the reference to the [`VotingModel`] object.
//! 6. Alternatively, before voting has concluded naturally, you can use
//! [`ForceEnding()`] method to immediately end voting with result being
//! determined by provided [`ForceEndingType`] argument.
/// Current state of voting for this model.
enum VotingModelStatus {
/// Voting hasn't even started, waiting for [`Initialize()`] call
VPM_Uninitialized,
/// Voting is currently in progress
VPM_InProgress,
/// Voting has ended with majority for its success
VPM_Success,
/// Voting has ended with majority for its failure
VPM_Failure
};
/// A result of user trying to make a vote
enum VotingResult {
/// Vote accepted
VFR_Success,
/// Voting is not allowed for this particular user
VFR_NotAllowed,
/// User already made a vote and changing votes isn't allowed
VFR_CannotChangeVote,
/// User has already voted the same way
VFR_AlreadyVoted,
/// Voting has already ended and doesn't accept new votes
VFR_VotingEnded
};
/// Checks how given user has voted
enum PlayerVoteStatus {
/// User hasn't voted yet
PVS_NoVote,
/// User voted for the change
PVS_VoteFor,
/// User voted against the change
PVS_VoteAgainst
};
/// Types of possible outcomes when forcing a voting to end
enum ForceEndingType {
/// Result will be decided by the votes that already have been cast
FET_CurrentLeader,
/// Voting will end in success
FET_Success,
/// Voting will end in failure
FET_Failure
};
var private VotingModelStatus status;
/// Specifies whether draw would count as a victory for corresponding voting.
var private bool policyDrawWinsVoting;
var private array<UserID> votesFor, votesAgainst;
/// Votes of people that voted before, but then were forbidden to vote
/// (either because they have left or simply lost the right to vote)
var private array<UserID> storedVotesFor, storedVotesAgainst;
/// List of users currently allowed to vote
var private array<UserID> allowedVoters;
protected function Constructor() {
status = VPM_Uninitialized;
}
protected function Finalizer() {
_.memory.FreeMany(allowedVoters);
_.memory.FreeMany(votesFor);
_.memory.FreeMany(votesAgainst);
_.memory.FreeMany(storedVotesFor);
_.memory.FreeMany(storedVotesAgainst);
allowedVoters.length = 0;
votesFor.length = 0;
votesAgainst.length = 0;
storedVotesFor.length = 0;
storedVotesAgainst.length = 0;
}
/// Initializes voting by providing it with a set of policies to follow.
///
/// The only available policy is configuring whether draw means victory or loss
/// in voting.
///
/// Can only be called once, after that will do nothing.
public final function Start(bool drawWinsVoting) {
if (status != VPM_Uninitialized) {
return;
}
policyDrawWinsVoting = drawWinsVoting;
status = VPM_InProgress;
}
/// Returns whether voting has already concluded.
///
/// This method should be checked after both [`CastVote()`] and
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to
/// conclude the voting result.
public final function bool HasEnded() {
return (status != VPM_Uninitialized && status != VPM_InProgress);
}
/// Returns current status of voting.
///
/// This method should be checked after both [`CastVote()`] and
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to
/// conclude the voting result.
public final function VotingModelStatus GetStatus() {
return status;
}
/// Changes set of [`User`]s that are allowed to vote.
///
/// Generally you want to provide this method with a list of current players,
/// optionally filtered from spectators, users not in priviledged group or any
/// other relevant criteria.
public final function UpdatePotentialVoters(array<UserID> potentialVoters) {
local int i;
_.memory.FreeMany(allowedVoters);
allowedVoters.length = 0;
for (i = 0; i < potentialVoters.length; i += 1) {
potentialVoters[i].NewRef();
allowedVoters[i] = potentialVoters[i];
}
RestoreStoredVoters(potentialVoters);
FilterCurrentVoters(potentialVoters);
RecountVotes();
}
/// Attempts to add a vote from specified user.
///
/// Adding a vote can fail if [`voter`] isn't allowed to vote.
public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
local bool votesSameWay;
local PlayerVoteStatus currentVote;
if (status != VPM_InProgress) {
return VFR_VotingEnded;
}
if (!IsVotingAllowedFor(voter)) {
return VFR_NotAllowed;
}
currentVote = GetVote(voter);
votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor)
|| (!voteForSuccess && currentVote == PVS_VoteAgainst);
if (votesSameWay) {
return VFR_AlreadyVoted;
}
EraseVote(voter);
voter.NewRef();
if (voteForSuccess) {
votesFor[votesFor.length] = voter;
} else {
votesAgainst[votesAgainst.length] = voter;
}
RecountVotes();
return VFR_Success;
}
/// Checks if the provided user is allowed to vote based on the current list of
/// potential voters.
///
/// The right to vote is decided solely by the list of potential voters set
/// using [`UpdatePotentialVoters()`].
///
/// Returns true if the user is allowed to vote, false otherwise.
public final function bool IsVotingAllowedFor(UserID voter) {
local int i;
if (voter == none) {
return false;
}
for (i = 0; i < allowedVoters.length; i += 1) {
if (voter.IsEqual(allowedVoters[i])) {
return true;
}
}
return false;
}
/// Returns the current vote status for the given voter.
///
/// If the voter was previously allowed to vote, voted, and had their right to
/// vote revoked, their vote won't count.
public final function PlayerVoteStatus GetVote(UserID voter) {
local int i;
if (voter == none) {
return PVS_NoVote;
}
for (i = 0; i < votesFor.length; i += 1) {
if (voter.IsEqual(votesFor[i])) {
return PVS_VoteFor;
}
}
for (i = 0; i < votesAgainst.length; i += 1) {
if (voter.IsEqual(votesAgainst[i])) {
return PVS_VoteAgainst;
}
}
return PVS_NoVote;
}
/// Returns amount of current valid votes for the success of this voting.
public final function int GetVotesFor() {
return votesFor.length;
}
/// Returns amount of current valid votes against the success of this voting.
public final function int GetVotesAgainst() {
return votesAgainst.length;
}
/// Returns amount of users that are currently allowed to vote in this voting.
public final function int GetTotalPossibleVotes() {
return allowedVoters.length;
}
/// Checks whether, if stopped now, voting will win.
public final function bool IsVotingWinning() {
if (status == VPM_Success) return true;
if (status == VPM_Failure) return false;
if (GetVotesFor() > GetVotesAgainst()) return true;
if (GetVotesFor() < GetVotesAgainst()) return false;
return policyDrawWinsVoting;
}
/// Forcibly ends the voting, deciding winner depending on the argument.
///
/// Only does anything if voting is currently in progress
/// (in `VPM_InProgress` state).
///
/// By default decides result by the votes that already have been cast.
///
/// Returns `true` only if voting was actually ended with this call.
public final function bool ForceEnding(optional ForceEndingType type) {
if (status != VPM_InProgress) {
return false;
}
switch (type) {
case FET_CurrentLeader:
if (IsVotingWinning()) {
status = VPM_Success;
} else {
status = VPM_Failure;
}
break;
case FET_Success:
status = VPM_Success;
break;
case FET_Failure:
default:
status = VPM_Failure;
break;
}
return true;
}
private final function RecountVotes() {
local MathApi.IntegerDivisionResult divisionResult;
local int winningScore, losingScore;
local int totalPossibleVotes;
if (status != VPM_InProgress) {
return;
}
totalPossibleVotes = GetTotalPossibleVotes();
divisionResult = _.math.IntegerDivision(totalPossibleVotes, 2);
if (divisionResult.remainder == 1) {
// For odd amount of voters winning is simply majority
winningScore = divisionResult.quotient + 1;
} else {
if (policyDrawWinsVoting) {
// For even amount of voters, exactly half is enough if draw means victory
winningScore = divisionResult.quotient;
} else {
// Otherwise - majority
winningScore = divisionResult.quotient + 1;
}
}
// The `winningScore` represents the number of votes required for a mean victory.
// If the number of votes against the mean is less than or equal to
// `totalPossibleVotes - winningScore`, then victory is still possible.
// However, if there is even one additional vote against, then victory is no longer achievable
// and a loss is inevitable.
losingScore = (totalPossibleVotes - winningScore) + 1;
// `totalPossibleVotes < losingScore + winningScore`, so only one of these inequalities
// can be satisfied at a time
if (GetVotesFor() >= winningScore) {
status = VPM_Success;
} else if (GetVotesAgainst() >= losingScore) {
status = VPM_Failure;
}
}
private final function EraseVote(UserID voter) {
local int i;
if (voter == none) {
return;
}
while (i < votesFor.length) {
if (voter.IsEqual(votesFor[i])) {
_.memory.Free(votesFor[i]);
votesFor.Remove(i, 1);
} else {
i += 1;
}
}
i = 0;
while (i < votesAgainst.length) {
if (voter.IsEqual(votesAgainst[i])) {
_.memory.Free(votesAgainst[i]);
votesAgainst.Remove(i, 1);
} else {
i += 1;
}
}
}
private final function RestoreStoredVoters(array<UserID> potentialVoters) {
local int i, j;
local bool isPotentialVoter;
while (i < storedVotesFor.length) {
isPotentialVoter = false;
for (j = 0; j < potentialVoters.length; j += 1) {
if (storedVotesFor[i].IsEqual(potentialVoters[j])) {
isPotentialVoter = true;
break;
}
}
if (isPotentialVoter) {
votesFor[votesFor.length] = storedVotesFor[i];
storedVotesFor.Remove(i, 1);
} else {
i += 1;
}
}
i = 0;
while (i < storedVotesAgainst.length) {
isPotentialVoter = false;
for (j = 0; j < potentialVoters.length; j += 1) {
if (storedVotesAgainst[i].IsEqual(potentialVoters[j])) {
isPotentialVoter = true;
break;
}
}
if (isPotentialVoter) {
votesAgainst[votesAgainst.length] = storedVotesAgainst[i];
storedVotesAgainst.Remove(i, 1);
} else {
i += 1;
}
}
}
private final function FilterCurrentVoters(array<UserID> potentialVoters) {
local int i, j;
local bool isPotentialVoter;
while (i < votesFor.length) {
isPotentialVoter = false;
for (j = 0; j < potentialVoters.length; j += 1) {
if (votesFor[i].IsEqual(potentialVoters[j])) {
isPotentialVoter = true;
break;
}
}
if (isPotentialVoter) {
i += 1;
} else {
storedVotesFor[storedVotesFor.length] = votesFor[i];
votesFor.Remove(i, 1);
}
}
i = 0;
while (i < votesAgainst.length) {
isPotentialVoter = false;
for (j = 0; j < potentialVoters.length; j += 1) {
if (votesAgainst[i].IsEqual(potentialVoters[j])) {
isPotentialVoter = true;
break;
}
}
if (isPotentialVoter) {
i += 1;
} else {
storedVotesAgainst[storedVotesAgainst.length] = votesAgainst[i];
votesAgainst.Remove(i, 1);
}
}
}
defaultproperties {
}

132
sources/BaseAPI/API/Commands/Voting/VotingPermissions.uc

@ -0,0 +1,132 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class VotingPermissions extends AcediaConfig
perobjectconfig
config(AcediaCommands);
/// Determines the duration of the voting period, specified in seconds.
/// Zero or negative values mean unlimited voting period.
var public config float votingTime;
/// Determines whether spectators are allowed to vote.
var public config bool allowSpectatorVoting;
/// Determines how draw will be interpreted.
/// `true` means draw counts as a vote's success, `false` means draw counts as a vote's failure.
var public config bool drawEqualsSuccess;
/// Specifies which group(s) of players are allowed to see who makes what vote.
var public config array<string> allowedToVoteGroup;
/// Specifies which group(s) of players are allowed to see who makes what vote.
var public config array<string> allowedToSeeVotesGroup;
/// Specifies which group(s) of players are allowed to forcibly end voting.
var public config array<string> allowedToForceGroup;
protected function HashTable ToData() {
local int i;
local HashTable data;
local ArrayList arrayOfTexts;
data = __().collections.EmptyHashTable();
data.SetFloat(P("votingTime"), votingTime);
data.SetBool(P("allowSpectatorVoting"), allowSpectatorVoting);
data.SetBool(P("drawEqualsSuccess"), drawEqualsSuccess);
arrayOfTexts = _.collections.EmptyArrayList();
for (i = 0; i < allowedToVoteGroup.length; i += 1) {
arrayOfTexts.AddString(allowedToVoteGroup[i]);
}
data.SetItem(P("allowedToVoteGroup"), arrayOfTexts);
_.memory.Free(arrayOfTexts);
arrayOfTexts = _.collections.EmptyArrayList();
for (i = 0; i < allowedToSeeVotesGroup.length; i += 1) {
arrayOfTexts.AddString(allowedToSeeVotesGroup[i]);
}
data.SetItem(P("allowedToSeeVotesGroup"), arrayOfTexts);
_.memory.Free(arrayOfTexts);
arrayOfTexts = _.collections.EmptyArrayList();
for (i = 0; i < allowedToForceGroup.length; i += 1) {
arrayOfTexts.AddString(allowedToForceGroup[i]);
}
data.SetItem(P("allowedToForceGroup"), arrayOfTexts);
_.memory.Free(arrayOfTexts);
return data;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList arrayOfTexts;
if (source == none) {
return;
}
votingTime = source.GetFloat(P("votingTime"), 30.0);
allowSpectatorVoting = source.GetBool(P("allowSpectatorVoting"), false);
drawEqualsSuccess = source.GetBool(P("drawEqualsSuccess"), false);
allowedToVoteGroup.length = 0;
arrayOfTexts = source.GetArrayList(P("allowedToVoteGroup"));
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) {
allowedToVoteGroup[allowedToVoteGroup.length] = arrayOfTexts.GetString(i);
}
allowedToSeeVotesGroup.length = 0;
arrayOfTexts = source.GetArrayList(P("allowedToSeeVotesGroup"));
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) {
allowedToSeeVotesGroup[allowedToSeeVotesGroup.length] = arrayOfTexts.GetString(i);
}
_.memory.Free(arrayOfTexts);
allowedToForceGroup.length = 0;
arrayOfTexts = source.GetArrayList(P("allowedToForceGroup"));
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) {
allowedToForceGroup[allowedToForceGroup.length] = arrayOfTexts.GetString(i);
}
_.memory.Free(arrayOfTexts);
}
protected function DefaultIt() {
votingTime = 30.0;
drawEqualsSuccess = false;
allowSpectatorVoting = false;
allowedToVoteGroup.length = 0;
allowedToSeeVotesGroup.length = 0;
allowedToForceGroup.length = 0;
allowedToVoteGroup[0] = "all";
allowedToSeeVotesGroup[0] = "all";
allowedToForceGroup[0] = "admin";
allowedToForceGroup[1] = "moderator";
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
votingTime = 30.0
drawEqualsSuccess = false
allowSpectatorVoting = false
allowedToVoteGroup(0) = "all"
allowedToSeeVotesGroup(0) = "all"
allowedToForceGroup(0) = "admin"
allowedToForceGroup(1) = "moderator"
}

654
sources/BaseAPI/API/Math/BigInt.uc

@ -0,0 +1,654 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BigInt extends AcediaObject
dependson(MathApi);
/// A simple big integer implementation.
///
/// [`BigInt`]'s main purpose is to allow Acedia's databases to store integers of arbitrary size.
/// It can be used for long arithmetic computations, but it was mainly meant as a players'
/// statistics counter and, therefore, not optimized for performing large amount of operations.
/// [`BigInt`] data as a struct - meant to be used to store [`BigInt`]'s values inside
/// the local databases.
struct BigIntData {
var bool negative;
var array<byte> digits;
};
/// Result of comparison for [`BigInt`]s with each other.
enum BigIntCompareResult
{
BICR_Less,
BICR_Equal,
BICR_Greater
};
/// Does stored [`BigInt`] have a negative sign?
var private bool negative;
/// Digits array, from least to most significant. For example, for 13524:
///
/// ```
/// `digits[0] = 4`
/// `digits[1] = 2`
/// `digits[2] = 5`
/// `digits[3] = 3`
/// `digits[4] = 1`
/// ```
///
/// Valid [`BigInt`] should not have this array empty: zero should be represented by an array with
/// a single `0`-element.
/// This isn't a most efficient representation for [`BigInt`], but it's easy to convert to and from
/// decimal representation.
///
/// # Invariants
///
/// This array must not have leading (in the sense of significance) zeroes.
/// That is, last element of the array should not be a `0`.
/// The only exception if if stored value is `0`, then `digits` must consist of
/// a single `0` element.
var private array<byte> digits;
/// Constants useful for converting [`BigInt`] back to [`int`], while avoiding overflow.
/// We can add less digits than that without any fear of overflow.
const DIGITS_IN_MAX_INT = 10;
/// Maximum [`int`] value is `2147483647`, so in case most significant digit is 10th and is `2`
/// (so number has a form of `2xxxxxxxxx`), to check for overflow we only need to compare
/// combination of the rest of the digits with this constant.
const ALMOST_MAX_INT = 147483647;
/// To add last digit we add/subtract that digit multiplied by this value.
const LAST_DIGIT_ORDER = 1000000000;
protected function Constructor() {
SetZero();
}
protected function Finalizer() {
negative = false;
digits.length = 0;
}
// Auxiliary method to set current value to zero
private function SetZero() {
negative = false;
digits.length = 1;
digits[0] = 0;
}
// Minimal [`int`] value `-2,147,483,648` is somewhat of a pain to handle, so just use this
// auxiliary pre-made constructor for it
private function SetMinimalNegative() {
negative = true;
digits.length = 10;
digits[0] = 8;
digits[1] = 4;
digits[2] = 6;
digits[3] = 3;
digits[4] = 8;
digits[5] = 4;
digits[6] = 7;
digits[7] = 4;
digits[8] = 1;
digits[9] = 2;
}
// Removes unnecessary zeroes from leading digit positions `digits`.
// Does not change contained value.
private final function TrimLeadingZeroes() {
local int i, zeroesToRemove;
// Finds how many leading zeroes there is.
// Since `digits` stores digits from least to most significant, we need to check from the end of
// `digits` array.
for (i = digits.length - 1; i >= 0; i -= 1) {
if (digits[i] != 0) {
break;
}
zeroesToRemove += 1;
}
// `digits` must not be empty, enforce `0` value in that case
if (zeroesToRemove >= digits.length) {
SetZero();
}
else {
digits.length = digits.length - zeroesToRemove;
}
}
/// Changes current value of [`BigInt`] to given value.
public final function Set(BigInt value)
{
if (value != none) {
value.TrimLeadingZeroes();
digits = value.digits;
negative = value.negative;
}
}
/// Changes current value of [`BigInt`] to given value.
public final function SetInt(int value) {
local MathApi.IntegerDivisionResult divisionResult;
negative = false;
digits.length = 0;
if (value < 0) {
// Treat special case of minimal [`int`] value `-2,147,483,648` that
// won't fit into positive [`int`] as special and use pre-made
// specialized constructor `CreateMinimalNegative()`
if (value < -maxInt) {
SetMinimalNegative();
return;
} else {
negative = true;
value *= -1;
}
}
if (value == 0) {
digits[0] = 0;
} else {
while (value > 0) {
divisionResult = __().math.IntegerDivision(value, 10);
value = divisionResult.quotient;
digits[digits.length] = divisionResult.remainder;
}
}
TrimLeadingZeroes();
}
/// Changes current value of [`BigInt`] to the value, given by decimal representation.
///
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given
/// as an argument, caller's value won't change.
/// Returns `true` in case of success and `false` otherwise.
public final function bool SetDecimal(BaseText value) {
local int i;
local byte nextDigit;
local bool newNegative;
local array<byte> newDigits;
local Parser parser;
local Basetext.Character nextCharacter;
if (value == none) {
return false;
}
parser = value.Parse();
newNegative = ParseSign(parser);
// Reset to valid state whether sign was consumed or not
parser.Confirm();
parser.R();
newDigits.length = parser.GetRemainingLength();
// Parse new one
i = newDigits.length - 1;
while (!parser.HasFinished()){
// This should not happen, but just in case
if (i < 0) {
break;
}
parser.MCharacter(nextCharacter);
nextDigit = __().text.CharacterToInt(nextCharacter, 10);
if (nextDigit < 0) {
return false;
}
newDigits[i] = nextDigit;
i -= 1;
}
parser.FreeSelf();
digits = newDigits;
negative = newNegative;
TrimLeadingZeroes();
return true;
}
// Tries to parse either `+` or `-` and returns `true` iff it parsed `-`.
// If neither got parsed, `parser` will enter failed state.
// Assumes `parser` isn't `none`.
private final function bool ParseSign(Parser parser) {
parser.Match(P("-"));
negative = parser.Ok();
if (parser.Ok()) {
negative = true;
}
else {
parser.R();
parser.Match(P("+"));
}
return negative;
}
/// Changes current value of [`BigInt`] to the value, given by decimal representation.
///
/// If invalid decimal representation (digits only, possibly with leading sign) is given as
/// an argument, caller's value won't change.
/// Returns `true` in case of success and `false` otherwise.
public final function bool SetDecimal_S(string value) {
local bool result;
local MutableText wrapper;
wrapper = __().text.FromStringM(value);
result = SetDecimal(wrapper);
wrapper.FreeSelf();
return result;
}
// Auxiliary method for comparing two [`BigInt`]s by their absolute value.
private function BigIntCompareResult _compareAbsolute(BigInt other) {
local int i;
local array<byte> otherDigits;
otherDigits = other.digits;
if (digits.length == otherDigits.length)
{
for (i = digits.length - 1; i >= 0; i -= 1)
{
if (digits[i] < otherDigits[i]) {
return BICR_Less;
}
if (digits[i] > otherDigits[i]) {
return BICR_Greater;
}
}
return BICR_Equal;
}
if (digits.length < otherDigits.length) {
return BICR_Less;
}
return BICR_Greater;
}
/// Compares caller [`BigInt`] to [`other`].
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
/// If argument is `none`, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
public function BigIntCompareResult Compare(BigInt other) {
local BigIntCompareResult resultForModulus;
if (other == none) return BICR_Less;
if (negative && !other.negative) return BICR_Less;
if (!negative && other.negative) return BICR_Greater;
resultForModulus = _compareAbsolute(other);
if (resultForModulus == BICR_Equal) return BICR_Equal;
if (negative && (resultForModulus == BICR_Greater)) return BICR_Less;
if (!negative && (resultForModulus == BICR_Less)) return BICR_Less;
return BICR_Greater;
}
/// Compares caller [`BigInt`] to [`other`].
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
public function BigIntCompareResult CompareInt(int other) {
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.ToBigInt(other);
result = Compare(wrapper);
wrapper.FreeSelf();
return result;
}
/// Compares caller [`BigInt`] to a decimal representation of a number.
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
/// If argument is `none` or is an invalid decimal representation (digits only, possibly with
/// leading sign, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
public function BigIntCompareResult CompareDecimal(BaseText other) {
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.MakeBigInt(other);
result = Compare(wrapper);
_.memory.Free(wrapper);
return result;
}
/// Compares caller [`BigInt`] to a decimal representation of a number.
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
/// If argument is an invalid decimal representation (digits only, possibly with leading sign, then
/// it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
public function BigIntCompareResult CompareDecimal_S(string other) {
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.MakeBigInt_S(other);
result = Compare(wrapper);
wrapper.FreeSelf();
return result;
}
// Adds absolute values of caller [`BigInt`] and [`other`] with no changes to the sign.
private function _add(BigInt other) {
local int i;
local byte carry, digitSum;
local array<byte> otherDigits;
if (other == none) {
return;
}
otherDigits = other.digits;
if (digits.length < otherDigits.length) {
digits.length = otherDigits.length;
} else {
otherDigits.length = digits.length;
}
carry = 0;
for (i = 0; i < digits.length; i += 1) {
digitSum = digits[i] + otherDigits[i] + carry;
digits[i] = _.math.Remainder(digitSum, 10);
carry = (digitSum - digits[i]) / 10;
}
if (carry > 0) {
digits[digits.length] = carry;
}
// No leading zeroes can be created here, so no need to trim
}
// Subtracts absolute value of [`other`] from the caller [`BigInt`], flipping caller's sign in case
// `other`'s absolute value is bigger.
private function _sub(BigInt other) {
local int i;
local int carry, nextDigit;
local array<byte> minuendDigits, subtrahendDigits;
local BigIntCompareResult resultForModulus;
if (other == none) {
return;
}
resultForModulus = _compareAbsolute(other);
if (resultForModulus == BICR_Equal) {
SetZero();
return;
}
if (resultForModulus == BICR_Less) {
negative = !negative;
minuendDigits = other.digits;
subtrahendDigits = digits;
} else {
minuendDigits = digits;
subtrahendDigits = other.digits;
}
digits.length = minuendDigits.length;
subtrahendDigits.length = minuendDigits.length;
carry = 0;
for (i = 0; i < digits.length; i += 1) {
nextDigit = int(minuendDigits[i]) - int(subtrahendDigits[i]) + carry;
if (nextDigit < 0) {
nextDigit += 10;
carry = -1;
} else {
carry = 0;
}
digits[i] = nextDigit;
}
TrimLeadingZeroes();
}
/// Adds another value to the caller [`BigInt`].
///
/// If argument is `none`, then given method does nothing.
public function Add(BigInt other) {
if (other == none) {
return;
}
if (negative == other.negative) {
_add(other);
} else {
_sub(other);
}
}
/// Adds another value to the caller [`BigInt`].
public function AddInt(int other) {
local BigInt otherObject;
otherObject = _.math.ToBigInt(other);
Add(otherObject);
_.memory.Free(otherObject);
}
/// Adds decimal representation of the number to the caller [`BigInt`].
///
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function AddDecimal(BaseText other) {
local BigInt otherObject;
if (other == none) {
return;
}
otherObject = _.math.MakeBigInt(other);
Add(otherObject);
_.memory.Free(otherObject);
}
/// Adds decimal representation of the number to the caller [`BigInt`].
///
/// If invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function AddDecimal_S(string other) {
local BigInt otherObject;
otherObject = _.math.MakeBigInt_S(other);
Add(otherObject);
_.memory.Free(otherObject);
}
/// Subtracts another value to the caller [`BigInt`].
///
/// If argument is `none`, then given method does nothing.
public function Subtract(BigInt other) {
if (negative != other.negative) {
_add(other);
} else {
_sub(other);
}
}
/// Adds another value to the caller [`BigInt`].
public function SubtractInt(int other) {
local BigInt otherObject;
otherObject = _.math.ToBigInt(other);
Subtract(otherObject);
_.memory.Free(otherObject);
}
/// Subtracts decimal representation of the number to the caller [`BigInt`].
///
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function SubtractDecimal(BaseText other) {
local BigInt otherObject;
if (other == none) {
return;
}
otherObject = _.math.MakeBigInt(other);
Subtract(otherObject);
_.memory.Free(otherObject);
}
/// Adds decimal representation of the number to the caller [`BigInt`].
///
/// If invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function SubtractDecimal_S(string other) {
local BigInt otherObject;
otherObject = _.math.MakeBigInt_S(other);
Subtract(otherObject);
_.memory.Free(otherObject);
}
/// Checks if caller [`BigInt`] is negative.
///
/// Returns if stored value is negative and `false` otherwise.
/// Zero is not considered negative number.
public function bool IsNegative() {
// Handle special case of zero first (it ignores `negative` flag)
if (digits.length == 1 && digits[0] == 0) {
return false;
}
return negative;
}
/// Converts caller [`BigInt`] into [`int`] representation.
///
/// In case stored value is outside `int`'s value range
/// (`[-maxInt-1, maxInt] == [-2147483648; 2147483647]`), method returns either maximal or minimal
// possible value, depending on the [`BigInt`]'s sign.
public function int ToInt() {
local int i;
local int accumulator;
local int safeDigitsAmount;
if (digits.length <= 0) {
return 0;
}
if (digits.length > DIGITS_IN_MAX_INT) {
if (negative) {
return (-maxInt - 1);
} else {
return maxInt;
}
}
// At most `DIGITS_IN_MAX_INT - 1` iterations
safeDigitsAmount = Min(DIGITS_IN_MAX_INT - 1, digits.length);
for (i = safeDigitsAmount - 1; i >= 0; i -= 1) {
accumulator *= 10;
accumulator += digits[i];
}
if (negative) {
accumulator *= -1;
}
accumulator = AddUnsafeDigitToInt(accumulator);
return accumulator;
}
/// Adding `DIGITS_IN_MAX_INT - 1` will never lead to an overflow, but adding the next digit can,
/// so we need to handle it differently and more carefully.
/// Assumes `digits.length <= DIGITS_IN_MAX_INT`.
private function int AddUnsafeDigitToInt(int accumulator) {
local int unsafeDigit;
local bool noOverflow;
if (digits.length < DIGITS_IN_MAX_INT) {
return accumulator;
}
unsafeDigit = digits[DIGITS_IN_MAX_INT - 1];
// `maxInt` stats with `2`, so if last/unsafe digit is either `0` or `1`, there is no overflow,
// otherwise - check rest of the digits
noOverflow = (unsafeDigit < 2);
if (unsafeDigit == 2) {
// Include `maxInt` and `-maxInt-1` (minimal possible value) into an overflow too - this way
// we still give a correct result, but do not have to worry about `int`-arithmetic error
noOverflow = noOverflow
|| (negative && (accumulator > -ALMOST_MAX_INT - 1))
|| (!negative && (accumulator < ALMOST_MAX_INT));
}
if (noOverflow) {
if (negative) {
accumulator -= unsafeDigit * LAST_DIGIT_ORDER;
}
else {
accumulator += unsafeDigit * LAST_DIGIT_ORDER;
}
return accumulator;
}
// Handle overflow
if (negative) {
return (-maxInt - 1);
}
return maxInt;
}
/// Converts caller [`BigInt`] into [`Text`] representation.
public function Text ToText() {
return ToText_M().IntoText();
}
/// Converts caller [`BigInt`] into [`MutableText`] representation.
public function MutableText ToText_M() {
local int i;
local MutableText result;
result = _.text.Empty();
if (negative) {
result.AppendCharacter(_.text.GetCharacter("-"));
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48));
}
return result;
}
/// Converts caller [`BigInt`] into [`string`] representation.
public function string ToString() {
local int i;
local string result;
if (negative) {
result = "-";
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result = result $ digits[i];
}
return result;
}
/// Restores [`BigInt`] from the [`BigIntData`] value.
///
/// This method is created to make an efficient way to store [`BigInt`].
public function FromData(BigIntData data) {
local int i;
negative = data.negative;
digits = data.digits;
// Deal with possibly erroneous data
for (i = 0; i < digits.length; i += 1) {
if (digits[i] > 9) {
digits[i] = 9;
}
}
}
/// Converts caller [`BigInt`]'s value into [`BigIntData`].
///
/// This method is created to make an efficient way to store [`BigInt`].
public function BigIntData ToData() {
local BigIntData result;
result.negative = negative;
result.digits = digits;
return result;
}
defaultproperties {
}

104
sources/BaseAPI/API/Math/MathAPI.uc

@ -0,0 +1,104 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2023 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
*/
class MathApi extends AcediaObject;
//! API for basic math methods and [`BigInt`] creation.
/// For storing result of integer division.
///
/// If we divide `number` by `divisor`, then `number = divisor/// quotient + remainder`.
struct IntegerDivisionResult
{
var int quotient;
var int remainder;
};
/// Converts given [`int`] value into [`BigInt`] value..
public function BigInt ToBigInt(int value)
{
local BigInt result;
result = BigInt(_.memory.Allocate(class'BigInt'));
result.SetInt(value);
return result;
}
/// Creates new `BigInt` value, based on the decimal number representation.
///
/// If (and only if) `none` or invalid decimal representation (digits only, possibly with
/// leading sign) is given as an argument, method will return `none`.
public function BigInt MakeBigInt(BaseText value)
{
local BigInt result;
result = BigInt(_.memory.Allocate(class'BigInt'));
if (result.SetDecimal(value)) {
return result;
}
result.FreeSelf();
return none;
}
/// Creates new `BigInt` value, based on the decimal number representation.
///
/// If (and only if) invalid decimal representation (digits only, possibly with leading sign) is
/// given as an argument, method will return `none`.
public function BigInt MakeBigInt_S(string value)
{
local BigInt result;
result = BigInt(_.memory.Allocate(class'BigInt'));
if (result.SetDecimal_S(value)) {
return result;
}
result.FreeSelf();
return none;
}
/// Computes remainder of the integer division of [`number`] by [`divisor`].
///
/// This method is necessary as a replacement for `%` module operator, since it is an operation on
/// `float`s in UnrealScript and does not have appropriate value range to work with big integer
// values.
public function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
/// Computes quotient and remainder of the integer division of [`number`] by [`divisor`].
///
/// See `MathApi::Remainder()` method if you only need remainder.
public function IntegerDivisionResult IntegerDivision(int number, int divisor)
{
local IntegerDivisionResult result;
result.quotient = number / divisor;
result.remainder = (number - result.quotient * divisor);
return result;
}
defaultproperties
{
}

63
sources/BaseRealm/API/Math/Tests/TEST_BigInt.uc → sources/BaseAPI/API/Math/Tests/TEST_BigInt.uc

@ -1,6 +1,8 @@
/**
* Set of tests for `BigInt` class.
* Copyright 2022 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -20,8 +22,7 @@
class TEST_BigInt extends TestCase
abstract;
protected static function TESTS()
{
protected static function TESTS() {
// Here we use `ToString()` method to check `BigInt` creation,
// therefore also testing it
Context("Testing creation of `BigInt`s.");
@ -36,43 +37,33 @@ protected static function TESTS()
Test_SubtractingValues();
}
protected static function Test_Creating()
{
protected static function Test_Creating() {
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a positive `int`.");
TEST_ExpectTrue(__().math.ToBigInt(13524).ToString() == "13524");
TEST_ExpectTrue(
__().math.ToBigInt(MaxInt).ToString() == "2147483647");
TEST_ExpectTrue(__().math.ToBigInt(MaxInt).ToString() == "2147483647");
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a positive integer inside `string`.");
TEST_ExpectTrue(
__().math.MakeBigInt_S("2147483647").ToString()
== "2147483647");
TEST_ExpectTrue(__().math.MakeBigInt_S("2147483647").ToString() == "2147483647");
TEST_ExpectTrue(
__().math.MakeBigInt_S("4238756872643464981264982128742389")
.ToString() == "4238756872643464981264982128742389");
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a negative `int`.");
Issue("`ToString()` doesn't return value `BigInt` was initialized with a negative `int`.");
TEST_ExpectTrue(__().math.ToBigInt(-666).ToString() == "-666");
TEST_ExpectTrue(
__().math.ToBigInt(-MaxInt).ToString() == "-2147483647");
TEST_ExpectTrue(
__().math.ToBigInt(-MaxInt - 1).ToString() == "-2147483648");
TEST_ExpectTrue(__().math.ToBigInt(-MaxInt).ToString() == "-2147483647");
TEST_ExpectTrue(__().math.ToBigInt(-MaxInt - 1).ToString() == "-2147483648");
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a negative integer inside `string`.");
TEST_ExpectTrue(
__().math.MakeBigInt_S("-2147483648").ToString()
== "-2147483648");
TEST_ExpectTrue(__().math.MakeBigInt_S("-2147483648").ToString() == "-2147483648");
TEST_ExpectTrue(
__().math.MakeBigInt_S("-238473846327894632879097410348127")
.ToString() == "-238473846327894632879097410348127");
}
protected static function Test_ToText()
{
protected static function Test_ToText() {
Issue("`ToText()` doesn't return value `BigInt` was initialized with" @
"a positive integer inside `string`.");
TEST_ExpectTrue(__().math
@ -96,14 +87,12 @@ protected static function Test_ToText()
.ToString() == "-9827657892365923510176386357863078603212901078175829");
}
protected static function Test_AddingValues()
{
protected static function Test_AddingValues() {
SubTest_AddingSameSignValues();
SubTest_AddingDifferentSignValues();
}
protected static function SubTest_AddingSameSignValues()
{
protected static function SubTest_AddingSameSignValues() {
local BigInt main, addition;
Issue("Two positive `BigInt`s are incorrectly added.");
@ -135,8 +124,7 @@ protected static function SubTest_AddingSameSignValues()
TEST_ExpectTrue(main.ToString() == "-1457931745873178552");
}
protected static function SubTest_AddingDifferentSignValues()
{
protected static function SubTest_AddingDifferentSignValues() {
local BigInt main, addition;
Issue("Negative `BigInt`s is incorrectly added to positive one.");
@ -168,14 +156,12 @@ protected static function SubTest_AddingDifferentSignValues()
TEST_ExpectTrue(main.ToString() == "0");
}
protected static function Test_SubtractingValues()
{
protected static function Test_SubtractingValues() {
SubTest_SubtractingSameSignValues();
SubTest_SubtractingDifferentSignValues();
}
protected static function SubTest_SubtractingSameSignValues()
{
protected static function SubTest_SubtractingSameSignValues() {
local BigInt main, sub;
Issue("Two positive `BigInt`s are incorrectly subtracted.");
@ -207,8 +193,7 @@ protected static function SubTest_SubtractingSameSignValues()
TEST_ExpectTrue(main.ToString() == "0");
}
protected static function SubTest_SubtractingDifferentSignValues()
{
protected static function SubTest_SubtractingDifferentSignValues() {
local BigInt main, sub;
Issue("Negative `BigInt`s is incorrectly subtracted from positive one.");
@ -240,8 +225,7 @@ protected static function SubTest_SubtractingDifferentSignValues()
TEST_ExpectTrue(main.ToString() == "-1457931745873178552");
}
protected static function Test_ToInt()
{
protected static function Test_ToInt() {
Issue("Testing conversion for non-overflowing values.");
TEST_ExpectTrue(__().math.MakeBigInt_S("0").ToInt() == 0);
TEST_ExpectTrue(__().math.MakeBigInt_S("-0").ToInt() == 0);
@ -264,8 +248,7 @@ protected static function Test_ToInt()
__().math.MakeBigInt_S("-32545657348437563873").ToInt() == -2147483648);
}
defaultproperties
{
caseGroup = "Math"
caseName = "BigInt"
defaultproperties {
caseGroup = "Math"
caseName = "BigInt"
}

158
sources/BaseAPI/API/Memory/AcediaObjectPool.uc

@ -0,0 +1,158 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaObjectPool extends Object
config(AcediaSystem);
//! Acedia's implementation for object pool.
//!
//! Unlike generic built in [`Engine::ObjectPool`], that can only store objects of one specific
//! class, it specializes in a single class to allow for both faster allocation and
//! faster deallocation (we don't need to look for an object of particular class to return
//! an unused instance).
//!
//! Allows to set a maximum capacity in a config.
/// Represents config entry about pool capacity.
///
/// This struct and it's associated array [`poolSizeOverwrite`] allows server admins to rewrite
/// the pool capacity for each class.
struct PoolSizeSetting {
var class<AcediaObject> objectClass;
var int maxPoolSize;
};
var private config const array<PoolSizeSetting> poolSizeOverwrite;
// Class of objects that this `AcediaObjectPool` stores.
// if `== none`, - object pool is considered uninitialized.
var private class<AcediaObject> storedClass;
/// Capacity for object pool that we are using.
/// Obtained from [`poolSizeOverwrite`] during initialization and cannot be changed later.
var private int usedMaxPoolSize;
// Actual storage, functions on LIFO principle.
var private array<AcediaObject> objectPool;
// Determines default object pool size for the initialization.
private final function int GetMaxPoolSizeForClass(class<AcediaObject> classToCheck) {
local int i;
local int result;
if (classToCheck != none) {
result = classToCheck.default.defaultMaxPoolSize;
}
else {
result = -1;
}
// Try to replace it with server's settings
for (i = 0; i < poolSizeOverwrite.length; i += 1) {
if (poolSizeOverwrite[i].objectClass == classToCheck) {
result = poolSizeOverwrite[i].maxPoolSize;
break;
}
}
return result;
}
/// Initializes caller object pool to store objects of the given class.
///
/// Returns `true` if initialization completed, `false` otherwise (including if it was already
/// completed with passed [`classToStore`]).
///
/// If successful, this action is irreversible: same pool cannot be re-initialized.
///
/// [`forcedPoolSize`] defines max pool size for the caller [`AcediaObjectPool`].
/// Leaving it at default `0` value will cause method to auto-determine the size: gives priority to
/// the [`poolSizeOverwrite`] config array; if not specified, uses [`AcediaObject`]'s
/// [`AcediaObject::defaultMaxPoolSize`] (ignoring [`AcediaObject::usesObjectPool`] setting).
public final function bool Initialize(
class<AcediaObject> classToStore,
optional int forcedPoolSize
) {
if (storedClass != none) return false;
if (classToStore == none) return false;
// If does not matter that we've set those variables until
// we set `storedClass`.
if (forcedPoolSize == 0) {
usedMaxPoolSize = GetMaxPoolSizeForClass(classToStore);
}
else {
usedMaxPoolSize = forcedPoolSize;
}
if (usedMaxPoolSize == 0) {
return false;
}
storedClass = classToStore;
return true;
}
/// Returns class of objects stored inside the caller [`AcediaObjectPool`].
///
/// `none` means object pool was not initialized.
public final function class<AcediaObject> GetClassOfStoredObjects() {
return storedClass;
}
/// Clear the storage of all its contents.
public final function Clear() {
objectPool.length = 0;
}
/// Adds object to the caller storage (that needs to be initialized to store [`newObject.class`]
/// classes).
///
/// Returns `true` on success and `false` on failure (can happen if passed [`newObject`] reference
/// was invalid, caller storage is not initialized yet or reached it's capacity).
///
/// For performance purposes does not do duplicates checks, this should be verified from outside
/// [`AcediaObjectPool`].
///
/// Performs type checks and only allows objects of the class that caller [`AcediaObjectPool`] was
/// initialized for.
public final function bool Store(AcediaObject newObject) {
if (newObject == none) return false;
if (newObject.class != storedClass) return false;
if (usedMaxPoolSize >= 0 && objectPool.length >= usedMaxPoolSize) {
return false;
}
objectPool[objectPool.length] = newObject;
return true;
}
/// Returns last stored object from the pool, removing it from that pool in the process.
///
/// Only returns `none` if caller `AcediaObjectPool` is either empty or not initialized.
public final function AcediaObject Fetch() {
local AcediaObject result;
if (storedClass == none) return none;
if (objectPool.length <= 0) return none;
result = objectPool[objectPool.length - 1];
objectPool.length = objectPool.length - 1;
return result;
}
defaultproperties {
}

366
sources/BaseAPI/API/Memory/MemoryAPI.uc

@ -0,0 +1,366 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MemoryApi extends AcediaObject;
//! API that provides functions for managing object of classes, derived from `AcediaObject`.
//!
//! This is most-basic API that must be created before anything else in Acedia, since it is
//! responsible for the proper creation of `AcediaObject`s.
//! It takes care of managing their object pools, as well as ensuring that constructors and
//! finalizers are called properly.
//!
//! Almost all `AcediaObject`s should use this API's methods for their own creation and destruction.
//!
//! ## Usage
//!
//! First of all, this API is only meant for non-actor `Object` creation.
//! `Actor` creation is generally avoided in Acedia and, when unavoidable, different APIs
//! are dealing with that.
//! `MemoryApi` is designed to work in the absence of any level (and, therefore, `Actor`s) at all.
//!
//! Simply use `MemoryApi.Allocate()` to create a new object and `MemoryApi.Free()` to get rid of
//! unneeded reference.
//! Do note that `AcediaObject`s use reference counting and object will be deallocated and pooled
//! only after every trackable reference was released by `MemoryApi.Free()`.
//!
//! Best practice is to only care about what object reference you're keeping, properly release them
//! with `MemoryApi.Free()` and to NEVER EVER USE THEM after you've release them.
//! Regardless of whether they were actually deallocated.
//!
//! There's also a set of auxiliary methods for either loading `class`es from their
//! `BaseText`/`string`-given names or even directly creating objects of said classes.
//!
//! ## Customizing object pools for your classes
//!
//! Object pool usage can be disabled completely for your class by setting `usesObjectPool = false`
//! in `defaultproperties` block.
//! Without object pools `MemoryApi.Allocate()` will create a new instance of your class every
//! single time.
//!
//! You can also set a limit to how many objects will be stored in an object pool with
//! `defaultMaxPoolSize` variable.
//! Negative number (default for `AcediaObject`) means that object pool can grow without a limit.
//! `0` effectively disables object pool, similar to setting `usesObjectPool = false`.
//! However, this can be overwritten by server's settings
//! (see `AcediaSystem.ini`: `AcediaObjectPool`).
// Store all created pools, so that we can quickly forget stored objects upon garbage collection
var private array<AcediaObjectPool> registeredPools;
/// Forgets about all stored (deallocated) object references in registered object pools.
protected function DropPools() {
local int i;
registeredPools = default.registeredPools;
for (i = 0; i < registeredPools.length; i += 1) {
if (registeredPools[i] == none) {
continue;
}
registeredPools[i].Clear();
}
}
/// Creates a class instance from its `BaseText` representation.
///
/// Does not generate log messages upon failure.
public function class<Object> LoadClass(BaseText classReference) {
if (classReference == none) {
return none;
}
return class<Object>(DynamicLoadObject(classReference.ToString(), class'Class', true));
}
/// Creates a class instance from its `string` representation.
///
/// Does not generate log messages upon failure.
public function class<Object> LoadClass_S(string classReference) {
return class<Object>(DynamicLoadObject(classReference, class'Class', true));
}
/// Creates a new `AcediaObject` of a given subclass using pool (if permitted by settings) and
/// calling its constructor.
///
/// If Acedia's object does make use of object pools, this method guarantees to return last pooled
/// object (in a LIFO queue), unless `forceNewInstance` is set to `true`.
///
/// Return value will only be `none` if `classToAllocate` is `none` or abstract.
public function AcediaObject Allocate(
class<AcediaObject> classToAllocate,
optional bool forceNewInstance
) {
local AcediaObject allocatedObject;
local AcediaObjectPool relevantPool;
if (classToAllocate == none) {
return none;
}
// Try using pool first (but only if new instance is not required)
if (!forceNewInstance) {
relevantPool = classToAllocate.static._getPool();
// `relevantPool == none` is expected if object / actor of is setup to
// not use object pools.
if (relevantPool != none) {
allocatedObject = relevantPool.Fetch();
}
}
// If pools did not work - simply create object manually
if (allocatedObject == none) {
allocatedObject = (new classToAllocate);
}
// Allocation through `new` cannot fail, so its safe to call constructor
allocatedObject._constructor();
return allocatedObject;
}
/// Creates a new `AcediaObject` of a given subclass using pool (if permitted by settings) and
/// calling its constructor.
///
/// If Acedia's object does make use of object pools, this method guarantees to return last pooled
/// object (in a LIFO queue), unless `forceNewInstance` is set to `true`.
///
/// Return value will only be `none` if `refToClassToAllocate` is `none`, doesn't refer to
/// an existing class or refers to an abstract class.
public function AcediaObject AllocateByReference(
BaseText refToClassToAllocate,
optional bool forceNewInstance
) {
local class<Object> classToAllocate;
classToAllocate = LoadClass(refToClassToAllocate);
return Allocate(class<AcediaObject>(classToAllocate), forceNewInstance);
}
/// Creates a new `AcediaObject` of a given subclass using pool (if permitted by settings) and
/// calling its constructor.
///
/// If Acedia's object does make use of object pools, this method guarantees to return last pooled
/// object (in a LIFO queue), unless `forceNewInstance` is set to `true`.
///
/// Return value will only be `none` if `refToClassToAllocate` is `none`, doesn't refer to
/// an existing class or refers to an abstract class.
public function AcediaObject AllocateByReference_S(
string refToClassToAllocate,
optional bool forceNewInstance
) {
local class<Object> classToAllocate;
classToAllocate = LoadClass_S(refToClassToAllocate);
return Allocate(class<AcediaObject>(classToAllocate), forceNewInstance);
}
/// Releases one reference to a given [`AcediaObject`], calling its finalizers in case
/// all references were released.
///
/// Method will attempt to store [`objectToRelease`] in its object pool once deallocated,
/// unless it is forbidden by its class' settings.
public function Free(AcediaObject objectToRelease) {
local AcediaObjectPool relevantPool;
if (objectToRelease == none) return;
if (!objectToRelease.IsAllocated()) return;
objectToRelease._deref();
// Finalize object if all of its references are gone
if (objectToRelease._getRefCount() <= 0) {
relevantPool = objectToRelease._getPool();
objectToRelease._finalizer();
if (relevantPool != none) {
relevantPool.Store(objectToRelease);
}
}
}
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case
/// all references were released.
///
/// Method will attempt to store released objects in its object pool once deallocated,
/// unless it is forbidden by its class' settings.
public function Free2(AcediaObject objectToRelease1, AcediaObject objectToRelease2) {
Free(objectToRelease1);
Free(objectToRelease2);
}
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case
/// all references were released.
///
/// Method will attempt to store released objects in its object pool once deallocated,
/// unless it is forbidden by its class' settings.
public function Free3(
AcediaObject objectToRelease1,
AcediaObject objectToRelease2,
AcediaObject objectToRelease3
) {
Free(objectToRelease1);
Free(objectToRelease2);
Free(objectToRelease3);
}
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case
/// all references were released.
///
/// Method will attempt to store released objects in its object pool once deallocated,
/// unless it is forbidden by its class' settings.
public function Free4(
AcediaObject objectToRelease1,
AcediaObject objectToRelease2,
AcediaObject objectToRelease3,
AcediaObject objectToRelease4
) {
Free(objectToRelease1);
Free(objectToRelease2);
Free(objectToRelease3);
Free(objectToRelease4);
}
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case
/// all references were released.
///
/// Method will attempt to store released objects in its object pool once deallocated,
/// unless it is forbidden by its class' settings.
public function Free5(
AcediaObject objectToRelease1,
AcediaObject objectToRelease2,
AcediaObject objectToRelease3,
AcediaObject objectToRelease4,
AcediaObject objectToRelease5
) {
Free(objectToRelease1);
Free(objectToRelease2);
Free(objectToRelease3);
Free(objectToRelease4);
Free(objectToRelease5);
}
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case
/// all references were released.
///
/// Method will attempt to store released objects in its object pool once deallocated,
/// unless it is forbidden by its class' settings.
public function Free6(
AcediaObject objectToRelease1,
AcediaObject objectToRelease2,
AcediaObject objectToRelease3,
AcediaObject objectToRelease4,
AcediaObject objectToRelease5,
AcediaObject objectToRelease6
) {
Free(objectToRelease1);
Free(objectToRelease2);
Free(objectToRelease3);
Free(objectToRelease4);
Free(objectToRelease5);
Free(objectToRelease6);
}
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case
/// all references were released.
///
/// Method will attempt to store released objects in its object pool once deallocated,
/// unless it is forbidden by its class' settings.
public function Free7(
AcediaObject objectToRelease1,
AcediaObject objectToRelease2,
AcediaObject objectToRelease3,
AcediaObject objectToRelease4,
AcediaObject objectToRelease5,
AcediaObject objectToRelease6,
AcediaObject objectToRelease7
) {
Free(objectToRelease1);
Free(objectToRelease2);
Free(objectToRelease3);
Free(objectToRelease4);
Free(objectToRelease5);
Free(objectToRelease6);
Free(objectToRelease7);
}
/// Releases one reference for each `AcediaObject` inside the given array `objectsToRelease`,
/// calling finalizers for the ones that got all of their references released.
///
/// Method will attempt to store objects inside `objectsToRelease` in their object pools, unless it
/// is forbidden by their class' settings.
public function FreeMany(array<AcediaObject> objectsToRelease) {
local int i;
for (i = 0; i < objectsToRelease.length; i += 1) {
Free(objectsToRelease[i]);
}
}
/// Forces engine to perform garbage collection.
///
/// Process of manual garbage collection causes significant lag spike during the game and should be
/// used sparingly and at right moments.
///
/// If no `LevelCore` was setup, Acedia doesn't have access to the level and cannot perform garbage
/// collection, meaning that this method can fail.
///
/// By default also cleans up all of the Acedia's objects pools.
/// Set [`keepAcediaPools`] to `true` to NOT garbage collect objects inside pools.
/// Pools won't be dropped regardless of this parameter if no `LevelCore` is found.
///
/// Returns `true` if garbage collection successfully happened and `false` if it failed.
/// Garbage collection can only fail if no `LevelCore` was yet setup.
public function bool /*unreal*/ CollectGarbage(optional bool keepAcediaPools) {
local LevelCore core;
// Try to find level core
core = class'ServerLevelCore'.static.GetInstance();
if (core == none) {
core = class'ClientLevelCore'.static.GetInstance();
}
if (core == none) {
return false;
}
// Drop content of all `AcediaObjectPools` first
if (!keepAcediaPools) {
DropPools();
}
// This makes Unreal Engine do garbage collection
core.ConsoleCommand("obj garbage");
return true;
}
/// Registers new object pool to auto-clean before Acedia's garbage collection.
///
/// Returns `true` if `newPool` was registered and `false` if `newPool == none` or was already
/// registered.
public function bool RegisterNewPool(AcediaObjectPool newPool) {
local int i;
if (newPool == none) {
return false;
}
registeredPools = default.registeredPools;
for (i = 0; i < registeredPools.length; i += 1) {
if (registeredPools[i] == newPool) {
return false;
}
}
registeredPools[registeredPools.length] = newPool;
default.registeredPools = registeredPools;
return true;
}
defaultproperties {
}

16
sources/BaseRealm/API/Memory/Tests/MockActor.uc → sources/BaseAPI/API/Memory/Tests/MockActor.uc

@ -1,7 +1,8 @@
/**
* Mock actor class for testing `MemoryAPI` and
* it's actor allocation/deallocation.
* Copyright 2020 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,17 +23,14 @@ class MockActor extends AcediaActor;
var public int actorCount;
protected function Constructor()
{
protected function Constructor() {
default.actorCount += 1;
}
protected function Finalizer()
{
protected function Finalizer() {
default.actorCount -= 1;
}
defaultproperties
{
defaultproperties {
actorCount = 0
}

16
sources/BaseRealm/API/Memory/Tests/MockObject.uc → sources/BaseAPI/API/Memory/Tests/MockObject.uc

@ -1,7 +1,8 @@
/**
* Mock object class for testing `MemoryAPI` and
* it's object allocation/deallocation.
* Copyright 2020 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,17 +23,14 @@ class MockObject extends AcediaObject;
var public int objectCount;
protected function Constructor()
{
protected function Constructor() {
default.objectCount += 1;
}
protected function Finalizer()
{
protected function Finalizer() {
default.objectCount -= 1;
}
defaultproperties
{
defaultproperties {
objectCount = 0
}

10
sources/BaseRealm/API/Memory/Tests/MockObjectNoPool.uc → sources/BaseAPI/API/Memory/Tests/MockObjectNoPool.uc

@ -1,7 +1,8 @@
/**
* Mock object class for testing `MemoryAPI` and
* it's object allocation/deallocation.
* Copyright 2020 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -20,7 +21,6 @@
*/
class MockObjectNoPool extends AcediaObject;
defaultproperties
{
defaultproperties {
usesObjectPool = false
}

163
sources/BaseRealm/API/Memory/Tests/TEST_Memory.uc → sources/BaseAPI/API/Memory/Tests/TEST_Memory.uc

@ -1,7 +1,8 @@
/**
* Set of tests related to `MemoryAPI` class and the chain of events related to
* creating/destroying Acedia's objects / actors.
* Copyright 2020-2022 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -21,8 +22,7 @@
class TEST_Memory extends TestCase
abstract;
protected static function TESTS()
{
protected static function TESTS() {
Test_ObjectConstructorsFinalizers();
Test_ActorConstructorsFinalizers();
Test_ObjectPoolUsage();
@ -30,24 +30,21 @@ protected static function TESTS()
Test_RefCounting();
}
protected static function Test_LifeVersionIsUnique()
{
local int i, j;
local int nextVersion;
local MockObject obj;
local array<int> objectVersions;
local bool versionsRepeated;
// Deallocate and reallocate same object/actor a bunch of times and
// ensure that every single time a unique number is returned.
// Not a comprehensive test of uniqueness, but such is impossible.
for (i = 0; i < 1000 && !versionsRepeated; i += 1)
{
protected static function Test_LifeVersionIsUnique() {
local int i, j;
local int nextVersion;
local MockObject obj;
local array<int> objectVersions;
local bool versionsRepeated;
// Deallocate and reallocate same object/actor a bunch of times and
// ensure that every single time a unique number is returned.
// Not a comprehensive test of uniqueness, but such is impossible.
for (i = 0; i < 1000 && !versionsRepeated; i += 1) {
obj = MockObject(__().memory.Allocate(class'MockObject'));
nextVersion = obj.GetLifeVersion();
for (j = 0; j < objectVersions.length; j += 1)
{
if (nextVersion == objectVersions[j])
{
for (j = 0; j < objectVersions.length; j += 1) {
if (nextVersion == objectVersions[j]) {
versionsRepeated = true;
break;
}
@ -61,11 +58,10 @@ protected static function Test_LifeVersionIsUnique()
TEST_ExpectFalse(versionsRepeated);
}
protected static function Test_ObjectConstructorsFinalizers()
{
protected static function Test_ObjectConstructorsFinalizers() {
local MockObject obj1, obj2;
Context("Testing that Acedia object's constructors and finalizers are"
@ "called properly.");
Context("Testing that Acedia object's constructors and finalizers are called properly.");
Issue("Object's constructor is not called.");
class'MockObject'.default.objectCount = 0;
obj1 = MockObject(__().memory.Allocate(class'MockObject'));
@ -92,60 +88,51 @@ protected static function Test_ObjectConstructorsFinalizers()
TEST_ExpectTrue(class'MockObject'.default.objectCount == 0);
}
protected static function Test_ActorConstructorsFinalizers()
{
protected static function Test_ActorConstructorsFinalizers() {
local MockActor act1, act2;
Context("Testing that Acedia actor's constructors and finalizers are"
@ "called properly.");
Context("Testing that Acedia actor's constructors and finalizers are called properly.");
Issue("Actor's constructor is not called.");
act1 = MockActor(__().memory.Allocate(class'MockActor'));
act1 = MockActor(__level().GetLevelCore().Allocate(class'MockActor'));
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
act2 = MockActor(__().memory.Allocate(class'MockActor'));
act2 = MockActor(__level().GetLevelCore().Allocate(class'MockActor'));
TEST_ExpectTrue(class'MockActor'.default.actorCount == 2);
Issue("Actor's finalizer is not called.");
__().memory.Free(act1);
act1.Destroy();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
Issue("`IsAllocated()` returns `false` for allocated actors.");
TEST_ExpectTrue(act2.IsAllocated());
Issue("Actor's finalizer is called for already freed object.");
__().memory.Free(act1);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
Issue("Actor's finalizer is not called.");
__().memory.Free(act2);
act2.Destroy();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 0);
}
protected static function Test_ObjectPoolUsage()
{
local bool allocatedNewObject;
local int i, j;
local MockObject temp;
protected static function Test_ObjectPoolUsage() {
local bool allocatedNewObject;
local int i, j;
local MockObject temp;
local array<MockObject> objects;
local MockObjectNoPool obj1, obj2;
local MockObjectNoPool obj1, obj2;
Context("Testing usage of object pools by `MockObject`s.");
Issue("Object pool is not utilized enough.");
for (i = 0; i < 200; i += 1)
{
for (i = 0; i < 200; i += 1) {
objects[objects.length] =
MockObject(__().memory.Allocate(class'MockObject'));
}
for (i = 0; i < 200; i += 1) {
__().memory.Free(objects[i]);
}
for (i = 0; i < 200; i += 1)
{
for (i = 0; i < 200; i += 1) {
temp = MockObject(__().memory.Allocate(class'MockObject'));
// Have to find just allocated object among already free ones
j = 0;
allocatedNewObject = true;
while (j < objects.length)
{
if (objects[j] == temp)
{
while (j < objects.length) {
if (objects[j] == temp) {
allocatedNewObject = false;
objects.Remove(j, 1);
break;
@ -165,20 +152,16 @@ protected static function Test_ObjectPoolUsage()
TEST_ExpectTrue(obj1 != obj2);
}
protected static function Test_RefCounting()
{
protected static function Test_RefCounting() {
Context("Testing usage of reference counting.");
SubTest_RefCountingObjectFreeSelf();
SubTest_RefCountingObjectFree();
SubTest_RefCountingActorFreeSelf();
SubTest_RefCountingActorFree();
}
protected static function SubTest_RefCountingObjectFreeSelf()
{
protected static function SubTest_RefCountingObjectFreeSelf() {
local MockObject temp;
Issue("Reference counting for `AcediaObject`s does not work correctly"
@ "with `FreeSelf()`");
Issue("Reference counting for `AcediaObject`s does not work correctly with `FreeSelf()`");
temp = MockObject(__().memory.Allocate(class'MockObject'));
temp.NewRef().NewRef().NewRef();
TEST_ExpectTrue(temp._getRefCount() == 4);
@ -197,9 +180,9 @@ protected static function SubTest_RefCountingObjectFreeSelf()
TEST_ExpectFalse(temp.IsAllocated());
}
protected static function SubTest_RefCountingObjectFree()
{
protected static function SubTest_RefCountingObjectFree() {
local MockObject temp;
Issue("Reference counting for `AcediaObject`s does not work correctly"
@ "with `__().memory.Free()`");
temp = MockObject(__().memory.Allocate(class'MockObject'));
@ -220,63 +203,7 @@ protected static function SubTest_RefCountingObjectFree()
TEST_ExpectFalse(temp.IsAllocated());
}
protected static function SubTest_RefCountingActorFreeSelf()
{
local MockActor temp;
class'MockActor'.default.actorCount = 0;
Issue("Reference counting for `AcediaActor`s does not work correctly"
@ "with `FreeSelf()`");
temp = MockActor(__().memory.Allocate(class'MockActor'));
temp.NewRef().NewRef().NewRef();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 4);
TEST_ExpectTrue(temp.IsAllocated());
temp.FreeSelf();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 3);
TEST_ExpectTrue(temp.IsAllocated());
temp.FreeSelf();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 2);
TEST_ExpectTrue(temp.IsAllocated());
temp.FreeSelf();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 1);
TEST_ExpectTrue(temp.IsAllocated());
temp.FreeSelf();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 0);
}
protected static function SubTest_RefCountingActorFree()
{
local MockActor temp;
class'MockActor'.default.actorCount = 0;
Issue("Reference counting for `AcediaActor`s does not work correctly"
@ "with `Free()`");
temp = MockActor(__().memory.Allocate(class'MockActor'));
temp.NewRef().NewRef().NewRef();
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 4);
TEST_ExpectTrue(temp.IsAllocated());
__().memory.Free(temp);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 3);
TEST_ExpectTrue(temp.IsAllocated());
__().memory.Free(temp);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 2);
TEST_ExpectTrue(temp.IsAllocated());
__().memory.Free(temp);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 1);
TEST_ExpectTrue(temp._getRefCount() == 1);
TEST_ExpectTrue(temp.IsAllocated());
__().memory.Free(temp);
TEST_ExpectTrue(class'MockActor'.default.actorCount == 0);
}
defaultproperties
{
defaultproperties {
caseGroup = "Memory"
caseName = "AllocationDeallocation"
}

333
sources/BaseAPI/API/Scheduler/SchedulerAPI.uc

@ -0,0 +1,333 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SchedulerApi extends AcediaObject
config(AcediaSystem);
//! This API is meant for scheduling various actions over time to help emulating
//! multi-threading by spreading some code executions over several different
//! game/server ticks.
//!
//! UnrealScript is inherently single-threaded and whatever method you call,
//! it will be completely executed within a single game's tick.
// How often can files be saved on disk.
//
// This is a relatively expensive operation and we don't want to write a lot of different files
// at once.
// But since we lack a way to exactly measure how much time that saving will take, AcediaCore falls
// back to simply performing every saving with same uniform time intervals in-between.
// This variable decides how much time there should be between two file writing accesses.
// Negative and zero values mean that all writing disk access will be granted as soon as possible,
// without any cooldowns.
var private config float diskSaveCooldown;
// Maximum total work units for jobs allowed per tick.
//
// Jobs are expected to be constructed such that they don't lead to a crash if they have to perform
// this much work.
// Changing default value of `10000` is not advised.
var private config int maxWorkUnits;
// How many different jobs can be performed per tick.
//
// This limit is added so that `maxWorkUnits` won't be spread too thin if a lot of jobs
// get registered at once.
var private config int maxJobsPerTick;
// We can (and will) automatically tick
var private bool tickAvailable;
// `true` == it is safe to use server API for a tick
// `false` == it is safe to use client API for a tick
var private bool tickFromServer;
// Our `Tick()` method is currently connected to the `OnTick()` signal.
//
// Keeping track of this allows us to disconnect from `OnTick()` signal when it is not necessary.
var private bool connectedToTick;
// How much time if left until we can write to the disk again?
var private float currentDiskCooldown;
// There is a limit (`maxJobsPerTick`) to how many different jobs we can perform per tick and if we
// register an amount jobs over that limit, we need to uniformly spread execution time between them.
//
// To achieve that we simply cyclically (in order) go over `currentJobs` array, each time executing
// exactly `maxJobsPerTick` jobs.
//
// `nextJobToPerform` remembers what job is to be executed next tick.
var private int nextJobToPerform;
var private array<SchedulerJob> currentJobs;
// Storing receiver objects, following example of signals/slots, is done without increasing their
// reference count, allowing them to get deallocated while we are still keeping their reference.
//
// To avoid using such deallocated receivers, we keep track of the life versions they've had when
// their disk requests were registered.
var private array<SchedulerDiskRequest> diskQueue;
var private array<AcediaObject> receivers;
var private array<int> receiversLifeVersions;
/// Registers new scheduler job to be executed in the API.
///
/// Does nothing if given `newJob` is already added.
public function AddJob(SchedulerJob newJob) {
local int i;
if (newJob == none) {
return;
}
for (i = 0; i < currentJobs.length; i += 1) {
if (currentJobs[i] == newJob) {
return;
}
}
newJob.NewRef();
currentJobs[currentJobs.length] = newJob;
UpdateTickConnection();
}
/// Requests another disk access.
///
/// Use it like signal: `RequestDiskAccess(<receiver>).connect = <handler>`.
/// Since it is meant to be used as a signal, so DO NOT STORE/RELEASE returned wrapper object
/// [`SchedulerDiskRequest`].
///
/// Same as for signal/slots, [`receiver`] is an object, responsible for the disk request.
/// If this object gets deallocated - request will be thrown away.
/// Typically this should be an object in which connected method will be executed.
/// Returns wrapper object that provides `connect` delegate.
///
/// # Examples
///
/// ```
/// _.scheduler.RequestDiskAccess(self).connect = MethodThatSaves();
/// ```
public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver) {
local SchedulerDiskRequest newRequest;
if (receiver == none) return none;
if (!receiver.IsAllocated()) return none;
newRequest = SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest'));
diskQueue[diskQueue.length] = newRequest;
receivers[receivers.length] = receiver;
receiversLifeVersions[receiversLifeVersions.length] = receiver.GetLifeVersion();
UpdateTickConnection();
return newRequest;
}
/// Returns amount of incomplete jobs are currently registered in the scheduler.
public function int GetJobsAmount() {
CleanCompletedJobs();
return currentJobs.length;
}
/// Returns amount of disk access requests are currently registered in the scheduler.
public function int GetDiskQueueSize() {
CleanDiskQueue();
return diskQueue.length;
}
/// Performs another batch of scheduled tasks.
///
/// In case neither server, nor client core is registered, scheduler must be ticked manually.
/// For that call this method each separate tick (or whatever is your closest approximation
/// available for that).
/// Before manually invoking this method, you should check if scheduler actually started to tick
/// *automatically*.
/// Use `_.scheduler.IsAutomated()` for that.
///
/// Argument is a time (real, not in-game one) that is supposedly passes from the moment
/// [`SchedulerApi::ManualTick()`] was called last time.
/// Used for tracking disk access cooldowns.
/// How [`SchedulerJob`]s are executed is independent from this value.
///
/// Returns time (real, not in-game one) that is supposedly passes from the moment
/// [`SchedulerApi::ManualTick()`] was called last time.
///
/// # Examples
///
/// ```
/// if (!_.scheduler.IsAutomated()) {
/// _.scheduler.ManualTick(0.05);
/// }
/// ```
///
/// # Note
///
/// If neither server-/client- core is created, nor [`SchedulerApi::ManualTick()`] is invoked
/// manually, [`SchedulerApi`] won't actually do anything.
public final function ManualTick(optional float delta) {
Tick(delta, 1.0);
}
/// Returns whether scheduler ticking automated.
///
/// It can only be automated if either server or client level cores are created.
/// Scheduler can automatically enable automation and it cannot be prevented, but can be helped by
/// using [`SchedulerApi::UpdateTickConnection()`] method.
public function bool IsAutomated() {
return tickAvailable;
}
/// Causes `SchedulerApi` to try automating itself by searching for level cores (checking if
/// server/client APIs are enabled).
public function UpdateTickConnection() {
local bool needsConnection;
local UnrealAPI api;
if (!tickAvailable) {
if (_server.IsAvailable()) {
tickAvailable = true;
tickFromServer = true;
}
else if (_client.IsAvailable()) {
tickAvailable = true;
tickFromServer = false;
}
if (!tickAvailable) {
return;
}
}
needsConnection = (currentJobs.length > 0 || diskQueue.length > 0);
if (connectedToTick == needsConnection) {
return;
}
if (tickFromServer) {
api = _server.unreal;
} else {
api = _client.unreal;
}
if (connectedToTick && !needsConnection) {
api.OnTick(self).Disconnect();
} else if (!connectedToTick && needsConnection) {
api.OnTick(self).connect = Tick;
}
connectedToTick = needsConnection;
}
private function Tick(float delta, float dilationCoefficient) {
delta = delta / dilationCoefficient;
if (currentDiskCooldown > 0) {
currentDiskCooldown -= delta;
}
if (currentDiskCooldown <= 0 && diskQueue.length > 0) {
currentDiskCooldown = diskSaveCooldown;
ProcessDiskQueue();
}
// Manage jobs
if (currentJobs.length > 0) {
ProcessJobs();
}
UpdateTickConnection();
}
private function ProcessJobs()
{
local int unitsPerJob;
local int jobsToPerform;
CleanCompletedJobs();
jobsToPerform = Min(currentJobs.length, maxJobsPerTick);
if (jobsToPerform <= 0) {
return;
}
unitsPerJob = maxWorkUnits / jobsToPerform;
while (jobsToPerform > 0) {
if (nextJobToPerform >= currentJobs.length) {
nextJobToPerform = 0;
}
currentJobs[nextJobToPerform].DoWork(unitsPerJob);
nextJobToPerform += 1;
jobsToPerform -= 1;
}
}
private function ProcessDiskQueue()
{
local int i;
// Even if we clean disk queue here, we still need to double check
// lifetimes in the code below, since we have no idea what `.connect()`
// calls might do
CleanDiskQueue();
if (diskQueue.length <= 0) {
return;
}
if (diskSaveCooldown > 0) {
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) {
diskQueue[i].connect();
}
_.memory.Free(diskQueue[0]);
diskQueue.Remove(0, 1);
receivers.Remove(0, 1);
receiversLifeVersions.Remove(0, 1);
return;
}
for (i = 0; i < diskQueue.length; i += 1) {
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) {
diskQueue[i].connect();
}
_.memory.Free(diskQueue[i]);
}
diskQueue.length = 0;
receivers.length = 0;
receiversLifeVersions.length = 0;
}
// Removes completed jobs
private function CleanCompletedJobs()
{
local int i;
while (i < currentJobs.length) {
if (currentJobs[i].IsCompleted()) {
if (i < nextJobToPerform) {
nextJobToPerform -= 1;
}
currentJobs[i].FreeSelf();
currentJobs.Remove(i, 1);
} else {
i += 1;
}
}
}
// Remove disk requests with deallocated receivers
private function CleanDiskQueue() {
local int i;
while (i < diskQueue.length) {
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) {
i += 1;
continue;
}
_.memory.Free(diskQueue[i]);
diskQueue.Remove(i, 1);
receivers.Remove(i, 1);
receiversLifeVersions.Remove(i, 1);
}
}
defaultproperties {
diskSaveCooldown = 0.25
maxWorkUnits = 10000
maxJobsPerTick = 5
}

31
sources/BaseAPI/API/Scheduler/SchedulerDiskRequest.uc

@ -0,0 +1,31 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SchedulerDiskRequest extends AcediaObject;
//! Slot-like object that represents a request for a writing disk access, capable of being scheduled
//! on the [`SchedulerApi`].
delegate connect() {
}
defaultproperties {
}

44
sources/BaseAPI/API/Scheduler/SchedulerJob.uc

@ -0,0 +1,44 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SchedulerJob extends AcediaObject
abstract;
//! Template object that represents a job, capable of being scheduled on the [`SchedulerAPI`].
//! Use [`IsCompleted()`] to mark job as completed.
/// Checks if caller [`SchedulerJob`] was completed.
///
/// Returns `true` if [`SchedulerJob`] is already completed and doesn't need to be further executed
/// and `false` otherwise.
/// Once this method returns `true`, it shouldn't start returning `false` again.
public function bool IsCompleted();
/// Called when scheduler decides that [`SchedulerJob`] should be executed, taking amount of abstract
/// "work units" that it is allowed to spend for work.
///
/// By default there is `10000` work units per second, so you can expect about 10000 / 1000 = 10
/// work units per millisecond or, on servers with `30` tick rate, about `10000 * (30 / 1000) = 300`
/// work units per tick to be allotted to all the scheduled jobs.
public function DoWork(int allottedWorkUnits);
defaultproperties {
}

0
sources/BaseRealm/API/Scheduler/API/MockJob.uc → sources/BaseAPI/API/Scheduler/Tests/MockJob.uc

0
sources/BaseRealm/API/Scheduler/API/TEST_SchedulerAPI.uc → sources/BaseAPI/API/Scheduler/Tests/TEST_SchedulerAPI.uc

277
sources/BaseAPI/API/SideEffects/SideEffect.uc

@ -0,0 +1,277 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SideEffect extends AcediaObject;
//! Defines the concept of "side effects" in the context of the Acedia and its derivative mods.
//!
//! Side effects are changes that are not part of the mod's main functionality, but rather something
//! necessary to enable that functionality, while also possibly affecting how other mods work.
//! Documenting these side effects helps developers and server admins understand changes performed
//! by Acedia or mods based on it, and anticipate any potential conflicts or issues that may arise.
//!
//! It should be noted that what constitutes a side effect is loosely defined, and it is simply
//! a tool to inform others that something unexpected has happened, possibly breaking other mods.
//! AcediaCore aims to leave a minimal footprint, but still needs to make some changes
//! (e.g., adding GameRules, patching code of some functions), and [`SideEffects`] can be used to
//! document them.
//! Similarly, [`SideEffect`]s can be used to document changes made by AcediaFixes, a package meant
//! only for fixing bugs that inevitably needs to make many under-the-hood changes to achieve
//! that goal.
//!
//! On the other hand gameplay mods like Futility or Ire can make a lot of changes, but they can all
//! be just expected part of its direct functionality: we expect feature that shares dosh of leavers
//! to alter players' dosh values, so this is not a side effect.
//! Such mods are likely not going to have to specify any side effects whatsoever.
var private Text name;
var private Text description;
var private Text package;
var private Text source;
var private Text status;
var private bool initialized;
protected function Finalizer() {
_.memory.Free(name);
_.memory.Free(description);
_.memory.Free(package);
_.memory.Free(source);
_.memory.Free(status);
name = none;
description = none;
package = none;
source = none;
status = none;
initialized = false;
}
/// Checks whether caller [`SideEffect`] was initialized.
///
/// Initialization must happen directly after creation and only initialized instances should
/// ever be used.
public final function bool IsInitialized() {
return initialized;
}
/// This function is used to set the initial values of the [`SideEffect`] object properties when it
/// is first created.
///
/// All arguments must be not `none`.
///
/// Returns `true` if the initialization was successful, `false` otherwise (including the case where
/// the [`SideEffect`] object has already been initialized).
public final function bool Initialize(
BaseText sideEffectName,
BaseText sideEffectDescription,
BaseText sideEffectPackage,
BaseText sideEffectSource,
BaseText sideEffectStatus
) {
if (initialized) return false;
if (sideEffectName == none) return false;
if (sideEffectDescription == none) return false;
if (sideEffectPackage == none) return false;
if (sideEffectSource == none) return false;
if (sideEffectStatus == none) return false;
name = sideEffectName.Copy();
description = sideEffectDescription.Copy();
package = sideEffectPackage.Copy();
source = sideEffectSource.Copy();
status = sideEffectStatus.Copy();
initialized = true;
return true;
}
/// This function is used to set the initial values of the [`SideEffect`] object properties when it
/// is first created.
///
/// Returns `true` if the initialization was successful, `false` otherwise (including the case where
/// the [`SideEffect`] object has already been initialized).
public final function bool Initialize_S(
string sideEffectName,
string sideEffectDescription,
string sideEffectPackage,
string sideEffectSource,
string sideEffectStatus
) {
name = _.text.FromString(sideEffectName);
description = _.text.FromString(sideEffectDescription);
package = _.text.FromString(sideEffectPackage);
source = _.text.FromString(sideEffectSource);
status = _.text.FromString(sideEffectStatus);
initialized = true;
return true;
}
/// Returns a brief summary that conveys the purpose of the caller [SideEffect] to the user in
/// a clear and concise manner.
///
/// While there is no hard limit on the length of this value, it is recommended to keep it under 80
/// characters for readability.
///
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is
/// a required property of the [`SideEffect`] object.
public final function Text GetName() {
if (initialized) {
return name.Copy();
}
return none;
}
/// Returns a brief summary that conveys the purpose of the caller [SideEffect] to the user in
/// a clear and concise manner.
///
/// While there is no hard limit on the length of this value, it is recommended to keep it under 80
/// characters for readability.
public final function string GetName_S() {
if (initialized && name != none) {
return name.ToString();
}
return "";
}
/// Returns the detailed description of the caller [`SideEffect`], which describes what was done
/// and why the relevant change was necessary.
///
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is
/// a required property of the [`SideEffect`] object.
public final function Text GetDescription() {
if (initialized) {
return description.Copy();
}
return none;
}
/// Returns the detailed description of the caller [`SideEffect`], which describes what was done
/// and why the relevant change was necessary.
public final function string GetDescription_S() {
if (initialized && description != none) {
return description.ToString();
}
return "";
}
/// Returns the name of the package ("*.u" file) that introduced the changes
/// represented by the caller `SideEffect`.
///
/// It should be noted that even if a different package requested the functionality that led to
/// the changes being made, the package responsible for the side effect is the one that performed
/// the changes.
///
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is
/// a required property of the [`SideEffect`] object.
public final function Text GetPackage() {
if (initialized) {
return package.Copy();
}
return none;
}
/// Returns the name of the package ("*.u" file) that introduced the changes
/// represented by the caller `SideEffect`.
///
/// It should be noted that even if a different package requested the functionality that led to
/// the changes being made, the package responsible for the side effect is the one that performed
/// the changes.
public final function string GetPackage_S() {
if (initialized && package != none) {
return package.ToString();
}
return "";
}
/// The origin of this change within the package is specified, and for larger packages, additional
/// details can be provided to clarify the cause of the change.
///
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is
/// a required property of the [`SideEffect`] object.
public final function Text GetSource() {
if (initialized) {
return source.Copy();
}
return none;
}
/// The origin of this change within the package is specified, and for larger packages, additional
/// details can be provided to clarify the cause of the change.
public final function string GetSource_S() {
if (initialized && source != none) {
return source.ToString();
}
return "";
}
/// The status of the caller [`SideEffect`], that is used to differentiate between different ways
/// that a side effect may have been introduced, allowing for better tracking and management of
/// the effect.
///
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is
/// a required property of the [`SideEffect`] object.
public final function Text GetStatus() {
if (initialized) {
return status.Copy();
}
return none;
}
/// The status of the caller [`SideEffect`], that is used to differentiate between different ways
/// that a side effect may have been introduced, allowing for better tracking and management of
/// the effect.
public final function string GetStatus_S() {
if (initialized && status != none) {
return status.ToString();
}
return "";
}
public function bool IsEqual(Object other) {
local SideEffect otherSideEffect;
if (self == other) return true;
otherSideEffect = SideEffect(other);
if (otherSideEffect == none) return false;
if (!otherSideEffect.initialized) return false;
if (GetHashCode() != otherSideEffect.GetHashCode()) return false;
if (!name.Compare(otherSideEffect.name,, SFORM_SENSITIVE)) return false;
if (!package.Compare(otherSideEffect.package,, SFORM_SENSITIVE)) return false;
if (!source.Compare(otherSideEffect.source,, SFORM_SENSITIVE)) return false;
if (!status.Compare(otherSideEffect.status,, SFORM_SENSITIVE)) return false;
if (!description.Compare(otherSideEffect.description,, SFORM_SENSITIVE)) return false;
return true;
}
protected function int CalculateHashCode() {
local int result;
if (initialized) {
result = name.GetHashCode();
result = CombineHash(result, description.GetHashCode());
result = CombineHash(result, package.GetHashCode());
result = CombineHash(result, source.GetHashCode());
result = CombineHash(result, status.GetHashCode());
return result;
}
}
defaultproperties {
}

212
sources/BaseAPI/API/SideEffects/SideEffectAPI.uc

@ -0,0 +1,212 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SideEffectAPI extends AcediaObject;
var private array<SideEffect> activeSideEffects;
/// Returns an array containing all SideEffect objects that have been registered up to this point.
///
/// The order of the elements in the array is not guaranteed.
public function array<SideEffect> GetAll() {
local int i;
for (i = 0; i < activeSideEffects.length; i += 1) {
activeSideEffects[i].NewRef();
}
return activeSideEffects;
}
/// Returns all registered [`SideEffects`] that are associated with the specified package name
/// (case-insensitive).
public function array<SideEffect> GetFromPackage(BaseText packageName) {
local int i;
local Text nextPackage;
local array<SideEffect> result;
if (packageName == none) {
return result;
}
for (i = 0; i < activeSideEffects.length; i += 1) {
nextPackage = activeSideEffects[i].GetPackage();
if (packageName.Compare(nextPackage, SCASE_INSENSITIVE)) {
activeSideEffects[i].NewRef();
result[result.length] = activeSideEffects[i];
}
_.memory.Free(nextPackage);
}
return result;
}
/// Adds a new side effect to the list of active side effects.
///
/// This method will fail if any of its arguments are `none` or a side effect with that exact
/// contents was already added.
public function SideEffect Add(
BaseText sideEffectName,
BaseText sideEffectDescription,
BaseText sideEffectPackage,
BaseText sideEffectSource,
BaseText sideEffectStatus
) {
local bool initialized;
local SideEffect newSideEffect;
newSideEffect = SideEffect(_.memory.Allocate(class'SideEffect'));
initialized = newSideEffect.Initialize(
sideEffectName,
sideEffectDescription,
sideEffectPackage,
sideEffectSource,
sideEffectStatus);
if (initialized) {
if (!AddInstance(newSideEffect)) {
_.memory.Free(newSideEffect);
return none;
}
} else {
_.memory.Free(newSideEffect);
return none;
}
return newSideEffect;
}
/// Adds a new side effect to the list of active side effects.
///
/// This method will fail if a side effect with that exact contents was already added.
public function SideEffect Add_S(
string sideEffectName,
string sideEffectDescription,
string sideEffectPackage,
string sideEffectSource,
string sideEffectStatus
) {
local bool initialized;
local SideEffect newSideEffect;
newSideEffect = SideEffect(_.memory.Allocate(class'SideEffect'));
initialized = newSideEffect.Initialize_S(
sideEffectName,
sideEffectDescription,
sideEffectPackage,
sideEffectSource,
sideEffectStatus);
if (initialized) {
if (!AddInstance(newSideEffect)) {
_.memory.Free(newSideEffect);
return none;
}
} else {
return none;
}
return newSideEffect;
}
/// Checks whether specified [`SideEffect`] is currently active.
///
/// Check is done via contents and not instance equality.
/// Returns `true` if specified [`SideEffect`] is currently active and `false` otherwise.
public function bool IsRegistered(SideEffect sideEffectToCheck) {
local int i;
if (sideEffectToCheck == none) return false;
if (!sideEffectToCheck.IsInitialized()) return false;
for (i = 0; i < activeSideEffects.length; i += 1) {
if (activeSideEffects[i].IsEqual(sideEffectToCheck)) {
return true;
}
}
return false;
}
/// Adds a new side effect to the list of active side effects.
///
/// This method will fail if its argument is `none`, non-initialized or a side effect with that
/// exact contents was already added.
public function bool AddInstance(SideEffect newSideEffect) {
local int i;
if (newSideEffect == none) return false;
if (!newSideEffect.IsInitialized()) return false;
for (i = 0; i < activeSideEffects.length; i += 1) {
if (activeSideEffects[i].IsEqual(newSideEffect)) {
return false;
}
}
newSideEffect.NewRef();
activeSideEffects[activeSideEffects.length] = newSideEffect;
LogAddingSideEffectChange(newSideEffect, true);
return true;
}
/// Removes a side effect from the list of active side effects.
///
/// This method will fail if its argument is `none`, non-initialized or a side effect with its
/// contents isn't in the records.
public function bool RemoveInstance(SideEffect inactiveSideEffect) {
local int i;
local bool foundInstance;
if (inactiveSideEffect == none) {
return false;
}
for (i = 0; i < activeSideEffects.length; i += 1) {
if (activeSideEffects[i].IsEqual(inactiveSideEffect)) {
LogAddingSideEffectChange(activeSideEffects[i], false);
_.memory.Free(activeSideEffects[i]);
activeSideEffects.Remove(i, 1);
foundInstance = true;
}
}
return foundInstance;
}
private function LogAddingSideEffectChange(SideEffect effect, bool added) {
local MutableText builder;
local Text sideEffectData;
if (effect == none) {
return;
}
builder = _.text.Empty();
if (added) {
builder.Append(P("NEW SIDE EFFECT: "));
} else {
builder.Append(P("REMOVED SIDE EFFECT: "));
}
sideEffectData = effect.GetName();
builder.Append(sideEffectData);
_.memory.Free(sideEffectData);
sideEffectData = effect.GetStatus();
if (sideEffectData != none) {
builder.Append(P(" {"));
builder.Append(sideEffectData);
_.memory.Free(sideEffectData);
builder.Append(P("}"));
}
_.logger.Info(builder);
builder.FreeSelf();
}
defaultproperties {
}

76
sources/BaseAPI/API/Unflect/FunctionReplacement.uc

@ -0,0 +1,76 @@
/**
* Config class for storing map lists.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class FunctionReplacement extends AcediaObject;
var private Text replaced;
var private Text replacer;
var private SideEffect effect;
protected function Finalizer() {
_.memory.Free(replaced);
_.memory.Free(replacer);
_.memory.Free(effect);
replaced = none;
replacer = none;
effect = none;
}
public static final function FunctionReplacement Make(
BaseText oldFunction,
BaseText newFunction,
SideEffect sideEffect
) {
local FunctionReplacement newReplacement;
if (oldFunction == none) return none;
if (newFunction == none) return none;
if (sideEffect == none) return none;
newReplacement = FunctionReplacement(__().memory.Allocate(class'FunctionReplacement'));
newReplacement.replaced = oldFunction.Copy();
newReplacement.replacer = newFunction.Copy();
sideEffect.NewRef();
newReplacement.effect = sideEffect;
return newReplacement;
}
public final function Text GetReplacedFunctionName() {
return replaced.Copy();
}
public final function string GetReplacedFunctionName_S() {
return replaced.ToString();
}
public final function Text GetReplacerFunctionName() {
return replacer.Copy();
}
public final function string GetReplacerFunctionName_S() {
return replacer.ToString();
}
public final function SideEffect GetSideEffect() {
effect.NewRef();
return effect;
}
defaultproperties {
}

30
sources/BaseAPI/API/Unflect/Tests/MockInitialClass.uc

@ -0,0 +1,30 @@
/**
* Config class for storing map lists.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockInitialClass extends AcediaObject;
var public int counter;
public final function int DoIt() {
counter += 1;
return counter;
}
defaultproperties {
}

33
sources/BaseAPI/API/Unflect/Tests/MockReplacerClass.uc

@ -0,0 +1,33 @@
/**
* Config class for storing map lists.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockReplacerClass extends MockInitialClass;
public final function int DoIt2() {
counter += 2;
return -counter;
}
public final function int DoIt3() {
counter -= 1;
return 7;
}
defaultproperties {
}

87
sources/BaseAPI/API/Unflect/Tests/TEST_Unflect.uc

@ -0,0 +1,87 @@
/**
* Set of tests for `Command` class.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_Unflect extends TestCase
abstract;
protected static function TESTS() {
local MockInitialClass obj;
obj = MockInitialClass(__().memory.Allocate(class'MockInitialClass'));
Context("Replacing functions with `UnflectApi`");
Test_InitialReplacement(obj);
Test_SecondReplacement(obj);
Test_ReplacementWithSelf(obj);
Test_RevertingReplacement(obj);
}
protected static function Test_InitialReplacement(MockInitialClass obj) {
Issue("Functions aren't being replaced correctly the first time.");
TEST_ExpectTrue(__().unflect.ReplaceFunction_S(
"AcediaCore.MockInitialClass.DoIt",
"AcediaCore.MockReplacerClass.DoIt2",
"testing"));
TEST_ExpectTrue(obj.DoIt() == -2);
TEST_ExpectTrue(obj.counter == 2);
TEST_ExpectTrue(obj.DoIt() == -4);
TEST_ExpectTrue(obj.counter == 4);
}
protected static function Test_SecondReplacement(MockInitialClass obj) {
Issue("Functions aren't being replaced correctly in case they were already replaced.");
TEST_ExpectTrue(__().unflect.ReplaceFunction_S(
"AcediaCore.MockInitialClass.DoIt",
"AcediaCore.MockReplacerClass.DoIt3",
"testing"));
TEST_ExpectTrue(obj.DoIt() == 7);
TEST_ExpectTrue(obj.counter == 3);
TEST_ExpectTrue(obj.DoIt() == 7);
TEST_ExpectTrue(obj.counter == 2);
}
protected static function Test_ReplacementWithSelf(MockInitialClass obj) {
Issue("Attempting to replacing function with itself makes unexpected change.");
TEST_ExpectFalse(__().unflect.ReplaceFunction_S(
"AcediaCore.MockInitialClass.DoIt",
"AcediaCore.MockInitialClass.DoIt",
"testing"));
TEST_ExpectTrue(obj.DoIt() == 7);
TEST_ExpectTrue(obj.counter == 1);
}
protected static function Test_RevertingReplacement(MockInitialClass obj) {
Issue("Reverting replaced function doesn't work.");
TEST_ExpectTrue(__().unflect.RevertFunction_S("AcediaCore.MockInitialClass.DoIt"));
TEST_ExpectTrue(obj.DoIt() == 2);
TEST_ExpectTrue(obj.counter == 2);
TEST_ExpectTrue(obj.DoIt() == 3);
TEST_ExpectTrue(obj.counter == 3);
Issue("Reverting already reverted function ends in success.");
TEST_ExpectFalse(__().unflect.RevertFunction_S("AcediaCore.MockInitialClass.DoIt"));
Issue("Reverting already reverted function leads to unexpected results.");
TEST_ExpectTrue(obj.DoIt() == 4);
TEST_ExpectTrue(obj.counter == 4);
}
defaultproperties {
caseName = "Function replacement"
caseGroup = "Unflect"
}

29
sources/BaseAPI/API/Unflect/TypeCast.uc

@ -0,0 +1,29 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TypeCast extends Object;
var Object nativeType;
final function NativeCast(Object type) {
nativeType = type;
}
defaultproperties {
}

49
sources/BaseAPI/API/Unflect/UClass.uc

@ -0,0 +1,49 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UClass extends UState within Package;
var int classFlags;
var int classUnique;
var Guid classGuid;
var UClass classWithin;
var name classConfigName;
var array<struct RepRecord {
var UProperty property;
var int index;
}> classReps;
var array<UField> netFields;
var array<struct Dependency {
var UClass class;
var int deep;
var int scriptTextCRC;
}> dependencies;
var array<name> packageImports;
var array<byte> defaults;
var array<name> hideCategories;
var array<name> dependentOn;
var string defaultPropText;
defaultproperties {
}

30
sources/BaseAPI/API/Unflect/UClassCast.uc

@ -0,0 +1,30 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UClassCast extends Object;
var UClass nativeType;
final function UClass Cast(Class type) {
super(TypeCast).NativeCast(type);
return nativeType;
}
defaultproperties {
}

28
sources/BaseAPI/API/Unflect/UField.uc

@ -0,0 +1,28 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UField extends Object
abstract;
var UField superField;
var UField next;
var UField hashNext;
defaultproperties {
}

30
sources/BaseAPI/API/Unflect/UFieldCast.uc

@ -0,0 +1,30 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UFieldCast extends Object;
var UField nativeType;
final function UField Cast(Field type) {
super(TypeCast).NativeCast(type);
return nativeType;
}
defaultproperties {
}

35
sources/BaseAPI/API/Unflect/UFunction.uc

@ -0,0 +1,35 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UFunction extends UStruct within UState
dependson(Unflect);
var byte functionMD5Digest[16];
var int functionFlags;
var Unflect.Int16 nativeIndex;
var Unflect.Int16 repOffset;
var byte operPrecedence;
var byte numParms;
var Unflect.Int16 parmsSize;
var Unflect.Int16 returnValueOffset;
defaultproperties {
}

17
sources/BaseRealm/API/Scheduler/SchedulerDiskRequest.uc → sources/BaseAPI/API/Unflect/UFunctionCast.uc

@ -1,7 +1,6 @@
/**
* Slot-like object that represents a request for a writing disk access,
* capable of being scheduled on the `SchedulerAPI`.
* Copyright 2022 Anton Tarasenko
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -18,12 +17,14 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SchedulerDiskRequest extends AcediaObject;
class UFunctionCast extends Object;
delegate connect()
{
var UFunction nativeType;
final function UFunction Cast(Function type) {
super(TypeCast).NativeCast(type);
return nativeType;
}
defaultproperties
{
defaultproperties {
}

41
sources/BaseAPI/API/Unflect/UProperty.uc

@ -0,0 +1,41 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UProperty extends UField within UField
abstract;
var int arrayDim;
var int elementSize;
var int propertyFlags;
var name category;
var byte repOffset[2];
var byte repIndex[2];
var transient int offset;
var transient UProperty propertyLinkNext;
var transient UProperty configLinkNext;
var transient UProperty constructorLinkNext;
var transient UProperty nextRef;
var transient UProperty repOwner;
var string commentString;
defaultproperties {
}

30
sources/BaseAPI/API/Unflect/UPropertyCast.uc

@ -0,0 +1,30 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UPropertyCast extends Object;
var UProperty nativeType;
final function UProperty Cast(Property type) {
super(TypeCast).NativeCast(type);
return nativeType;
}
defaultproperties {
}

30
sources/BaseAPI/API/Unflect/UState.uc

@ -0,0 +1,30 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UState extends UStruct
dependson(Unflect);
var Unflect.Int64 probeMask;
var Unflect.Int64 ignoreMask;
var int stateFlags;
var Unflect.Int16 labelTableOffset;
var UField vfHash[256];
defaultproperties {
}

30
sources/BaseAPI/API/Unflect/UStateCast.uc

@ -0,0 +1,30 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UStateCast extends Object;
var UState nativeType;
final function UState Cast(State type) {
super(TypeCast).NativeCast(type);
return nativeType;
}
defaultproperties {
}

48
sources/BaseAPI/API/Unflect/UStruct.uc

@ -0,0 +1,48 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UStruct extends UField;
var UTextBuffer scriptText;
var UTextBuffer cppText;
var UField children;
var int propertiesSize;
var name friendlyName;
var array<byte> script;
var int textPos;
var int line;
var struct EStructFlags {
var bool native;
var bool export;
var bool long;
var bool init;
var bool unused1;
var bool unused2;
var bool unused3;
var bool unused4;
} StructFlags;
var Property refLink;
var Property propertyLink;
var Property configLink;
var Property constructorLink;
defaultproperties {
}

30
sources/BaseAPI/API/Unflect/UStructCast.uc

@ -0,0 +1,30 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UStructCast extends Object;
var UStruct nativeType;
final function UStruct Cast(/*Core.Struct*/ Object type) {
super(TypeCast).NativeCast(type);
return nativeType;
}
defaultproperties {
}

28
sources/BaseAPI/API/Unflect/UTextBuffer.uc

@ -0,0 +1,28 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UTextBuffer extends Object;
var private native const pointer outputDeviceVtbl;
var int pos, top;
var string text;
defaultproperties {
}

32
sources/BaseAPI/API/Unflect/Unflect.uc

@ -0,0 +1,32 @@
/**
* One of the original Unflect files.
* Copyright 2022-2023 EliotVU
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Unflect extends Object
abstract;
struct Int16 {
var byte h, l;
};
struct Int64 {
var int h, l;
};
defaultproperties {
}

432
sources/BaseAPI/API/Unflect/UnflectApi.uc

@ -0,0 +1,432 @@
/**
* Config class for storing map lists.
* Copyright 2020 bibibi
* 2020-2023 Shtoyan
* 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UnflectApi extends AcediaObject;
//! This API offers advanced reflection capabilities for Unreal Script.
//!
//! Currently, the API supports the ability to replace the code of existing functions with
//! custom code.
//! This can greatly simplify the process of bug fixing and hooking into game events.
/// A variable responsible for replacing function code in real-time.
/// This variable is used to support dynamic function replacement/patching and event interception
/// at runtime.
var private UFunctionCast functionCaster;
/// Maps lower case function name (specifies by the full path "package.class.functionName")
/// to a `FunctionRule` that completely describes how it was replaced
var private HashTable completedReplacements;
/// Maps lower case function name (specifies by the full path "package.class.functionName")
/// to the `ByteArrayBox` with that function's original code.
var private HashTable originalScriptCodes;
var private LoggerAPI.Definition warnSameFunction;
var private LoggerAPI.Definition warnOverridingReplacement, errFailedToFindFunction;
var private LoggerAPI.Definition errReplacementWithoutSources, errCannotCreateReplacementRule;
protected function Constructor() {
functionCaster = new class'UFunctionCast';
completedReplacements = _.collections.EmptyHashTable();
originalScriptCodes = _.collections.EmptyHashTable();
}
protected function Finalizer() {
_drop();
_.memory.Free(completedReplacements);
_.memory.Free(originalScriptCodes);
completedReplacements = none;
originalScriptCodes = none;
functionCaster = none;
}
public final function _drop() {
local UFunction nextFunctionInstance;
local Text nextFunctionName;
local HashTableIterator iter;
local ByteArrayBox nextSources;
// Drop is called when Acedia is shutting down, so releasing references isn't necessary
iter = HashTableIterator(completedReplacements.Iterate());
while (!iter.HasFinished()) {
nextFunctionInstance = none;
nextFunctionName = Text(iter.GetKey());
nextSources = ByteArrayBox(originalScriptCodes.GetItem(nextFunctionName));
if (nextSources != none ) {
nextFunctionInstance = FindFunction(nextFunctionName);
}
if (nextFunctionInstance != none) {
nextFunctionInstance.script = nextSources.Get();
}
iter.Next();
}
}
/// Reverts the replacement of the function's code, restoring its original behavior.
///
/// The function to be reverted should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// It's worth noting that several function replacements do not stack.
/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function,
/// this method will cancel all the changes at once.
///
/// This method returns true if the specified function was previously replaced and has now been
/// successfully reverted.
///
/// # Errors
///
/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if
/// UnflectApi has not yet replaced it with any other function, this method will log an error.
public final function bool RevertFunction(BaseText functionName) {
local bool result;
local FunctionReplacement storedReplacement;
local ByteArrayBox storedSources;
local Text functionNameLowerCase;
local UFunction functionInstance;
local SideEffect sideEffect;
if (functionName == none) {
return false;
}
functionNameLowerCase = functionName.LowerCopy();
storedReplacement = FunctionReplacement(completedReplacements.GetItem(functionNameLowerCase));
if (storedReplacement != none) {
storedSources = ByteArrayBox(originalScriptCodes.GetItem(functionNameLowerCase));
if (storedSources == none) {
_.logger.Auto(errReplacementWithoutSources).Arg(functionNameLowerCase.Copy());
} else {
functionInstance = FindFunction(functionNameLowerCase);
if (functionInstance != none) {
functionInstance.script = storedSources.Get();
completedReplacements.RemoveItem(functionNameLowerCase);
sideEffect = storedReplacement.GetSideEffect();
_.sideEffects.RemoveInstance(sideEffect);
result = true;
} else {
_.logger.Auto(errFailedToFindFunction).Arg(functionNameLowerCase.Copy());
}
}
}
_.memory.Free4(storedReplacement, functionNameLowerCase, storedSources, sideEffect);
return result;
}
/// Reverts the replacement of the function's code, restoring its original behavior.
///
/// The function to be reverted should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// It's worth noting that several function replacements do not stack.
/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function,
/// this method will cancel all the changes at once.
///
/// This method returns true if the specified function was previously replaced and has now been
/// successfully reverted.
///
/// # Errors
///
/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if
/// UnflectApi has not yet replaced it with any other function, this method will log an error.
public final function bool RevertFunction_S(string functionName) {
local bool result;
local MutableText wrapper;
wrapper = _.text.FromStringM(functionName);
result = RevertFunction(wrapper);
_.memory.Free(wrapper);
return result;
}
/// Determines whether the specified function has been replaced by UnflectApi.
///
/// The function to be checked should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// If the function has been replaced, this method will return `true`;
/// otherwise, it will return `false`.
public final function bool IsFunctionReplaced(BaseText functionName) {
local bool result;
local Text functionNameLowerCase;
if (functionName == none) {
return false;
}
functionNameLowerCase = functionName.LowerCopy();
result = completedReplacements.HasKey(functionNameLowerCase);
_.memory.Free(functionNameLowerCase);
return result;
}
/// Determines whether the specified function has been replaced by UnflectApi.
///
/// The function to be checked should be specified using its full path, in the format
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash").
///
/// If the function has been replaced, this method will return `true`;
/// otherwise, it will return `false`.
public final function bool IsFunctionReplaced_S(string functionName) {
local bool result;
local MutableText wrapper;
wrapper = _.text.FromStringM(functionName);
result = IsFunctionReplaced(wrapper);
_.memory.Free(wrapper);
return result;
}
/// Replaces one function with another by modifying its script code in real-time.
///
/// The reason for replacement must be specified and will serve as a human-readable explanation
/// for a [SideEffect] associated with the replacement.
///
/// If you need to replace a function in a class, follow these steps:
///
/// 1. Create a new class that extends the class in which the function you want to replace is
/// located.
/// 2. Declare that function in the created class.
/// 3. **DO NOT** change the function declaration and argument types/amount.
/// 4. **DO NOT** create new local variables, as this can cause random crashes.
/// If you need additional variables, make them global and access them using the
/// `class'myNewClass'.default.myNewVariable` syntax.
/// 5. If you want to call or override parent code, make sure to always specify the desired parent
/// class name.
/// For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`.
/// This will prevent runaway loop crashes.
/// 6. Make your edits to the function's code, and then call the replacement function:
/// ```unrealscript
/// _.unflect.ReplaceFunction(
/// "package.class.targetFunction",
/// "myNewPackage.myNewClass.newFunction");
/// ```
///
/// Following these steps will help ensure that your code changes are compatible with the rest of
/// the codebase and do not cause unexpected crashes.
///
/// # Errors
///
/// This method can log error messages in cases where:
///
/// * The specified function(s) cannot be found.
/// * An attempt is made to replace a function with itself.
/// * An attempt is made to replace a function that has already been replaced.
public final function bool ReplaceFunction(
BaseText oldFunction,
BaseText newFunction,
BaseText replacementReason
) {
local bool result;
local Text oldFunctionLowerCase, newFunctionLowerCase;
if (oldFunction == none) return false;
if (newFunction == none) return false;
oldFunctionLowerCase = oldFunction.LowerCopy();
newFunctionLowerCase = newFunction.LowerCopy();
result = _replaceFunction(oldFunctionLowerCase, newFunctionLowerCase);
if (result) {
RecordNewReplacement(oldFunctionLowerCase, newFunctionLowerCase, replacementReason);
}
_.memory.Free2(oldFunctionLowerCase, newFunctionLowerCase);
return result;
}
/// Replaces one function with another by modifying its script code in real-time.
///
/// The reason for replacement must be specified and will serve as a human-readable explanation
/// for a [SideEffect] associated with the replacement.
///
/// If you need to replace a function in a class, follow these steps:
///
/// 1. Create a new class that extends the class in which the function you want to replace is
/// located.
/// 2. Declare that function in the created class.
/// 3. **DO NOT** change the function declaration and argument types/amount.
/// 4. **DO NOT** create new local variables, as this can cause random crashes.
/// If you need additional variables, make them global and access them using the
/// `class'myNewClass'.default.myNewVariable` syntax.
/// 5. If you want to call or override parent code, make sure to always specify the desired parent
/// class name.
/// For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`.
/// This will prevent runaway loop crashes.
/// 6. Make your edits to the function's code, and then call the replacement function:
/// ```unrealscript
/// _.unflect.ReplaceFunction(
/// "package.class.targetFunction",
/// "myNewPackage.myNewClass.newFunction");
/// ```
///
/// Following these steps will help ensure that your code changes are compatible with the rest of
/// the codebase and do not cause unexpected crashes.
///
/// # Errors
///
/// This method can log error messages in cases where:
///
/// * The specified function(s) cannot be found.
/// * An attempt is made to replace a function with itself.
/// * An attempt is made to replace a function that has already been replaced.
public final function bool ReplaceFunction_S(
string oldFunction,
string newFunction,
string replacementReason
) {
local Text oldWrapper, newWrapper, reasonWrapper;
local bool result;
oldWrapper = _.text.FromString(oldFunction);
newWrapper = _.text.FromString(newFunction);
reasonWrapper = _.text.FromString(replacementReason);
result = ReplaceFunction(oldWrapper, newWrapper, reasonWrapper);
_.memory.Free3(oldWrapper, newWrapper, reasonWrapper);
return result;
}
// Does actual work for function replacement.
// Arguments are assumed to be not `none` and in lower case.
private final function bool _replaceFunction(Text oldFunctionLowerCase, Text newFunctionLowerCase) {
local ByteArrayBox initialCode;
local UFunction replace, with;
replace = FindFunction(oldFunctionLowerCase);
if (replace == none) {
_.logger.Auto(errFailedToFindFunction).Arg(oldFunctionLowerCase.Copy());
return false;
}
with = FindFunction(newFunctionLowerCase);
if (with == none) {
_.logger.Auto(errFailedToFindFunction).Arg(newFunctionLowerCase.Copy());
return false;
}
if (replace == with) {
_.logger.Auto(warnSameFunction).Arg(oldFunctionLowerCase.Copy());
return false;
}
// Remember old code, if haven't done so yet.
// Since we attempt it on each replacement, the first recorded `script` value will be
// the initial code.
if (!originalScriptCodes.HasKey(oldFunctionLowerCase)) {
initialCode = _.box.ByteArray(replace.script);
originalScriptCodes.SetItem(oldFunctionLowerCase, initialCode);
_.memory.Free(initialCode);
}
replace.script = with.script;
return true;
}
// Arguments assumed to be not `none` and in lower case.
private final function UFunction FindFunction(Text functionNameLowerCase) {
local string stringName;
stringName = functionNameLowerCase.ToString();
// We need to make sure functions are loaded before performing the replacement.
DynamicLoadObject(GetClassName(stringName), class'class', true);
return functionCaster.Cast(function(FindObject(stringName, class'Function')));
}
// Arguments are assumed to be not `none`.
// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case.
private final function RecordNewReplacement(
Text oldFunctionLowerCase,
Text newFunctionLowerCase,
BaseText replacementReason
) {
local SideEffect oldSideEffect, newSideEffect;
local FunctionReplacement oldRule, newRule;
// Remove old `FunctionReplacement`, if there is any
oldRule = FunctionReplacement(completedReplacements.GetItem(oldFunctionLowerCase));
if (oldRule != none) {
_.logger
.Auto(warnOverridingReplacement)
.Arg(oldFunctionLowerCase.Copy())
.Arg(oldRule.GetReplacerFunctionName())
.Arg(newFunctionLowerCase.Copy());
oldSideEffect = oldRule.GetSideEffect();
_.sideEffects.RemoveInstance(oldSideEffect);
_.memory.Free2(oldRule, oldSideEffect);
}
// Create replacement instance
newSideEffect = MakeSideEffect(oldFunctionLowerCase, newFunctionLowerCase, replacementReason);
newRule = class'FunctionReplacement'.static
.Make(oldFunctionLowerCase, newFunctionLowerCase, newSideEffect);
completedReplacements.SetItem(oldFunctionLowerCase, newRule);
if (newRule == none) {
_.logger
.Auto(errCannotCreateReplacementRule)
.Arg(oldFunctionLowerCase.Copy())
.Arg(newFunctionLowerCase.Copy());
}
}
// Arguments are assumed to be not `none`.
// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case.
private final function SideEffect MakeSideEffect(
Text oldFunctionLowerCase,
Text newFunctionLowerCase,
BaseText replacementReason
) {
local SideEffect sideEffect;
local MutableText status;
// Add side effect
status = oldFunctionLowerCase.MutableCopy();
status.Append(P(" -> "));
status.Append(newFunctionLowerCase);
sideEffect = _.sideEffects.Add(
P("Changed function's code"),
replacementReason,
P("AcediaCore"),
P("UnflectAPI"),
status
);
_.memory.Free(status);
return sideEffect;
}
// Get the "package + dot + class" string for DynamicLoadObject()
private final static function string GetClassName(string input) {
local array<string> parts;
// create an array
Split(input, ".", parts);
// state functions
if (parts.length < 3) {
return "";
}
if (parts.length == 4) {
ReplaceText(input, "." $ parts[2], "");
ReplaceText(input, "." $ parts[3], "");
} else {
ReplaceText(input, "." $ parts[2], "");
}
return input;
}
defaultproperties {
warnOverridingReplacement = (l=LOG_Error,m="Attempt to replace a function `%1` with function `%3` after it has already been replaced with `%2`.")
warnSameFunction = (l=LOG_Error,m="Attempt to replace a function `%1` with itself.")
errFailedToFindFunction = (l=LOG_Error,m="`UnflectApi` has failed to find function `%1`.")
errReplacementWithoutSources = (l=LOG_Error,m="Cannot restore function `%1` - its initial source code wasn't preserved. This most likely means that it wasn't yet replaced.")
errCannotCreateReplacementRule = (l=LOG_Error,m="`Cannot create new rule for replacing function `%1` with `%2` even though code was successfully replaces. This should happen, please report this.")
}

411
sources/BaseAPI/AcediaEnvironment/AcediaEnvironment.uc

@ -0,0 +1,411 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaEnvironment extends AcediaObject
config(AcediaSystem);
//! API for management of running `Feature`s and loaded packages.
//!
//! Instance of this class will be used by Acedia to manage resources available
//! from different packages like `Feature`s and such other etc..
//! This is mostly necessary to implement Acedia loader (and, possibly,
//! its alternatives) that would load available packages and enable `Feature`s
//! admin wants to be enabled.
var private bool acediaShutDown;
var private array< class<_manifest> > availablePackages;
var private array< class<Feature> > availableFeatures;
var private array<Feature> enabledFeatures;
var private array<int> enabledFeaturesLifeVersions;
var private string manifestSuffix;
var private const config bool debugMode;
var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered;
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled;
var private LoggerAPI.Definition warnFeatureAlreadyEnabled;
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled;
var private SimpleSignal onShutdownSignal;
var private SimpleSignal onShutdownSystemSignal;
var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal;
var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal;
protected function Constructor() {
// Always register our core package
RegisterPackage_S("AcediaCore");
onShutdownSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onShutdownSystemSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onFeatureEnabledSignal = Environment_FeatureEnabled_Signal(
_.memory.Allocate(class'Environment_FeatureEnabled_Signal'));
onFeatureDisabledSignal = Environment_FeatureDisabled_Signal(
_.memory.Allocate(class'Environment_FeatureDisabled_Signal'));
}
protected function Finalizer() {
_.memory.Free(onShutdownSignal);
_.memory.Free(onShutdownSystemSignal);
_.memory.Free(onFeatureEnabledSignal);
_.memory.Free(onFeatureDisabledSignal);
}
/// Signal that will be emitted right before Acedia shuts down.
///
/// At the point of emission all APIs should still exist and function.
///
/// # Signature
///
/// void <slot>()
public final /*signal*/ function SimpleSlot OnShutDown(AcediaObject receiver) {
return SimpleSlot(onShutdownSignal.NewSlot(receiver));
}
/// Signal that will be emitted during Acedia shut down.
///
/// System API will use it to clean up after themselves, so one shouldn't rely on using them.
///
/// There is no reason to use this signal unless you're reimplementing one of the APIs.
/// Otherwise you probably want to use `OnShutDown()` signal instead.
///
/// # Signature
///
/// void <slot>()
public final /*signal*/ function SimpleSlot OnShutDownSystem(AcediaObject receiver) {
return SimpleSlot(onShutdownSystemSignal.NewSlot(receiver));
}
/// Signal that will be emitted right after a new `Feature` is enabled and its `OnEnabled()` method
// was called.
///
/// # Signature
///
/// void <slot>(Feature enabledFeature)
/// @param enabledFeature `Feature` instance that was just enabled.
public final /*signal*/ function Environment_FeatureEnabled_Slot OnFeatureEnabled(
AcediaObject receiver
) {
return Environment_FeatureEnabled_Slot(onFeatureEnabledSignal.NewSlot(receiver));
}
/// Signal that will be emitted right after when a `Feature` is disabled and its `OnDisabled()`
/// method was called.
///
/// # Signature
///
/// void <slot>(class<Feature> disabledFeatureClass)
/// @param disabledFeatureClass Class of the `Feature` instance that was just disabled.
public final /*signal*/ function Environment_FeatureDisabled_Slot OnFeatureDisabled(
AcediaObject receiver
) {
return Environment_FeatureDisabled_Slot(onFeatureEnabledSignal.NewSlot(receiver));
}
/// Shuts AcediaCore down, performing all the necessary clean up.
public final function Shutdown() {
local LevelCore core;
if (acediaShutDown) {
return;
}
DisableAllFeatures();
onShutdownSignal.Emit();
onShutdownSystemSignal.Emit();
core = class'ServerLevelCore'.static.GetInstance();
if (core != none) {
core.Destroy();
}
core = class'ClientLevelCore'.static.GetInstance();
if (core != none) {
core.Destroy();
}
_.DropCoreAPI();
acediaShutDown = true;
}
/// Registers an Acedia package wit ha given name.
///
/// Returns `true` if package was successfully registered, `false` if it either does not exist,
/// was already registered or [`packageName`] is `none`.
///
/// # Errors
///
/// Will log an error if the package has failed to get registered (it is either missing or not
/// an Acedia package).
public final function bool RegisterPackage(BaseText packageName) {
local class<_manifest> manifestClass;
if (packageName == none) {
return false;
}
_.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy());
manifestClass = class<_manifest>(DynamicLoadObject(
packageName.ToString() $ manifestSuffix, class'Class', true));
if (manifestClass == none) {
_.logger.Auto(errNotRegistered).Arg(packageName.Copy());
return false;
}
if (IsManifestRegistered(manifestClass)) {
_.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy());
return false;
}
availablePackages[availablePackages.length] = manifestClass;
ReadManifest(manifestClass);
return true;
}
/// Registers an Acedia package wit ha given name.
///
/// Returns `true` if package was successfully registered, `false` if it either does not exist or
/// was already registered.
///
/// # Errors
///
/// Will log an error if the package has failed to get registered (it is either missing or not
/// an Acedia package).
public final function RegisterPackage_S(string packageName) {
local Text wrapper;
wrapper = _.text.FromString(packageName);
RegisterPackage(wrapper);
_.memory.Free(wrapper);
}
private final function bool IsManifestRegistered(class<_manifest> manifestClass) {
local int i;
for (i = 0; i < availablePackages.length; i += 1) {
if (manifestClass == availablePackages[i]) {
return true;
}
}
return false;
}
private final function ReadManifest(class<_manifest> manifestClass) {
local int i;
for (i = 0; i < manifestClass.default.features.length; i += 1) {
if (manifestClass.default.features[i] == none) {
continue;
}
manifestClass.default.features[i].static.LoadConfigs();
availableFeatures[availableFeatures.length] = manifestClass.default.features[i];
}
for (i = 0; i < manifestClass.default.testCases.length; i += 1) {
class'TestingService'.static.RegisterTestCase(manifestClass.default.testCases[i]);
}
}
/// Returns `true` iff AcediaCore is running in the debug mode.
///
/// AcediaCore's debug mode allows features to enable functionality that is only useful during
/// development.
/// Whether AcediaCore is running in a debug mode is decided at launch and cannot be changed.
public final function bool IsDebugging() {
return debugMode;
}
/// Returns all packages registered in the caller [`AcediaEnvironment`].
public final function array< class<_manifest> > GetAvailablePackages() {
return availablePackages;
}
/// Returns all [`Feature`]s available in the caller `AcediaEnvironment`.
public final function array< class<Feature> > GetAvailableFeatures() {
return availableFeatures;
}
/// Returns all currently enabled [`Feature`]s.
public final function array<Feature> GetEnabledFeatures() {
local int i;
for (i = 0; i < enabledFeatures.length; i += 1) {
enabledFeatures[i].NewRef();
}
return enabledFeatures;
}
// CleanRemove `Feature`s that got deallocated.
// This shouldn't happen unless someone messes up.
private final function CleanEnabledFeatures()
{
local int i;
while (i < enabledFeatures.length) {
if (enabledFeatures[i].GetLifeVersion() != enabledFeaturesLifeVersions[i]) {
enabledFeatures.Remove(i, 1);
} else {
i += 1;
}
}
}
/// Checks if `Feature` of given class is enabled.
///
/// Even if If feature of class `featureClass` is enabled, it's not necessarily that the instance
/// you have reference to is enabled.
///
/// Although unlikely, it is possible that someone spawned another instance of the same class that
/// isn't considered enabled. If you want to check whether some particular instance of given class
/// [`featureClass`] is enabled, use [`IsFeatureEnabled()`] method instead.
public final function bool IsFeatureClassEnabled(class<Feature> featureClass) {
local int i;
if (featureClass == none) {
return false;
}
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (featureClass == enabledFeatures[i].class) {
return true;
}
}
return false;
}
/// Checks if given `Feature` instance is enabled.
///
/// If you want to check if any instance instance of given class `classToCheck` is enabled
/// (and not [`feature`] specifically), use [`IsFeatureClassEnabled()`] method instead.
public final function bool IsFeatureEnabled(Feature feature) {
local int i;
if (feature == none) return false;
if (!feature.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (feature == enabledFeatures[i]) {
return true;
}
}
return false;
}
/// Returns enabled `Feature` instance of the given class.
///
/// Returns `none` only if `featureClass` is not enabled (or also `none`).
public final function Feature GetEnabledFeature(class<Feature> featureClass) {
local int i;
if (featureClass == none) {
return none;
}
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (featureClass == enabledFeatures[i].class) {
enabledFeatures[i].NewRef();
return enabledFeatures[i];
}
}
return none;
}
/// Enables given `Feature` instance `newEnabledFeature` with a given config.
/// Does not change a config for already enabled feature, failing instead.
///
/// Returns `true` if given `newEnabledFeature` was enabled and `false` otherwise
/// (including if feature of the same class has already been enabled).
public final function bool EnableFeature(Feature newEnabledFeature, optional BaseText configName) {
local int i;
if (newEnabledFeature == none) return false;
if (!newEnabledFeature.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (newEnabledFeature.class == enabledFeatures[i].class) {
if (newEnabledFeature == enabledFeatures[i]) {
_.logger
.Auto(warnFeatureAlreadyEnabled)
.Arg(_.text.FromClass(newEnabledFeature.class));
}
else {
_.logger
.Auto(errFeatureClassAlreadyEnabled)
.Arg(_.text.FromClass(newEnabledFeature.class));
}
return false;
}
}
newEnabledFeature.NewRef();
enabledFeatures[enabledFeatures.length] = newEnabledFeature;
enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] =
newEnabledFeature.GetLifeVersion();
newEnabledFeature.EnableInternal(configName);
onFeatureEnabledSignal.Emit(newEnabledFeature);
return true;
}
/// Disables given `Feature` instance `featureToDisable`.
///
/// Returns `true` if given `newEnabledFeature` was disabled and `false` otherwise
/// (including if it already was disabled).
public final function bool DisableFeature(Feature featureToDisable) {
local int i;
if (featureToDisable == none) return false;
if (!featureToDisable.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (featureToDisable == enabledFeatures[i]) {
enabledFeatures.Remove(i, 1);
enabledFeaturesLifeVersions.Remove(i, 1);
featureToDisable.DisableInternal();
onFeatureDisabledSignal.Emit(featureToDisable.class);
_.memory.Free(featureToDisable);
return true;
}
}
return false;
}
/// Disables all currently enabled `Feature`s.
///
/// Mainly intended for the clean up when Acedia shuts down.
public final function DisableAllFeatures() {
local int i;
local array<Feature> featuresCopy;
CleanEnabledFeatures();
featuresCopy = enabledFeatures;
enabledFeatures.length = 0;
enabledFeaturesLifeVersions.length = 0;
for (i = 0; i < enabledFeatures.length; i += 1) {
featuresCopy[i].DisableInternal();
onFeatureDisabledSignal.Emit(featuresCopy[i].class);
}
_.memory.FreeMany(featuresCopy);
}
defaultproperties
{
manifestSuffix = ".Manifest"
debugMode = false
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".")
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.")
errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.")
warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Same instance of `Feature` class `%1` is already enabled.")
errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.")
}

15
sources/BaseRealm/AcediaEnvironment/Events/Environment_FeatureDisabled_Signal.uc → sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureDisabled_Signal.uc

@ -1,6 +1,8 @@
/**
* Signal class for `AcediaEnvironment`'s `FeatureDisabled()` signal.
* Copyright 2022 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -19,20 +21,17 @@
*/
class Environment_FeatureDisabled_Signal extends Signal;
public final function Emit(class<Feature> disabledFeatureClass)
{
public final function Emit(class<Feature> disabledFeatureClass) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
while (nextSlot != none) {
Environment_FeatureDisabled_Slot(nextSlot).connect(disabledFeatureClass);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
defaultproperties {
relatedSlotClass = class'Environment_FeatureDisabled_Slot'
}

38
sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureDisabled_Slot.uc

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Environment_FeatureDisabled_Slot extends Slot;
delegate connect(class<Feature> disabledFeatureClass) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

16
sources/BaseRealm/AcediaEnvironment/Events/Environment_FeatureEnabled_Signal.uc → sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureEnabled_Signal.uc

@ -1,6 +1,8 @@
/**
* Signal class for `AcediaEnvironment`'s `FeatureEnabled()` signal.
* Copyright 2022 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -19,20 +21,18 @@
*/
class Environment_FeatureEnabled_Signal extends Signal;
public final function Emit(Feature enabledFeature)
{
public final function Emit(Feature enabledFeature) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
while (nextSlot != none) {
Environment_FeatureEnabled_Slot(nextSlot).connect(enabledFeature);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
defaultproperties {
relatedSlotClass = class'Environment_FeatureEnabled_Slot'
}

38
sources/BaseAPI/AcediaEnvironment/Events/Environment_FeatureEnabled_Slot.uc

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Environment_FeatureEnabled_Slot extends Slot;
delegate connect(Feature enabledFeature) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

132
sources/BaseAPI/Global.uc

@ -0,0 +1,132 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Global extends Object;
//! Class singleton instance of an object that would hold references to any API that do not depend
//! on [`Actor`]s.
//!
//! To overcome cumbersome syntax of UnrealScript we gather all functions we want to be global into
//! special "API objects" and store their single references inside this one, [`Global`]'s instance.
//! Providing reference to properly initialized [`Global`] object to all [`AcediaObject`]s and
//! [`AcediaActor`]s will give them convenient accessors to all Acedia API.
//!
//! [`Global`] is expected to behave like a singleton and will store its main instance in this
//! variable's default value.
// For getting instance of [`Global`] from any object.
var protected Global myself;
var public UnflectApi unflect;
var public SideEffectAPI sideEffects;
var public RefAPI ref;
var public BoxAPI box;
var public MathAPI math;
var public LoggerAPI logger;
var public CollectionsAPI collections;
var public AliasesAPI alias;
var public TextAPI text;
var public MemoryAPI memory;
var public ConsoleAPI console;
var public ChatAPI chat;
var public ColorAPI color;
var public UserAPI users;
var public PlayersAPI players;
var public JsonAPI json;
var public SchedulerAPI scheduler;
var public CommandAPI commands;
var public AvariceAPI avarice;
var public AcediaEnvironment environment;
/// Returns instance of the [`Global`] object.
///
/// [`Global`] is supposed to be used as a singleton, meaning that only one instance of it should be
/// created.
/// This method creates and returns such instance.
/// In case it was already created, that instance will be returned from now one.
public final static function Global GetInstance() {
if (default.myself == none) {
// `...Global`s are special and exist outside main Acedia's object infrastructure,
//so we allocate it without using [`MemoryAPI`] methods.
default.myself = new class'Global';
default.myself.Initialize();
}
return default.myself;
}
/// Initializes [`Global`] by creating all API from base realm.
protected function Initialize() {
// Special case that we cannot spawn with memory API since it obviously
// does not exist yet!
memory = new class'MemoryAPI';
memory._constructor();
// `TextAPI` and `CollectionsAPI` need to be loaded before `LoggerAPI`
sideEffects = SideEffectAPI(memory.Allocate(class'SideEffectAPI'));
ref = RefAPI(memory.Allocate(class'RefAPI'));
box = BoxAPI(memory.Allocate(class'BoxAPI'));
text = TextAPI(memory.Allocate(class'TextAPI'));
math = MathAPI(memory.Allocate(class'MathAPI'));
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI'));
unflect = UnflectAPI(memory.Allocate(class'UnflectAPI'));
json = JsonAPI(memory.Allocate(class'JsonAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
color = ColorAPI(memory.Allocate(class'ColorAPI'));
alias = AliasesAPI(memory.Allocate(class'AliasesAPI'));
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI'));
chat = ChatAPI(memory.Allocate(class'ChatAPI'));
users = UserAPI(memory.Allocate(class'UserAPI'));
players = PlayersAPI(memory.Allocate(class'PlayersAPI'));
scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI'));
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI'));
commands = CommandAPI(memory.Allocate(class'CommandAPI'));
environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment'));
}
/// Drops references to all API from base realm, including self-reference, previously returned by
/// [`Global::GetInstance()`] method.
public function DropCoreAPI() {
unflect._drop();
memory = none;
unflect = none;
sideEffects = none;
ref = none;
box = none;
text = none;
math = none;
collections = none;
logger = none;
alias = none;
console = none;
chat = none;
color = none;
users = none;
players = none;
json = none;
scheduler = none;
commands = none;
avarice = none;
environment = none;
default.myself = none;
}
defaultproperties {
}

68
sources/BaseAPI/Iter.uc

@ -0,0 +1,68 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Iter extends AcediaObject
abstract;
//! Base class for iterator, an auxiliary object for iterating through
//! a set of objects obtained from some context-dependent source.
/// Status of the [`Iter`]'s filter regarding some specific property.
///
/// [`Iter`]s can filter objects they're iterating on by the presence or lack of a certain property,
/// recording this choice requires 3 values (for requiring having/not having a certain property and
/// for ignoring it).
/// This enumeration is for inner purposes and is there to unify iterator implementations.
enum IterFilter {
/// We don't use relevant property for filtering
IF_Nothing,
/// Iterated objects must have that property
IF_Have,
/// Iterated objects must not have that property
IF_NotHave
};
/// Advances iterator to the next item.
///
/// Makes iterator refer to the next item from the source and returns `true`, as long as the source
/// of items isn't yet exhausted.
/// In case there's no more items, method has to return `false` and do nothing else.
/// [`Iter::HasFinished()`] can also be used to check whether there are more items available.
public function bool Next();
/// Returns value currently pointed to by an iterator.
///
/// Does not advance iteration: use [`Iter::Next()`] to pick next value.
///
/// In case iterator has already reached the end (and [`Iter::Next()``] returned `false`), this
/// method will return `none`.
/// Note that depending on context `none` values can also be returned, use
/// [`Iter::LeaveOnlyNotNone()`] method to prevent that.
public function AcediaObject Get();
/// Checks if caller [`Iter`] has finished iterating.
public function bool HasFinished();
/// Makes caller iterator skip any `none` items during iteration.
public function LeaveOnlyNotNone();
defaultproperties {
}

27
sources/BaseRealm/_manifest.uc → sources/BaseAPI/_manifest.uc

@ -1,7 +1,8 @@
/**
* `Manifest` is meant to describe contents of the Acedia's package.
* This is the base class, every package's `Manifest` must directly extend it.
* Copyright 2020 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -21,15 +22,19 @@
class _manifest extends Object
abstract;
// List of alias sources in this manifest's package.
var public const array< class<AliasSource> > aliasSources;
//! `Manifest` is meant to describe contents of the Acedia's package.
//!
//! It contains description of all resources inside the package that AcediaCore can recognize.
//! This is the base class, every package's `Manifest` must directly extend it.
// List of features in this manifest's package.
var public const array< class<Feature> > features;
/// List of alias sources in this manifest's package.
var public const array< class<AliasSource> > aliasSources;
// List of test cases in this manifest's package.
var public const array< class<TestCase> > testCases;
/// List of features in this manifest's package.
var public const array< class<Feature> > features;
defaultproperties
{
/// List of test cases in this manifest's package.
var public const array< class<TestCase> > testCases;
defaultproperties {
}

839
sources/BaseRealm/API/Math/BigInt.uc

@ -1,839 +0,0 @@
/**
* A simple big integer implementation, mostly to allow Acedia's databases to
* store integers of arbitrary size.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BigInt extends AcediaObject
dependson(MathAPI);
/**
* # `BigInt`
*
* A simple big integer implementation, mostly to allow Acedia's databases to
* store integers of arbitrary size. It can be used for long arithmetic
* computations, but it was mainly meant as a players' statistics counter and,
* therefore, not optimized for performing large amount of operations.
*
* ## Usage
*
* `BigInt` can be created from both `int` and decimal `BaseText`/`string`
* representation, preferably by `MathAPI` (`_.math.`) methods
* `ToBigInt()`/`MakeBigInt()`.
* Then it can be combined either directly with other `BigInt` or with
* `int`/`BaseText`/`string` through available arithmetic operations.
* To make use of stored value one can convert it back into either `int` or
* decimal `BaseText`/`string` representation.
* Newly allocated `BigInt` is guaranteed to hold `0` as value.
*/
/**
* `BigInt` data as a `struct` - meant to be used to store `BigInt`'s values
* inside the local databases.
*/
struct BigIntData
{
var bool negative;
var array<byte> digits;
};
/**
* Used to represent a result of comparison for `BigInt`s with each other.
*/
enum BigIntCompareResult
{
BICR_Less,
BICR_Equal,
BICR_Greater
};
// Does stored `BigInt` has negative sign?
var private bool negative;
// Digits array, from least to most significant. For example, for 13524:
// `digits[0] = 4`
// `digits[1] = 2`
// `digits[2] = 5`
// `digits[3] = 3`
// `digits[4] = 1`
// Valid `BigInt` should not have this array empty: zero should be
// represented by an array with a single `0`-element.
// This isn't a most efficient representation for `BigInt`, but it's easy
// to convert to and from decimal representation.
// INVARIANT: this array must not have leading (in the sense of significance)
// zeroes. That is, last element of the array should not be a `0`. The only
// exception if if stored value is `0`, then `digits` must consist of a single
// `0` element.
var private array<byte> digits;
// Constants useful for converting `BigInt` back to `int`, while avoiding
// overflow.
// We can add less digits than that without any fear of overflow
const DIGITS_IN_MAX_INT = 10;
// Maximum `int` value is `2147483647`, so in case most significant digit
// is 10th and is `2` (so number has a form of "2xxxxxxxxx"), to check for
// overflow we only need to compare combination of the rest of the digits with
// this constant.
const ALMOST_MAX_INT = 147483647;
// To add last digit we add/subtract that digit multiplied by this value.
const LAST_DIGIT_ORDER = 1000000000;
protected function Constructor()
{
SetZero();
}
protected function Finalizer()
{
negative = false;
digits.length = 0;
}
// Auxiliary method to set current value to zero
private function BigInt SetZero()
{
negative = false;
digits.length = 1;
digits[0] = 0;
return self;
}
// Minimal `int` value `-2,147,483,648` is somewhat of a pain to handle, so
// just use this auxiliary pre-made constructor for it
private function BigInt SetMinimalNegative()
{
negative = true;
digits.length = 10;
digits[0] = 8;
digits[1] = 4;
digits[2] = 6;
digits[3] = 3;
digits[4] = 8;
digits[5] = 4;
digits[6] = 7;
digits[7] = 4;
digits[8] = 1;
digits[9] = 2;
return self;
}
// Removes unnecessary zeroes from leading digit positions `digits`.
// Does not change contained value.
private final function TrimLeadingZeroes()
{
local int i, zeroesToRemove;
// Find how many leading zeroes there is.
// Since `digits` stores digits from least to most significant, we need
// to check from the end of `digits` array.
for (i = digits.length - 1; i >= 0; i -= 1)
{
if (digits[i] != 0) {
break;
}
zeroesToRemove += 1;
}
// `digits` must not be empty, enforce `0` value in that case
if (zeroesToRemove >= digits.length) {
SetZero();
}
else {
digits.length = digits.length - zeroesToRemove;
}
}
/**
* Changes current value of `BigInt` to given `BigInt` value.
*
* @param value New value of the caller `BigInt`. If `none` is given,
* method does nothing.
* @return Self-reference to allow for method chaining.
*/
public final function BigInt Set(BigInt value)
{
if (value == none) {
return self;
}
value.TrimLeadingZeroes();
digits = value.digits;
negative = value.negative;
return self;
}
/**
* Changes current value of `BigInt` to given `int` value `value`.
*
* Cannot fail.
*
* @param value New value of the caller `BigInt`.
* @return Self-reference to allow for method chaining.
*/
public final function BigInt SetInt(int value)
{
local MathAPI.IntegerDivisionResult divisionResult;
negative = false;
digits.length = 0;
if (value < 0)
{
// Treat special case of minimal `int` value `-2,147,483,648` that
// won't fit into positive `int` as special and use pre-made
// specialized constructor `CreateMinimalNegative()`
if (value < -MaxInt) {
return SetMinimalNegative();
}
else
{
negative = true;
value *= -1;
}
}
if (value == 0) {
digits[0] = 0;
}
else
{
while (value > 0)
{
divisionResult = __().math.IntegerDivision(value, 10);
value = divisionResult.quotient;
digits[digits.length] = divisionResult.remainder;
}
}
TrimLeadingZeroes();
return self;
}
/**
* Changes current value of `BigInt` to the value, given by decimal
* representation inside `value` argument.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - behavior is undefined. Otherwise cannot fail.
*
* @param value New value of the caller `BigInt`, given by decimal
* its representation. If `none` is given, method does nothing.
* @return Self-reference to allow for method chaining.
*/
public final function BigInt SetDecimal(BaseText value)
{
local int i;
local byte nextDigit;
local Parser parser;
local Basetext.Character nextCharacter;
if (value == none) {
return none;
}
parser = value.Parse();
negative = parser.Match(P("-")).Ok();
if (!parser.Ok()) {
parser.R().Match(P("+")).Ok();
}
// Reset to valid state whether sign was consumed or not
parser.Confirm();
parser.R();
// Reset current value
digits.length = 0;
digits.length = parser.GetRemainingLength();
// Parse new one
i = digits.length - 1;
while (!parser.HasFinished())
{
// This should not happen, but just in case
if (i < 0) {
break;
}
parser.MCharacter(nextCharacter);
nextDigit = Clamp(__().text.CharacterToInt(nextCharacter), 0, 9);
digits[i] = nextDigit;
i -= 1;
}
parser.FreeSelf();
TrimLeadingZeroes();
return self;
}
/**
* Changes current value of `BigInt` to the value, given by decimal
* representation inside `value` argument.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - behavior is undefined. Otherwise cannot fail.
*
* @param value New value of the caller `BigInt`, given by decimal
* its representation.
* @return Self-reference to allow for method chaining.
*/
public final function BigInt SetDecimal_S(string value)
{
local MutableText wrapper;
wrapper = __().text.FromStringM(value);
SetDecimal(wrapper);
wrapper.FreeSelf();
return self;
}
// Auxiliary method for comparing two `BigInt`s by their absolute value.
private function BigIntCompareResult _compareAbsolute(BigInt other)
{
local int i;
local array<byte> otherDigits;
otherDigits = other.digits;
if (digits.length == otherDigits.length)
{
for (i = digits.length - 1; i >= 0; i -= 1)
{
if (digits[i] < otherDigits[i]) {
return BICR_Less;
}
if (digits[i] > otherDigits[i]) {
return BICR_Greater;
}
}
return BICR_Equal;
}
if (digits.length < otherDigits.length) {
return BICR_Less;
}
return BICR_Greater;
}
/**
* Compares caller `BigInt` to `other`.
*
* @param other Value to compare the caller `BigInt`.
* If given reference is `none` - behavior is undefined.
* @return `BigIntCompareResult` representing the result of comparison.
* Returned value describes how caller `BigInt` relates to the `other`,
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is
* smaller that `other`.
*/
public function BigIntCompareResult Compare(BigInt other)
{
local BigIntCompareResult resultForModulus;
if (other == none) {
return BICR_Less;
}
if (negative && !other.negative) {
return BICR_Less;
}
if (!negative && other.negative) {
return BICR_Greater;
}
resultForModulus = _compareAbsolute(other);
if (resultForModulus == BICR_Equal) {
return BICR_Equal;
}
if ( (negative && (resultForModulus == BICR_Greater))
|| (!negative && (resultForModulus == BICR_Less)) )
{
return BICR_Less;
}
return BICR_Greater;
}
/**
* Compares caller `BigInt` to `other`.
*
* @param other Value to compare the caller `BigInt`.
* @return `BigIntCompareResult` representing the result of comparison.
* Returned value describes how caller `BigInt` relates to the `other`,
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is
* smaller that `other`.
*/
public function BigIntCompareResult CompareInt(int other)
{
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.ToBigInt(other);
result = Compare(wrapper);
wrapper.FreeSelf();
return result;
}
/**
* Compares caller `BigInt` to `other`.
*
* @param other Value to compare the caller `BigInt`.
* If given reference is `none` - behavior is undefined.
* @return `BigIntCompareResult` representing the result of comparison.
* Returned value describes how caller `BigInt` relates to the `other`,
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is
* smaller that `other`.
*/
public function BigIntCompareResult CompareDecimal(BaseText other)
{
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.MakeBigInt(other);
result = Compare(wrapper);
wrapper.FreeSelf();
return result;
}
/**
* Compares caller `BigInt` to `other`.
*
* @param other Value to compare the caller `BigInt`.
* If given value contains invalid decimal value - behavior is undefined.
* @return `BigIntCompareResult` representing the result of comparison.
* Returned value describes how caller `BigInt` relates to the `other`,
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is
* smaller that `other`.
*/
public function BigIntCompareResult CompareDecimal_S(string other)
{
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.MakeBigInt_S(other);
result = Compare(wrapper);
wrapper.FreeSelf();
return result;
}
// Adds absolute values of caller `BigInt` and `other` with no changes to
// the sign
private function _add(BigInt other)
{
local int i;
local byte carry, digitSum;
local array<byte> otherDigits;
if (other == none) {
return;
}
otherDigits = other.digits;
if (digits.length < otherDigits.length) {
digits.length = otherDigits.length;
}
else {
otherDigits.length = digits.length;
}
carry = 0;
for (i = 0; i < digits.length; i += 1)
{
digitSum = digits[i] + otherDigits[i] + carry;
digits[i] = _.math.Remainder(digitSum, 10);
carry = (digitSum - digits[i]) / 10;
}
if (carry > 0) {
digits[digits.length] = carry;
}
// No leading zeroes can be created here, so no need to trim
}
// Subtracts absolute value of `other` from the caller `BigInt`, flipping
// caller's sign in case `other`'s absolute value is bigger.
private function _sub(BigInt other)
{
local int i;
local int carry, nextDigit;
local array<byte> minuendDigits, subtrahendDigits;
local BigIntCompareResult resultForModulus;
if (other == none) {
return;
}
resultForModulus = _compareAbsolute(other);
if (resultForModulus == BICR_Equal)
{
SetZero();
return;
}
if (resultForModulus == BICR_Less)
{
negative = !negative;
minuendDigits = other.digits;
subtrahendDigits = digits;
}
else
{
minuendDigits = digits;
subtrahendDigits = other.digits;
}
digits.length = minuendDigits.length;
subtrahendDigits.length = minuendDigits.length;
carry = 0;
for (i = 0; i < digits.length; i += 1)
{
nextDigit = int(minuendDigits[i]) - int(subtrahendDigits[i]) + carry;
if (nextDigit < 0)
{
nextDigit += 10;
carry = -1;
}
else {
carry = 0;
}
digits[i] = nextDigit;
}
TrimLeadingZeroes();
}
/**
* Adds `other` value to the caller `BigInt`.
*
* @param other Value to add. If `none` is given method does nothing.
* @return Self-reference to allow for method chaining.
*/
public function BigInt Add(BigInt other)
{
if (other == none) {
return self;
}
if (negative == other.negative) {
_add(other);
}
else {
_sub(other);
}
return self;
}
/**
* Adds `other` value to the caller `BigInt`.
*
* Cannot fail.
*
* @param other Value to add.
* @return Self-reference to allow for method chaining.
*/
public function BigInt AddInt(int other)
{
local BigInt otherObject;
otherObject = _.math.ToBigInt(other);
Add(otherObject);
_.memory.Free(otherObject);
return self;
}
/**
* Adds `other` value to the caller `BigInt`.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - behavior is undefined. Otherwise cannot fail.
*
* @param other Value to add. If `none` is given, method does nothing.
* @return Self-reference to allow for method chaining.
*/
public function BigInt AddDecimal(BaseText other)
{
local BigInt otherObject;
if (other == none) {
return self;
}
otherObject = _.math.MakeBigInt(other);
Add(otherObject);
_.memory.Free(otherObject);
return self;
}
/**
* Adds `other` value to the caller `BigInt`.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - behavior is undefined. Otherwise cannot fail.
*
* @param other Value to add.
* @return Self-reference to allow for method chaining.
*/
public function BigInt AddDecimal_S(string other)
{
local BigInt otherObject;
otherObject = _.math.MakeBigInt_S(other);
Add(otherObject);
_.memory.Free(otherObject);
return self;
}
/**
* Subtracts `other` value to the caller `BigInt`.
*
* @param other Value to subtract. If `none` is given method does nothing.
* @return Self-reference to allow for method chaining.
*/
public function BigInt Subtract(BigInt other)
{
if (negative != other.negative) {
_add(other);
}
else {
_sub(other);
}
return self;
}
/**
* Subtracts `other` value to the caller `BigInt`.
*
* Cannot fail.
*
* @param other Value to subtract.
* @return Self-reference to allow for method chaining.
*/
public function BigInt SubtractInt(int other)
{
local BigInt otherObject;
otherObject = _.math.ToBigInt(other);
Subtract(otherObject);
_.memory.Free(otherObject);
return self;
}
/**
* Subtracts `other` value to the caller `BigInt`.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - behavior is undefined. Otherwise cannot fail.
*
* @param other Value to subtract. If `none`, method does nothing.
* @return Self-reference to allow for method chaining.
*/
public function BigInt SubtractDecimal(BaseText other)
{
local BigInt otherObject;
if (other == none) {
return self;
}
otherObject = _.math.MakeBigInt(other);
Subtract(otherObject);
_.memory.Free(otherObject);
return self;
}
/**
* Subtracts `other` value to the caller `BigInt`.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - behavior is undefined. Otherwise cannot fail.
*
* @param other Value to subtract.
* @return Self-reference to allow for method chaining.
*/
public function BigInt SubtractDecimal_S(string other)
{
local BigInt otherObject;
otherObject = _.math.MakeBigInt_S(other);
Subtract(otherObject);
_.memory.Free(otherObject);
return self;
}
/**
* Checks if caller `BigInt` is negative. Zero is not considered negative
* number.
*
* @return `true` if stored value is negative (`< 0`) and `false` otherwise
* (`>= 0`).
*/
public function bool IsNegative()
{
// Handle special case of zero first (it ignores `negative` flag)
if (digits.length == 1 && digits[0] == 0) {
return false;
}
return negative;
}
/**
* Converts caller `BigInt` into `int` representation.
*
* In case stored value is outside `int`'s value range
* (`[-MaxInt-1, MaxInt] == [-2147483648; 2147483647]`),
* method returns either maximal or minimal possible value, depending on
* the `BigInt`'s sign.
*
* @return `int` representation of the caller `BigInt`, clamped into available
* `int` value range.
*/
public function int ToInt()
{
local int i;
local int accumulator;
local int safeDigitsAmount;
if (digits.length <= 0) {
return 0;
}
if (digits.length > DIGITS_IN_MAX_INT)
{
if (negative) {
return (-MaxInt - 1);
}
else {
return MaxInt;
}
}
// At most `DIGITS_IN_MAX_INT - 1` iterations
safeDigitsAmount = Min(DIGITS_IN_MAX_INT - 1, digits.length);
for (i = safeDigitsAmount - 1; i >= 0; i -= 1)
{
accumulator *= 10;
accumulator += digits[i];
}
if (negative) {
accumulator *= -1;
}
accumulator = AddUnsafeDigitToInt(accumulator);
return accumulator;
}
// Adding `DIGITS_IN_MAX_INT - 1` will never lead to an overflow, but
// adding the next digit can, so we need to handle it differently and more
// carefully.
// Assumes `digits.length <= DIGITS_IN_MAX_INT`.
private function int AddUnsafeDigitToInt(int accumulator)
{
local int unsafeDigit;
local bool noOverflow;
if (digits.length < DIGITS_IN_MAX_INT) {
return accumulator;
}
unsafeDigit = digits[DIGITS_IN_MAX_INT - 1];
// `MaxInt` stats with `2`, so if last/unsafe digit is either `0` or `1`,
// there is no overflow, otherwise - check rest of the digits
noOverflow = (unsafeDigit < 2);
if (unsafeDigit == 2)
{
// Include `MaxInt` and `-MaxInt-1` (minimal possible value) into
// an overflow too - this way we still give a correct result, but do
// not have to worry about `int`-arithmetic error
noOverflow = noOverflow
|| (negative && (accumulator > -ALMOST_MAX_INT - 1))
|| (!negative && (accumulator < ALMOST_MAX_INT));
}
if (noOverflow)
{
if (negative) {
accumulator -= unsafeDigit * LAST_DIGIT_ORDER;
}
else {
accumulator += unsafeDigit * LAST_DIGIT_ORDER;
}
return accumulator;
}
// Handle overflow
if (negative) {
return (-MaxInt - 1);
}
return MaxInt;
}
/**
* Converts caller `BigInt` into `Text` representation.
*
* @return `Text` representation of the caller `BigInt`.
*/
public function Text ToText()
{
return ToText_M().IntoText();
}
/**
* Converts caller `BigInt` into `MutableText` representation.
*
* @return `MutableText` representation of the caller `BigInt`.
*/
public function MutableText ToText_M()
{
local int i;
local MutableText result;
result = _.text.Empty();
if (negative) {
result.AppendCharacter(_.text.GetCharacter("-"));
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48));
}
return result;
}
/**
* Converts caller `BigInt` into `string` representation.
*
* @return `string` representation of the caller `BigInt`.
*/
public function string ToString()
{
local int i;
local string result;
if (negative) {
result = "-";
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result = result $ digits[i];
}
return result;
}
/**
* Restores `BigInt` from the `BigIntData` value.
*
* This method is created to make an efficient way to store `BigInt` inside
* local databases.
*
* @param data Data to read new caller `BigInt`'s value from.
*/
public function FromData(BigIntData data)
{
local int i;
negative = data.negative;
digits = data.digits;
// Deal with possibly erroneous data
for (i = 0; i < digits.length; i += 1) {
if (digits[i] > 9) {
digits[i] = 9;
}
}
}
/**
* Converts caller `BigInt`'s value into `BigIntData`.
*
* This method is created to make an efficient way to store `BigInt` inside
* local databases.
*
* @return Value of the caller `BigInt` in the `struct` form.
*/
public function BigIntData ToData()
{
local BigIntData result;
result.negative = negative;
result.digits = digits;
return result;
}
defaultproperties
{
}

131
sources/BaseRealm/API/Math/MathAPI.uc

@ -1,131 +0,0 @@
/**
* API that provides a collection of non-built in math methods used in Acedia.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MathAPI extends AcediaObject;
/**
* For storing result of integer division.
*
* If we divide `number` by `divisor`, then
* `number = divisor * quotient + remainder`
*/
struct IntegerDivisionResult
{
var int quotient;
var int remainder;
};
/**
* Changes current value of `BigInt` to given `BigInt` value.
*
* @param value New value of the caller `BigInt`. If `none` is given,
* method does nothing.
* @return Self-reference to allow for method chaining.
*/
public function BigInt ToBigInt(int value)
{
local BigInt result;
result = BigInt(_.memory.Allocate(class'BigInt'));
return result.SetInt(value);
}
/**
* Creates new `BigInt` value, base on the decimal representation given by
* `value`.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - contents of returned value are undefined. Otherwise cannot fail.
*
* @param value New value of the caller `BigInt`, given by decimal
* its representation. If `none` is given, method returns `BigInt`
* containing `0` as value.
* @return Created `BigInt`, containing value, given by its the decimal
* representation `value`.
*/
public function BigInt MakeBigInt(BaseText value)
{
local BigInt result;
result = BigInt(_.memory.Allocate(class'BigInt'));
return result.SetDecimal(value);
}
/**
* Creates new `BigInt` value, base on the decimal representation given by
* `value`.
*
* If invalid decimal representation (digits only, possibly with leading sign)
* is given - contents of returned value are undefined. Otherwise cannot fail.
*
* @param value New value of the caller `BigInt`, given by decimal
* its representation.
* @return Created `BigInt`, containing value, given by its the decimal
* representation `value`.
*/
public function BigInt MakeBigInt_S(string value)
{
local BigInt result;
result = BigInt(_.memory.Allocate(class'BigInt'));
return result.SetDecimal_S(value);
}
/**
* Computes remainder of the integer division of `number` by `divisor`.
*
* This method is necessary as a replacement for `%` module operator, since it
* is an operation on `float`s in UnrealScript and does not have appropriate
* value range to work with big integer values.
*
* @see `IntegerDivision()` method if you need both quotient and remainder.
*
* @param number Number that we are dividing.
* @param divisor Number we are dividing by.
* @return Remainder of the integer division.
*/
public function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
/**
* Computes quotient and remainder of the integer division of `number` by
* `divisor`.
*
* @see `IntegerDivision()` method if you only need remainder.
* @param number Number that we are dividing.
* @param divisor Number we are dividing by.
* @return `struct` with quotient and remainder of the integer division.
*/
public function IntegerDivisionResult IntegerDivision(int number, int divisor)
{
local IntegerDivisionResult result;
result.quotient = number / divisor;
result.remainder = (number - result.quotient * divisor);
return result;
}
defaultproperties
{
}

175
sources/BaseRealm/API/Memory/AcediaObjectPool.uc

@ -1,175 +0,0 @@
/**
* Acedia's implementation for object pool that can only store objects of
* one specific class to allow for both faster allocation and
* faster deallocation.
* Allows to set a maximum capacity.
* Copyright 2020-2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaObjectPool extends Object
config(AcediaSystem);
// Class of objects that this `AcediaObjectPool` stores.
// if `== none`, - object pool is considered uninitialized.
var private class<AcediaObject> storedClass;
// Actual storage, functions on LIFO principle.
var public array<AcediaObject> objectPool;
// This struct and it's associated array `poolSizeOverwrite` allows
// server admins to rewrite the pool capacity for each class.
struct PoolSizeSetting
{
var class<AcediaObject> objectClass;
var int maxPoolSize;
};
var private config const array<PoolSizeSetting> poolSizeOverwrite;
// Capacity for object pool that we are using.
// Set during initialization and cannot be changed later.
var private int usedMaxPoolSize;
/**
* Initialize caller object pool to store objects of `initStoredClass` class.
*
* If successful, this action is irreversible: same pool cannot be
* re-initialized.
*
* @param initStoredClass Class of objects that caller object pool will store.
* @param forcedPoolSize Max pool size for the caller `AcediaObjectPool`.
* Leaving it at default `0` value will cause method to auto-determine
* the size: gives priority to the `poolSizeOverwrite` config array;
* if not specified, uses `AcediaObject`'s `defaultMaxPoolSize`
* (ignoring `usesObjectPool` setting).
* @return `true` if initialization completed, `false` otherwise
* (including if it was already completed with passed `initStoredClass`).
*/
public final function bool Initialize(
class<AcediaObject> initStoredClass,
optional int forcedPoolSize)
{
if (storedClass != none) return false;
if (initStoredClass == none) return false;
// If does not matter that we've set those variables until
// we set `storedClass`.
if (forcedPoolSize == 0) {
usedMaxPoolSize = GetMaxPoolSizeForClass(initStoredClass);
}
else {
usedMaxPoolSize = forcedPoolSize;
}
if (usedMaxPoolSize == 0) {
return false;
}
storedClass = initStoredClass;
return true;
}
// Determines default object pool size for the initialization.
private final function int GetMaxPoolSizeForClass(
class<AcediaObject> classToCheck)
{
local int i;
local int result;
if (classToCheck != none) {
result = classToCheck.default.defaultMaxPoolSize;
}
else {
result = -1;
}
// Try to replace it with server's settings
for (i = 0; i < poolSizeOverwrite.length; i += 1)
{
if (poolSizeOverwrite[i].objectClass == classToCheck)
{
result = poolSizeOverwrite[i].maxPoolSize;
break;
}
}
return result;
}
/**
* Returns class of objects inside the caller `AcediaObjectPool`.
*
* @return class of objects inside caller the caller object pool;
* `none` means object pool was not initialized.
*/
public final function class<AcediaObject> GetClassOfStoredObjects()
{
return storedClass;
}
/**
* Clear the storage of all it's contents.
*
* Can be used before UnrealEngine's garbage collection to free pooled objects.
*/
public final function Clear()
{
objectPool.length = 0;
}
/**
* Adds object to the caller storage
* (that needs to be initialized to store `newObject.class` classes).
*
* For performance purposes does not do duplicates checks,
* this should be verified from outside `AcediaObjectPool`.
*
* Does type checks and only allows objects of the class that caller
* `AcediaObjectPool` was initialized for.
*
* @param newObject Object to put inside caller pool. Must be not `none` and
* have precisely the class this object pool was initialized to store.
* @return `true` on success and `false` on failure
* (can happen if passed `newObject` reference was invalid, caller storage
* is not initialized yet or reached it's capacity).
*/
public final function bool Store(AcediaObject newObject)
{
if (newObject == none) return false;
if (newObject.class != storedClass) return false;
if (usedMaxPoolSize >= 0 && objectPool.length >= usedMaxPoolSize) {
return false;
}
objectPool[objectPool.length] = newObject;
return true;
}
/**
* Extracts last stored object from the pool. Returned object will no longer
* be stored in the pool.
*
* @return Reference to the last (not destroyed) stored object.
* Only returns `none` if caller `AcediaObjectPool` is either empty or
* not initialized.
*/
public final function AcediaObject Fetch()
{
local AcediaObject result;
if (storedClass == none) return none;
if (objectPool.length <= 0) return none;
result = objectPool[objectPool.length - 1];
objectPool.length = objectPool.length - 1;
return result;
}
defaultproperties
{
}

437
sources/BaseRealm/API/Memory/MemoryAPI.uc

@ -1,437 +0,0 @@
/**
* API that provides functions for managing object of classes, derived from
* `AcediaObject`. It takes care of managing their object pools, as well as
* ensuring that constructors and finalizers are called properly.
* Almost all `AcediaObject`s should use this API's methods for their own
* creation and destruction.
* Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MemoryAPI extends AcediaObject;
/**
* # Memory API
*
* This is most-basic API that must be created before anything else in Acedia,
* since it is responsible for the proper creation of `AcediaObject`s.
* It takes care of managing their object pools, as well as ensuring that
* constructors and finalizers are called properly.
* Almost all `AcediaObject`s should use this API's methods for their own
* creation and destruction.
*
* ## Usage
*
* First of all, this API is only meant for non-actor `Object` creation.
* `Actor` creation is generally avoided in Acedia and, when unavoidable,
* different APIs are dealing with that. `MemoryAPI` is designed to work in
* the absence of any level (and, therefore, `Actor`s) at all.
* Simply use `MemoryAPI.Allocate()` to create a new object and
* `MemoryAPI.Free()` to get rid on unneeded reference. Do note that
* `AcediaObject`s use reference counting and object will be deallocated and
* pooled only after every trackable reference was released by
* `MemoryAPI.Free()`.
* Best practice is to only care about what object reference you're
* keeping, properly release them with `MemoryAPI.Free()` and to NEVER EVER USE
* THEM after you've release them. Regardless of whether they were actually
* deallocated.
*
* There's also a set of auxiliary methods for either loading `class`es from
* their `BaseText`/`string`-given names or even directly creating objects of
* said classes.
*
* ## Motivation
*
* UnrealScript lacks any practical way to destroy non-actor objects on
* demand: the best one can do is remove any references to the object and wait
* for garbage collection. But garbage collection itself is too slow and causes
* noticeable lag spikes for players, making it suitable only for cleaning
* objects when switching levels. To alleviate this problem, there exists
* a standard class `ObjectPool` that stores unused objects (mostly resources
* such as textures) inside dynamic array until they are needed.
* Unfortunately, using a single ObjectPool for a large volume of objects
* is impractical from performance perspective, since it stores objects of all
* classes together and each object allocation from the pool can potentially
* require going through the whole array (see `Engine/ObjectPool.uc`).
* Acedia uses a separate object pool (implemented by `AcediaObjectPool`)
* for every single class, making object allocation as trivial as grabbing
* the last stored object from `AcediaObjectPool`'s internal dynamic array.
* New pool is prepared for every class you create, as long as it is
* derived from `AcediaObject`. `AcediaActors` do not use object pools and are
* meant to be simply `Destroy()`ed.
*
* ## Customizing object pools for your classes
*
* Object pool usage can be disabled completely for your class by setting
* `usesObjectPool = false` in `defaultproperties` block. Without object pools
* `MemoryAPI.Allocate()` will create a new instance of your class every single
* time.
* You can also set a limit to how many objects will be stored in an object
* pool with defaultMaxPoolSize variable. Negative number (default for
* `AcediaObject`) means that object pool can grow without a limit.
* `0` effectively disables object pool, similar to setting
* `usesObjectPool = false`. However, this can be overwritten by server's
* settings (see `AcediaSystem.ini`: `AcediaObjectPool`).
*/
// Store all created pools, so that we can quickly forget stored objects upon
// garbage collection
var private array<AcediaObjectPool> registeredPools;
/**
* Creates a class instance from its `Text` representation.
*
* Does not generate log messages upon failure.
*
* @param classReference Text representation of the class to return.
* @return Loaded class, corresponding to its name from `classReference`.
*/
public function class<Object> LoadClass(BaseText classReference)
{
if (classReference == none) {
return none;
}
return class<Object>(
DynamicLoadObject(classReference.ToString(),
class'Class',
true));
}
/**
* Creates a class instance from its `string` representation.
*
* Does not generate log messages upon failure.
*
* @param classReference `string` representation of the class to return.
* @return Loaded class, corresponding to its name from `classReference`.
*/
public function class<Object> LoadClass_S(string classReference)
{
return class<Object>(DynamicLoadObject(classReference, class'Class', true));
}
/**
* Creates a new `Object` of a given class.
*
* For `AcediaObject`s calls constructors and tries (uses them only if they
* aren't forbidden for a given class) to make use of their classes' object
* pools.
*
* If Acedia's object does make use of object pools, -
* guarantees to return last pooled object (in a LIFO queue),
* unless `forceNewInstance` is set to `true`.
*
* @see `AllocateByReference()`, `AllocateByReference_S()`
*
* @param classToAllocate Class of the `Object` that this method will
* create. Must not be subclass of `Actor`.
* @param forceNewInstance Set this to `true` if you require this method to
* create a new instance, bypassing any object pools.
* @return Newly created object. Will only be `none` if:
* 1. `classToAllocate` is `none`;
* 2. `classToAllocate` is abstract;
* 3. `classToAllocate` is derived from `Actor`.
*/
public function Object Allocate(
class<Object> classToAllocate,
optional bool forceNewInstance)
{
// TODO: this is an old code require while we still didn't get rid of
// services - replace it later
local LevelCore core;
local Object allocatedObject;
local AcediaObjectPool relevantPool;
local class<AcediaObject> acediaObjectClassToAllocate;
local class<AcediaActor> acediaActorClassToAllocate;
local class<Actor> actorClassToAllocate;
if (classToAllocate == none) {
return none;
}
// Try using pool first (only if new instance is not required)
acediaObjectClassToAllocate = class<AcediaObject>(classToAllocate);
acediaActorClassToAllocate = class<AcediaActor>(classToAllocate);
if (!forceNewInstance)
{
if (acediaObjectClassToAllocate != none) {
relevantPool = acediaObjectClassToAllocate.static._getPool();
}
// `relevantPool == none` is expected if object / actor of is setup to
// not use object pools.
if (relevantPool != none) {
allocatedObject = relevantPool.Fetch();
}
}
// If pools did not work - spawn / create object through regular methods
if (allocatedObject == none)
{
actorClassToAllocate = class<Actor>(classToAllocate);
if (actorClassToAllocate != none)
{
core = class'ServerLevelCore'.static.GetInstance();
if (core == none) {
core = class'ClientLevelCore'.static.GetInstance();
}
allocatedObject = core.Spawn(actorClassToAllocate);
}
else {
allocatedObject = (new classToAllocate);
}
}
// Call constructors
if (acediaObjectClassToAllocate != none) {
AcediaObject(allocatedObject)._constructor();
}
if (acediaActorClassToAllocate != none)
{
// Call it here, just in case, to make sure constructor is called
// as soon as possible
AcediaActor(allocatedObject)._constructor();
}
return allocatedObject;
}
/**
* Creates a new `Object` of a given class using its `BaseText`
* representation.
*
* For `AcediaObject`s calls constructors and tries (uses them only if they
* aren't forbidden for a given class) to make use of their classes' object
* pools.
*
* If Acedia's object does make use of object pools, -
* guarantees to return last pooled object (in a LIFO queue),
* unless `forceNewInstance` is set to `true`.
* @see `Allocate()`, `AllocateByReference_S()`
*
* @param refToClassToAllocate `BaseText` representation of the class' name
* of the `Object` that this method will create. Must not be subclass of
* `Actor`.
* @param forceNewInstance Set this to `true` if you require this method to
* create a new instance, bypassing any object pools.
* @return Newly created object. Will only be `none` if:
* 1. `classToAllocate` is `none`;
* 2. `classToAllocate` is abstract;
* 3. `classToAllocate` is derived from `Actor`.
*/
public function Object AllocateByReference(
BaseText refToClassToAllocate,
optional bool forceNewInstance)
{
return Allocate(LoadClass(refToClassToAllocate), forceNewInstance);
}
/**
* Creates a new `Object` of a given class using its `string`
* representation.
*
* For `AcediaObject`s calls constructors and tries (uses them only if they
* aren't forbidden for a given class) to make use of their classes' object
* pools.
*
* If Acedia's object does make use of object pools, -
* guarantees to return last pooled object (in a LIFO queue),
* unless `forceNewInstance` is set to `true`.
*
* @see `Allocate()`, `AllocateByReference()`
*
* @param refToClassToAllocate `string` representation of the class' name
* of the `Object` that this method will create. Must not be subclass of
* `Actor`.
* @param forceNewInstance Set this to `true` if you require this method to
* create a new instance, bypassing any object pools.
* @return Newly created object. Will only be `none` if:
* 1. `classToAllocate` is `none`;
* 2. `classToAllocate` is abstract;
* 3. `classToAllocate` is derived from `Actor`.
*/
public function Object AllocateByReference_S(
string refToClassToAllocate,
optional bool forceNewInstance)
{
return Allocate(LoadClass_S(refToClassToAllocate), forceNewInstance);
}
/**
* Releases one reference to a given `AcediaObject`, calling its finalizers in
* case all references were released.
*
* Method will attempt to store `objectToRelease` in its object pool once
* deallocated, unless it is forbidden by its class' settings.
*
* @see `FreeMany()`
*
* @param objectToRelease Object, which reference method needs to release.
*/
public function Free(Object objectToRelease)
{
// TODO: this is an old code require while we still didn't get rid of
// services - replace it later, changing argument to `AcediaObject`
local AcediaObjectPool relevantPool;
local Actor objectAsActor;
local AcediaActor objectAsAcediaActor;
local AcediaObject objectAsAcediaObject;
if (objectToRelease == none) {
return;
}
// Call finalizers for Acedia's objects and actors
objectAsAcediaObject = AcediaObject(objectToRelease);
objectAsAcediaActor = AcediaActor(objectToRelease);
if (objectAsAcediaObject != none)
{
if (!objectAsAcediaObject.IsAllocated()) {
return;
}
objectAsAcediaObject._deref();
if (objectAsAcediaObject._getRefCount() > 0) {
return;
}
relevantPool = objectAsAcediaObject._getPool();
objectAsAcediaObject._finalizer();
}
if (objectAsAcediaActor != none)
{
if (!objectAsAcediaActor.IsAllocated()) {
return;
}
objectAsAcediaActor._deref();
if (objectAsAcediaActor._getRefCount() > 0) {
return;
}
objectAsAcediaActor._finalizer();
}
// Try to store freed object in a pool
if (relevantPool != none && relevantPool.Store(objectAsAcediaObject)) {
return;
}
// Otherwise destroy actors and forget about objects
objectAsActor = Actor(objectToRelease);
if (objectAsActor != none) {
objectAsActor.Destroy();
}
}
/**
* Releases one reference to each `AcediaObject` inside the given array
* `objectsToRelease`, calling finalizers for the ones that got all of their
* references released.
*
* Method will attempt to store objects inside `objectsToRelease` in their
* object pools, unless it is forbidden by their class' settings.
*
* @see `Free()`
*
* @param objectToRelease Array of objects, which reference method needs
* to release.
*/
public function FreeMany(array<Object> objectsToRelease)
{
// TODO: this is an old code require while we still didn't get rid of
// services - replace it later, changing argument to `AcediaObject`
local int i;
for (i = 0; i < objectsToRelease.length; i += 1) {
Free(objectsToRelease[i]);
}
}
/**
* Forces Unreal Engine to perform garbage collection.
* By default also cleans up all of the Acedia's objects pools.
*
* Process of garbage collection causes significant lag spike during the game
* and should be used sparingly and at right moments.
*
* If not `LevelCore` was setup, Acedia doesn't have access to the level and
* cannot perform garbage collection, meaning that this method can fail.
*
* @param keepAcediaPools Set this to `true` to NOT garbage collect
* objects inside pools. Otherwise keep it `false`.
* Pools won't be dropped regardless of this parameter if no `LevelCore` is
* found.
* @return `true` if garbage collection successfully happened and `false` if it
* failed. Garbage collection can only fail if no `LevelCore` was yet
* setup.
*/
public function bool CollectGarbage(optional bool keepAcediaPools)
{
local LevelCore core;
// Try to find level core
core = class'ServerLevelCore'.static.GetInstance();
if (core == none) {
core = class'ClientLevelCore'.static.GetInstance();
}
if (core == none) {
return false;
}
// Drop content of all `AcediaObjectPools` first
if (!keepAcediaPools) {
DropPools();
}
// This makes Unreal Engine do garbage collection
core.ConsoleCommand("obj garbage");
return true;
}
/**
* Registers new object pool to auto-clean before Acedia's garbage collection.
*
* @param newPool New object pool that can get cleaned if `CollectGarbage()`
* is called with appropriate parameters.
* @return `true` if `newPool` was registered,
* `false` if `newPool == none` or was already registered.
*/
public function bool RegisterNewPool(AcediaObjectPool newPool)
{
local int i;
if (newPool == none) {
return false;
}
registeredPools = default.registeredPools;
for (i = 0; i < registeredPools.length; i += 1)
{
if (registeredPools[i] == newPool) {
return false;
}
}
registeredPools[registeredPools.length] = newPool;
default.registeredPools = registeredPools;
return true;
}
/**
* Forgets about all stored (deallocated) object references in registered
* object pools.
*/
protected function DropPools()
{
local int i;
registeredPools = default.registeredPools;
for (i = 0; i < registeredPools.length; i += 1)
{
if (registeredPools[i] == none) {
continue;
}
registeredPools[i].Clear();
}
}
defaultproperties
{
}

421
sources/BaseRealm/API/Scheduler/SchedulerAPI.uc

@ -1,421 +0,0 @@
/**
* API that provides functions for scheduling jobs and expensive tasks such
* as writing onto the disk. Also provides methods for users to inform API that
* they've recently did an expensive operation, so that `SchedulerAPI` is to
* try and use less resources when managing jobs.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SchedulerAPI extends AcediaObject
config(AcediaSystem);
/**
* # `SchedulerAPI`
*
* UnrealScript is inherently single-threaded and whatever method you call,
* it will be completely executed within a single game's tick.
* This API is meant for scheduling various actions over time to help emulating
* multi-threading by spreading some code executions over several different
* game/server ticks.
*
* ## Usage
*
* ### Job scheduling
*
* One of the reasons which is faulty infinite loop detection system that
* will crash the game/server if it thinks UnrealScript code has executed too
* many operations (it is not about execution time, logging a lot of messages
* with `Log()` can take a lot of time and not crash anything, while simple
* loop, that would've finished much sooner, can trigger a crash).
* This is a very atypical problem for mods to have, but Acedia's
* introduction of databases and avarice link can lead to users trying to read
* (from database or network) an object that is too big, leading to a crash.
* Jobs are not about performance, they're about crash prevention.
*
* In case you have such a job of your own, that can potentially take too
* many steps to finish without crashing, you can convert it into
* a `SchedulerJob` (you make a subclass for your type of the job and
* instantiate it for each execution of the job). This requires you to
* restructure your algorithm in such a way, that it is able to run for some
* finite (maybe small) amount of steps and postpone the rest of calculations
* to the next tick and put it into a method
* `SchedulerJob.DoWork(int allottedWorkUnits)`, where `allottedWorkUnits` is
* how much your method is allowed to do during this call, assuming `10000`
* units of work on their own won't lead to a crash.
* Another method `SchedulerJob.IsCompleted()` needs to be setup to return
* `true` iff your job is done.
* After you prepared an instance of your job subclass, simply pass it to
* `_.scheduler.AddJob()`.
*
* ### Disk usage requests
*
* Writing to the disk (saving data into config file, saving local database
* changes) can be an expensive operation and to avoid lags in gameplay you
* might want to spread such operations over time.
* `_.scheduler.RequestDiskAccess()` method allows you to do that. It is not
* exactly a signal, but it acts similar to one: to request a right to save to
* the disk, just do the following:
* `_.scheduler.RequestDiskAccess(<receiver>).connect = <disk_writing_method>`
* and `disk_writing_method()` will be called once your turn come up.
*
* ## Manual ticking
*
* If any kind of level core (either server or client one) was created,
* this API will automatically perform necessary actions every tick.
* Otherwise, if only base API is available, there's no way to do that, but
* you can manually decide when to tick this API by calling `ManualTick()`
* method.
*/
/**
* How often can files be saved on disk. This is a relatively expensive
* operation and we don't want to write a lot of different files at once.
* But since we lack a way to exactly measure how much time that saving will
* take, AcediaCore falls back to simply performing every saving with same
* uniform time intervals in-between.
* This variable decides how much time there should be between two file
* writing accesses.
* Negative and zero values mean that all writing disk access will be
* granted as soon as possible, without any cooldowns.
*/
var private config float diskSaveCooldown;
/**
* Maximum total work units for jobs allowed per tick. Jobs are expected to be
* constructed such that they don't lead to a crash if they have to perform
* this much work.
*
* Changing default value of `10000` is not advised.
*/
var private config int maxWorkUnits;
/**
* How many different jobs can be performed per tick. This limit is added so
* that `maxWorkUnits` won't be spread too thin if a lot of jobs get registered
* at once.
*/
var private config int maxJobsPerTick;
// We can (and will) automatically tick
var private bool tickAvailable;
// `true` == it is safe to use server API for a tick
// `false` == it is safe to use client API for a tick
var private bool tickFromServer;
// Our `Tick()` method is currently connected to the `OnTick()` signal.
// Keeping track of this allows us to disconnect from `OnTick()` signal
// when it is not necessary.
var private bool connectedToTick;
// How much time if left until we can write to the disk again?
var private float currentDiskCooldown;
// There is a limit (`maxJobsPerTick`) to how many different jobs we can
// perform per tick and if we register an amount jobs over that limit, we need
// to uniformly spread execution time between them.
// To achieve that we simply cyclically (in order) go over `currentJobs`
// array, each time executing exactly `maxJobsPerTick` jobs.
// `nextJobToPerform` remembers what job is to be executed next tick.
var private int nextJobToPerform;
var private array<SchedulerJob> currentJobs;
// Storing receiver objects, following example of signals/slots, is done
// without increasing their reference count, allowing them to get deallocated
// while we are still keeping their reference.
// To avoid using such deallocated receivers, we keep track of the life
// versions they've had when their disk requests were registered.
var private array<SchedulerDiskRequest> diskQueue;
var private array<AcediaObject> receivers;
var private array<int> receiversLifeVersions;
/**
* Registers new scheduler job `newJob` to be executed in the API.
*
* @param newJob New job to be scheduled for execution.
* Does nothing if given `newJob` is already added.
*/
public function AddJob(SchedulerJob newJob)
{
local int i;
if (newJob == none) {
return;
}
for (i = 0; i < currentJobs.length; i += 1)
{
if (currentJobs[i] == newJob) {
return;
}
}
newJob.NewRef();
currentJobs[currentJobs.length] = newJob;
UpdateTickConnection();
}
/**
* Requests another disk access.
*
* Use it like signal: `RequestDiskAccess(<receiver>).connect = <handler>`.
* Since it is meant to be used as a signal, so DO NOT STORE/RELEASE returned
* wrapper object `SchedulerDiskRequest`.
*
* @param receiver Same as for signal/slots, this is an object, responsible
* for the disk request. If this object gets deallocated - request will be
* thrown away.
* Typically this should be an object in which connected method will be
* executed.
* @return Wrapper object that provides `connect` delegate.
*/
public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver)
{
local SchedulerDiskRequest newRequest;
if (receiver == none) return none;
if (!receiver.IsAllocated()) return none;
newRequest =
SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest'));
diskQueue[diskQueue.length] = newRequest;
receivers[receivers.length] = receiver;
receiversLifeVersions[receiversLifeVersions.length] =
receiver.GetLifeVersion();
UpdateTickConnection();
return newRequest;
}
/**
* Tells you how many incomplete jobs are currently registered in
* the scheduler.
*
* @return How many incomplete jobs are currently registered in the scheduler.
*/
public function int GetJobsAmount()
{
CleanCompletedJobs();
return currentJobs.length;
}
/**
* Tells you how many disk access requests are currently registered in
* the scheduler.
*
* @return How many incomplete disk access requests are currently registered
* in the scheduler.
*/
public function int GetDiskQueueSize()
{
CleanDiskQueue();
return diskQueue.length;
}
/**
* In case neither server, nor client core is registered, scheduler must be
* ticked manually. For that call this method each separate tick (or whatever
* is your closest approximation available for that).
*
* Before manually invoking this method, you should check if scheduler
* actually started to tick *automatically*. Use `_.scheduler.IsAutomated()`
* for that.
*
* NOTE: If neither server-/client- core is created, nor `ManualTick()` is
* invoked manually, `SchedulerAPI` won't actually do anything.
*
* @param delta Time (real one) that is supposedly passes from the moment
* `ManualTick()` was called last time. Used for tracking disk access
* cooldowns. How `SchedulerJob`s are executed is independent from this
* value.
*/
public final function ManualTick(optional float delta)
{
Tick(delta, 1.0);
}
/**
* Is scheduler ticking automated? It can only be automated if either
* server or client level cores are created. Scheduler can automatically enable
* automation and it cannot be prevented, but can be helped by using
* `UpdateTickConnection()` method.
*
* @return `true` if scheduler's tick is automatically called and `false`
* otherwise (and calling `ManualTick()` is required).
*/
public function bool IsAutomated()
{
return tickAvailable;
}
/**
* Causes `SchedulerAPI` to try automating itself by searching for level cores
* (checking if server/client APIs are enabled).
*/
public function UpdateTickConnection()
{
local bool needsConnection;
local UnrealAPI api;
if (!tickAvailable)
{
if (_server.IsAvailable())
{
tickAvailable = true;
tickFromServer = true;
}
else if (_client.IsAvailable())
{
tickAvailable = true;
tickFromServer = false;
}
if (!tickAvailable) {
return;
}
}
needsConnection = (currentJobs.length > 0 || diskQueue.length > 0);
if (connectedToTick == needsConnection) {
return;
}
if (tickFromServer) {
api = _server.unreal;
}
else {
api = _client.unreal;
}
if (connectedToTick && !needsConnection) {
api.OnTick(self).Disconnect();
}
else if (!connectedToTick && needsConnection) {
api.OnTick(self).connect = Tick;
}
connectedToTick = needsConnection;
}
private function Tick(float delta, float dilationCoefficient)
{
delta = delta / dilationCoefficient;
// Manage disk cooldown
if (currentDiskCooldown > 0) {
currentDiskCooldown -= delta;
}
if (currentDiskCooldown <= 0 && diskQueue.length > 0)
{
currentDiskCooldown = diskSaveCooldown;
ProcessDiskQueue();
}
// Manage jobs
if (currentJobs.length > 0) {
ProcessJobs();
}
UpdateTickConnection();
}
private function ProcessJobs()
{
local int unitsPerJob;
local int jobsToPerform;
CleanCompletedJobs();
jobsToPerform = Min(currentJobs.length, maxJobsPerTick);
if (jobsToPerform <= 0) {
return;
}
unitsPerJob = maxWorkUnits / jobsToPerform;
while (jobsToPerform > 0)
{
if (nextJobToPerform >= currentJobs.length) {
nextJobToPerform = 0;
}
currentJobs[nextJobToPerform].DoWork(unitsPerJob);
nextJobToPerform += 1;
jobsToPerform -= 1;
}
}
private function ProcessDiskQueue()
{
local int i;
// Even if we clean disk queue here, we still need to double check
// lifetimes in the code below, since we have no idea what `.connect()`
// calls might do
CleanDiskQueue();
if (diskQueue.length <= 0) {
return;
}
if (diskSaveCooldown > 0)
{
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) {
diskQueue[i].connect();
}
_.memory.Free(diskQueue[0]);
diskQueue.Remove(0, 1);
receivers.Remove(0, 1);
receiversLifeVersions.Remove(0, 1);
return;
}
for (i = 0; i < diskQueue.length; i += 1)
{
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) {
diskQueue[i].connect();
}
_.memory.Free(diskQueue[i]);
}
diskQueue.length = 0;
receivers.length = 0;
receiversLifeVersions.length = 0;
}
// Removes completed jobs
private function CleanCompletedJobs()
{
local int i;
while (i < currentJobs.length)
{
if (currentJobs[i].IsCompleted())
{
if (i < nextJobToPerform) {
nextJobToPerform -= 1;
}
currentJobs[i].FreeSelf();
currentJobs.Remove(i, 1);
}
else {
i += 1;
}
}
}
// Remove disk requests with deallocated receivers
private function CleanDiskQueue()
{
local int i;
while (i < diskQueue.length)
{
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i])
{
i += 1;
continue;
}
_.memory.Free(diskQueue[i]);
diskQueue.Remove(i, 1);
receivers.Remove(i, 1);
receiversLifeVersions.Remove(i, 1);
}
}
defaultproperties
{
diskSaveCooldown = 0.25
maxWorkUnits = 10000
maxJobsPerTick = 5
}

47
sources/BaseRealm/API/Scheduler/SchedulerJob.uc

@ -1,47 +0,0 @@
/**
* Template object that represents a job, capable of being scheduled on the
* `SchedulerAPI`. Use `IsCompleted()` to mark job as completed.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class SchedulerJob extends AcediaObject
abstract;
/**
* Checks if caller `SchedulerJob` was completed.
* Once this method returns `true`, it shouldn't start returning `false` again.
*
* @return `true` if `SchedulerJob` is already completed and doesn't need to
* be further executed and `false` otherwise.
*/
public function bool IsCompleted();
/**
* Called when scheduler decides that `SchedulerJob` should be executed, taking
* amount of abstract "work units" that it is allowed to spend for work.
*
* @param allottedWorkUnits Work units allotted to the caller
* `SchedulerJob`. By default there is `10000` work units per second, so
* you can expect about 10000 / 1000 = 10 work units per millisecond or,
* on servers with 30 tick rate, about 10000 * (30 / 1000) = 300 work units
* per tick to be allotted to all the scheduled jobs.
*/
public function DoWork(int allottedWorkUnits);
defaultproperties
{
}

526
sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc

@ -1,526 +0,0 @@
/**
* Container for the information about available resources from other packages.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaEnvironment extends AcediaObject;
/**
* # `AcediaEnvironment`
*
* Instance of this class will be used by Acedia to manage resources available
* from different packages like `Feature`s and such other etc..
* This is mostly necessary to implement Acedia loader (and, possibly,
* its alternatives) that would load available packages and enable `Feature`s
* admin wants to be enabled.
*
* ## Packages
*
* Any package to be used in Acedia should first be *registered* with
* `RegisterPackage()` method. Then a manifest class from it will be read and
* Acedia will become aware of all the resources that package contains.
* Once any of those resources is used, package gets marked as *loaded* and its
* *entry object* (if specified) will be created.
*
* ## `Feature`s
*
* Whether `Feature` is enabled is governed by the `AcediaEnvironment` added
* into the `Global` class. It is possible to create several `Feature`
* instances of the same class instance of each class, but only one can be
* considered enabled at the same time.
*/
var private bool acediaShutDown;
var private array< class<_manifest> > availablePackages;
var private array< class<_manifest> > loadedPackages;
var private array< class<Feature> > availableFeatures;
var private array<Feature> enabledFeatures;
var private array<int> enabledFeaturesLifeVersions;
var private string manifestSuffix;
var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered;
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled;
var private LoggerAPI.Definition warnFeatureAlreadyEnabled;
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled;
var private SimpleSignal onShutdownSignal;
var private SimpleSignal onShutdownSystemSignal;
var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal;
var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal;
protected function Constructor()
{
// Always register our core package
RegisterPackage_S("AcediaCore");
onShutdownSignal = SimpleSignal(
_.memory.Allocate(class'SimpleSignal'));
onShutdownSystemSignal = SimpleSignal(
_.memory.Allocate(class'SimpleSignal'));
onFeatureEnabledSignal = Environment_FeatureEnabled_Signal(
_.memory.Allocate(class'Environment_FeatureEnabled_Signal'));
onFeatureDisabledSignal = Environment_FeatureDisabled_Signal(
_.memory.Allocate(class'Environment_FeatureDisabled_Signal'));
}
protected function Finalizer()
{
_.memory.Free(onShutdownSignal);
_.memory.Free(onShutdownSystemSignal);
_.memory.Free(onFeatureEnabledSignal);
_.memory.Free(onFeatureDisabledSignal);
}
/**
* Signal that will be emitted before Acedia shuts down.
* At this point all APIs should still exist and function.
*
* [Signature]
* void <slot>()
*/
/* SIGNAL */
public final function SimpleSlot OnShutDown(AcediaObject receiver)
{
return SimpleSlot(onShutdownSignal.NewSlot(receiver));
}
/**
* Signal that will be emitted during Acedia shut down. System API use it to
* clean up after themselves, so one shouldn't rely on them.
*
* There is no reason to use this signal unless you're reimplementing one of
* the APIs. Otherwise you probably want to use `OnShutDown()` signal instead.
*
* [Signature]
* void <slot>()
*/
/* SIGNAL */
public final function SimpleSlot OnShutDownSystem(AcediaObject receiver)
{
return SimpleSlot(onShutdownSystemSignal.NewSlot(receiver));
}
/**
* Signal that will be emitted when new `Feature` is enabled.
* Emitted after `Feature`'s `OnEnabled()` method was called.
*
* [Signature]
* void <slot>(Feature enabledFeature)
*
* @param enabledFeature `Feature` instance that was just enabled.
*/
/* SIGNAL */
public final function Environment_FeatureEnabled_Slot OnFeatureEnabled(
AcediaObject receiver)
{
return Environment_FeatureEnabled_Slot(
onFeatureEnabledSignal.NewSlot(receiver));
}
/**
* Signal that will be emitted when new `Feature` is disabled.
* Emitted after `Feature`'s `OnDisabled()` method was called.
*
* [Signature]
* void <slot>(class<Feature> disabledFeatureClass)
*
* @param disabledFeatureClass Class of the `Feature` instance that was
* just disabled.
*/
/* SIGNAL */
public final function Environment_FeatureDisabled_Slot OnFeatureDisabled(
AcediaObject receiver)
{
return Environment_FeatureDisabled_Slot(
onFeatureEnabledSignal.NewSlot(receiver));
}
/**
* Shuts AcediaCore down, performing all the necessary cleaning up.
*/
public final function Shutdown()
{
local LevelCore core;
if (acediaShutDown) {
return;
}
DisableAllFeatures();
onShutdownSignal.Emit();
onShutdownSystemSignal.Emit();
core = class'ServerLevelCore'.static.GetInstance();
if (core != none) {
core.Destroy();
}
core = class'ClientLevelCore'.static.GetInstance();
if (core != none) {
core.Destroy();
}
acediaShutDown = true;
}
/**
* Registers an Acedia package with name given by `packageName`.
*
* @param packageName Name of the package to register. Must not be `none`.
* This package must exist and not have yet been registered in this
* environment.
* @return `true` if package was successfully registered, `false` if it
* either does not exist, was already registered or `packageName` is
* `none`.
*/
public final function bool RegisterPackage(BaseText packageName)
{
local class<_manifest> manifestClass;
if (packageName == none) {
return false;
}
_.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy());
manifestClass = class<_manifest>(DynamicLoadObject(
packageName.ToString() $ manifestSuffix, class'Class', true));
if (manifestClass == none)
{
_.logger.Auto(errNotRegistered).Arg(packageName.Copy());
return false;
}
if (IsManifestRegistered(manifestClass))
{
_.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy());
return false;
}
availablePackages[availablePackages.length] = manifestClass;
ReadManifest(manifestClass);
return true;
}
/**
* Registers an Acedia package with name given by `packageName`.
*
* @param packageName Name of the package to register.
* This package must exist and not have yet been registered in this
* environment.
* @return `true` if package was successfully registered, `false` if it
* either does not exist or was already registered.
*/
public final function RegisterPackage_S(string packageName)
{
local Text wrapper;
wrapper = _.text.FromString(packageName);
RegisterPackage(wrapper);
_.memory.Free(wrapper);
}
private final function bool IsManifestRegistered(class<_manifest> manifestClass)
{
local int i;
for (i = 0; i < availablePackages.length; i += 1)
{
if (manifestClass == availablePackages[i]) {
return true;
}
}
return false;
}
private final function ReadManifest(class<_manifest> manifestClass)
{
local int i;
for (i = 0; i < manifestClass.default.features.length; i += 1)
{
if (manifestClass.default.features[i] == none) {
continue;
}
manifestClass.default.features[i].static.LoadConfigs();
availableFeatures[availableFeatures.length] =
manifestClass.default.features[i];
}
for (i = 0; i < manifestClass.default.testCases.length; i += 1)
{
class'TestingService'.static
.RegisterTestCase(manifestClass.default.testCases[i]);
}
}
/**
* Returns all packages registered in the caller `AcediaEnvironment`.
*
* NOTE: package being registered doesn't mean it's actually loaded.
* Package must either be explicitly loaded or automatically when one of its
* resources is being used.
*
* @return All packages registered in caller `AcediaEnvironment`.
*/
public final function array< class<_manifest> > GetAvailablePackages()
{
return availablePackages;
}
/**
* Returns all packages loaded in the caller `AcediaEnvironment`.
*
* NOTE: package being registered doesn't mean it's actually loaded.
* Package must either be explicitly loaded or automatically when one of its
* resources is being used.
*
* @return All packages loaded in caller `AcediaEnvironment`.
*/
public final function array< class<_manifest> > GetLoadedPackages()
{
return loadedPackages;
}
/**
* Returns all `Feature`s available in the caller `AcediaEnvironment`.
*
* @return All `Feature`s available in the caller `AcediaEnvironment`.
*/
public final function array< class<Feature> > GetAvailableFeatures()
{
return availableFeatures;
}
/**
* Returns all `Feature` instances enabled in the caller `AcediaEnvironment`.
*
* @return All `Feature`s enabled in the caller `AcediaEnvironment`.
*/
public final function array<Feature> GetEnabledFeatures()
{
local int i;
for (i = 0; i < enabledFeatures.length; i += 1) {
enabledFeatures[i].NewRef();
}
return enabledFeatures;
}
// CleanRemove `Feature`s that got deallocated.
// This shouldn't happen unless someone messes up.
private final function CleanEnabledFeatures()
{
local int i;
while (i < enabledFeatures.length)
{
if ( enabledFeatures[i].GetLifeVersion()
!= enabledFeaturesLifeVersions[i])
{
enabledFeatures.Remove(i, 1);
}
else {
i += 1;
}
}
}
/**
* Checks if `Feature` of given class `featureClass` is enabled.
*
* NOTE: even if If feature of class `featureClass` is enabled, it's not
* necessarily that the instance you have reference to is enabled.
* Although unlikely, it is possible that someone spawned another instance
* of the same class that isn't considered enabled. If you want to check
* whether some particular instance of given class `featureClass` is enabled,
* use `IsFeatureEnabled()` method instead.
*
* @param featureClass Feature class to check for being enabled.
* @return `true` if feature of class `featureClass` is currently enabled and
* `false` otherwise.
*/
public final function bool IsFeatureClassEnabled(class<Feature> featureClass)
{
local int i;
if (featureClass == none) {
return false;
}
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1)
{
if (featureClass == enabledFeatures[i].class) {
return true;
}
}
return false;
}
/**
* Checks if given `Feature` instance is enabled.
*
* If you want to check if any instance instance of given class
* `classToCheck` is enabled (and not `feature` specifically), use
* `IsFeatureClassEnabled()` method instead.
*
* @param feature Feature instance to check for being enabled.
* @return `true` if feature `feature` is currently enabled and
* `false` otherwise.
*/
public final function bool IsFeatureEnabled(Feature feature)
{
local int i;
if (feature == none) return false;
if (!feature.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1)
{
if (feature == enabledFeatures[i]) {
return true;
}
}
return false;
}
/**
* Returns enabled `Feature` instance of the given class `featureClass`.
*
* @param featureClass Feature class to find enabled instance for.
* @return Enabled `Feature` instance of the given class `featureClass`.
* If no feature of `featureClass` is enabled, returns `none`.
*/
public final function Feature GetEnabledFeature(class<Feature> featureClass)
{
local int i;
if (featureClass == none) {
return none;
}
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1)
{
if (featureClass == enabledFeatures[i].class)
{
enabledFeatures[i].NewRef();
return enabledFeatures[i];
}
}
return none;
}
/**
* Enables given `Feature` instance `newEnabledFeature` with a given config.
*
* @see `Feature::EnableMe()`.
*
* @param newEnabledFeature Instance to enable.
* @param configName Name of the config to enable `newEnabledFeature`
* feature with. `none` means "default" config (will be created, if
* necessary).
* @return `true` if given `newEnabledFeature` was enabled and `false`
* otherwise (including if feature of the same class has already been
* enabled).
*/
public final function bool EnableFeature(
Feature newEnabledFeature,
BaseText configName)
{
local int i;
if (newEnabledFeature == none) return false;
if (!newEnabledFeature.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1)
{
if (newEnabledFeature.class == enabledFeatures[i].class)
{
if (newEnabledFeature == enabledFeatures[i])
{
_.logger
.Auto(warnFeatureAlreadyEnabled)
.Arg(_.text.FromClass(newEnabledFeature.class));
}
else
{
_.logger
.Auto(errFeatureClassAlreadyEnabled)
.Arg(_.text.FromClass(newEnabledFeature.class));
}
return false;
}
}
newEnabledFeature.NewRef();
enabledFeatures[enabledFeatures.length] = newEnabledFeature;
enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] =
newEnabledFeature.GetLifeVersion();
newEnabledFeature.EnableInternal(configName);
onFeatureEnabledSignal.Emit(newEnabledFeature);
return true;
}
/**
* Disables given `Feature` instance `featureToDisable`.
*
* @see `Feature::EnableMe()`.
*
* @param featureToDisable Instance to disable.
* @return `true` if given `newEnabledFeature` was disabled and `false`
* otherwise (including if it already was disabled).
*/
public final function bool DisableFeature(Feature featureToDisable)
{
local int i;
if (featureToDisable == none) return false;
if (!featureToDisable.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1)
{
if (featureToDisable == enabledFeatures[i])
{
enabledFeatures.Remove(i, 1);
enabledFeaturesLifeVersions.Remove(i, 1);
featureToDisable.DisableInternal();
onFeatureDisabledSignal.Emit(featureToDisable.class);
_.memory.Free(featureToDisable);
return true;
}
}
return false;
}
/**
* Disables all currently enabled `Feature`s.
*
* Mainly intended for the clean up when Acedia shuts down.
*/
public final function DisableAllFeatures()
{
local int i;
local array<Feature> featuresCopy;
CleanEnabledFeatures();
featuresCopy = enabledFeatures;
enabledFeatures.length = 0;
enabledFeaturesLifeVersions.length = 0;
for (i = 0; i < enabledFeatures.length; i += 1)
{
featuresCopy[i].DisableInternal();
onFeatureDisabledSignal.Emit(featuresCopy[i].class);
}
_.memory.FreeMany(featuresCopy);
}
defaultproperties
{
manifestSuffix = ".Manifest"
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".")
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.")
errNotRegistered = (l=LOG_Error,m="Package \"%2\" has failed to be registered.")
warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Same instance of `Feature` class `%1` is already enabled.")
errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.")
}

104
sources/BaseRealm/Global.uc

@ -1,104 +0,0 @@
/**
* Class for an object that will provide an access to a Acedia's functionality
* that is common for both clients and servers by giving a reference to this
* object to all Acedia's objects and actors, emulating a global API namespace.
* Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Global extends Object;
// `Global` is expected to behave like a singleton and will store it's
// main instance in this variable's default value.
var protected Global myself;
var public RefAPI ref;
var public BoxAPI box;
var public MathAPI math;
var public LoggerAPI logger;
var public CollectionsAPI collections;
var public AliasesAPI alias;
var public TextAPI text;
var public MemoryAPI memory;
var public ConsoleAPI console;
var public ChatAPI chat;
var public ColorAPI color;
var public UserAPI users;
var public PlayersAPI players;
var public JSONAPI json;
var public DBAPI db;
var public SchedulerAPI scheduler;
var public AvariceAPI avarice;
var public AcediaEnvironment environment;
public final static function Global GetInstance()
{
if (default.myself == none) {
// `...Global`s are special and exist outside main Acedia's
// object infrastructure, so we allocate it without using API methods.
default.myself = new class'Global';
default.myself.Initialize();
}
return default.myself;
}
protected function Initialize()
{
// Special case that we cannot spawn with memory API since it obviously
// does not exist yet!
memory = new class'MemoryAPI';
memory._constructor();
// `TextAPI` and `CollectionsAPI` need to be loaded before `LoggerAPI`
ref = RefAPI(memory.Allocate(class'RefAPI'));
box = BoxAPI(memory.Allocate(class'BoxAPI'));
text = TextAPI(memory.Allocate(class'TextAPI'));
math = MathAPI(memory.Allocate(class'MathAPI'));
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
color = ColorAPI(memory.Allocate(class'ColorAPI'));
alias = AliasesAPI(memory.Allocate(class'AliasesAPI'));
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI'));
chat = ChatAPI(memory.Allocate(class'ChatAPI'));
users = UserAPI(memory.Allocate(class'UserAPI'));
players = PlayersAPI(memory.Allocate(class'PlayersAPI'));
json = JSONAPI(memory.Allocate(class'JSONAPI'));
db = DBAPI(memory.Allocate(class'DBAPI'));
scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI'));
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI'));
environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment'));
}
public function DropCoreAPI()
{
memory = none;
ref = none;
box = none;
text = none;
collections = none;
logger = none;
alias = none;
console = none;
chat = none;
color = none;
users = none;
players = none;
json = none;
db = none;
scheduler = none;
avarice = none;
default.myself = none;
}

76
sources/BaseRealm/Iter.uc

@ -1,76 +0,0 @@
/**
* Base class for iterator, an auxiliary object for iterating through
* a set of objects obtained from some context-dependent source.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Iter extends AcediaObject
abstract;
/**
* Iterators can filter objects they're iterating on by a presence or lack of
* a certain property, recording this choice requires 3 values, so `bool`
* isn't enough and we need to use this `enum` instead.
*/
enum IterFilter
{
// We don't use relevant property for filtering
ITF_Nothing,
// Iterated objects must have that property
ITF_Have,
// Iterated objects must not have that property
ITF_NotHave
};
/**
* Makes iterator pick next item.
* Use `HasFinished()` to check whether you have iterated all of them.
*
* @return Reference to caller `Iterator` to allow for method chaining.
*/
public function Iter Next();
/**
* Returns current value pointed to by an iterator.
*
* Does not advance iteration: use `Next()` to pick next value.
*
* @return Current value being iterated over. If `Iterator()` has finished
* iterating over all values or was not initialized - returns `none`.
* Note that depending on context `none` values can also be returned,
* use `LeaveOnlyNotNone()` method to prevent that.
*/
public function AcediaObject Get();
/**
* Checks if caller `Iterator` has finished iterating.
*
* @return `true` if caller `Iterator` has finished iterating or
* was not initialized. `false` otherwise.
*/
public function bool HasFinished();
/**
* Makes caller iterator skip any `none` items during iteration.
*
* @return Reference to caller `Iterator` to allow for method chaining.
*/
public function Iter LeaveOnlyNotNone();
defaultproperties
{
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save