diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a3108ab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,33 @@ +root = true + +# Global +[*] +indent_style = unset +indent_size = 4 +tab_width = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = unset + +# Unreal Engine 3 / Source +[*.uc] +indent_style = tab + +[*.{uci,upkg}] + +# Unreal Engine 3 / i18n +[*.{chn,cht,cze,dan,deu,dut,esl,esn,fra,frc,hun,int,ita,jpn,kor,pol,por,ptb,rus,tur,ukr}] +charset = utf-16le + +# Other +[*.md] +indent_style = space +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 + +[*.{txt,cfg,conf}] +indent_style = tab diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml new file mode 100644 index 0000000..e9b52d6 --- /dev/null +++ b/.github/workflows/mega-linter.yml @@ -0,0 +1,72 @@ +--- +name: MegaLinter + +permissions: read-all + +on: + push: + pull_request: + branches: [master] + +env: + APPLY_FIXES: none + APPLY_FIXES_EVENT: pull_request + APPLY_FIXES_MODE: commit + DISABLE: SPELL + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + build: + name: MegaLinter + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + - name: MegaLinter + id: ml + uses: oxsecurity/megalinter@v7 + env: + VALIDATE_ALL_CODEBASE: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Archive production artifacts + if: ${{ success() }} || ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: MegaLinter reports + path: | + megalinter-reports + mega-linter.log + + - name: Create Pull Request with applied fixes + id: cpr + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + commit-message: "[MegaLinter] Apply linters automatic fixes" + title: "[MegaLinter] Apply linters automatic fixes" + labels: bot + - name: Create PR output + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) + run: | + echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" + + - name: Prepare commit + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) + run: sudo chown -Rc $UID .git/ + - name: Commit and push applied linter fixes + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) + uses: stefanzweifel/git-auto-commit-action@v4 + with: + branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} + commit_message: "[MegaLinter] Apply linters fixes" + commit_user_name: "github-actions" + commit_user_email: "github-actions[bot]@users.noreply.github.com" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..27ed978 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tools"] + path = tools + url = https://github.com/GenZmeY/KF2-BuildTools diff --git a/PublicationContent/description.txt b/PublicationContent/description.txt new file mode 100644 index 0000000..cbfa7f4 --- /dev/null +++ b/PublicationContent/description.txt @@ -0,0 +1,69 @@ +[img]https://img.shields.io/static/v1?logo=GitHub&labelColor=gray&color=blue&logoColor=white&label=&message=Open Source[/img] [img]https://img.shields.io/github/license/GenZmeY/KF2-TrueRandomBoss[/img] [img]https://img.shields.io/steam/favorites/3047331564[/img] [img]https://img.shields.io/steam/update-date/3047331564[/img] [url=https://steamcommunity.com/sharedfiles/filedetails/changelog/3047331564][img]https://img.shields.io/github/v/tag/GenZmeY/KF2-TrueRandomBoss[/img][/url] + +[h1]Description[/h1] +Server-side mutator that makes bosses truly random. + +[h1]What does it mean?[/h1] +Some players use the command to predict the boss. Here's the command: +[code]getall kfgamereplicationinfo bossindex[/code] +Copy this command, open the console (~), paste the command and press Enter. +In response, you will receive a string, the last number of which is the boss index: +[code] +0 = Hans Volter +1 = Patriarch +2 = King Fleshpound +3 = Abomination +4 = Matriarch +[/code] +This is possible because the boss is chosen at the beginning of the game. + +[b]This mutator sets a random boss index right before the start of the boss wave and thus makes the command above completely useless.[/b] + +[h1]Whitelisted?[/h1] +No. This mod is not whitelisted and will de-rank your server. Any XP gained will not be saved. + +[h1]Usage (single player)[/h1] +[olist] +[*]Subscribe to this mutator; +[*]Start KF2; +[*]Open console (~) and input: +[b]open KF-BioticsLab?Mutator=TRB.TRBMut[/b] +(replace the map and add the parameters you need) +[*]. +[/olist] + +[h1]Usage (server)[/h1] +[b]Note:[/b] [i]If you don't understand what is written here, read the article [url=https://wiki.killingfloor2.com/index.php?title=Dedicated_Server_(Killing_Floor_2)][u]Dedicated Server (KF2 wiki)[/u][/url] before following these instructions.[/i] +[olist] +[*]Open your [b]PCServer-KFEngine.ini[/b] / [b]LinuxServer-KFEngine.ini[/b]; +[*]Find the [b][IpDrv.TcpNetDriver][/b] section and make sure that there is a line (add if not): +[b]DownloadManagers=OnlineSubsystemSteamworks.SteamWorkshopDownload[/b] +❗️ If there are several [b]DownloadManagers=[/b] then the line above should be the first ❗️ +[*]Add the following string to the [b][OnlineSubsystemSteamworks.KFWorkshopSteamworks][/b] section (create one if it doesn't exist): +[b]ServerSubscribedWorkshopItems=3047331564[/b] +[*]Start the server and wait while the mutator is downloading; +[*]Add mutator to server start parameters: [b]?Mutator=TRB.TRBMut[/b] and restart the server. +[/olist] + +[h1]Important setup information[/h1] +The config should be created on first start, but now the game contains a bug that initializes the config values ​​randomly if they are not explicitly set. Thus, the config may have incorrect values ​​or not be created at all. +So if you are using this mutator for the first time, I highly recommend doing the following: +[olist] +[*]Create (modify) [b]KFTRB.ini[/b] manually. Put the following content there: +[b][TRB.TRB] +Version=0[/b] +[*]Start the game/server with TRB to generate the contents of the config. +[*]Close the game/server. +[/olist] +[b]Right now this is the only way to correctly create the default config.[/b] +Unfortunately I can't do anything about it because it's a game problem (not mutator). I hope TWI fixes this someday. + +[h1]Setup (KFTRB.ini)[/h1] +Honestly, you don't need to edit anything in the config, [b]just make sure you create the default config correctly[/b]. +The config exists because I needed it during development ¯\_(ツ)_/¯ + +[h1]Notes[/h1] +📌 Even if you use this mod, the boss prediction command can sometimes give correct predictions, just by chance. And that's okay. After all, this is a real random :) + +[h1]Sources[/h1] +[url=https://github.com/GenZmeY/KF2-TrueRandomBoss]https://github.com/GenZmeY/KF2-TrueRandomBoss[/url] (GNU GPLv3) diff --git a/PublicationContent/preview.png b/PublicationContent/preview.png new file mode 100644 index 0000000..a742745 Binary files /dev/null and b/PublicationContent/preview.png differ diff --git a/PublicationContent/previewDesc.png b/PublicationContent/previewDesc.png new file mode 100644 index 0000000..9268883 Binary files /dev/null and b/PublicationContent/previewDesc.png differ diff --git a/PublicationContent/tags.txt b/PublicationContent/tags.txt new file mode 100644 index 0000000..bcf8fe8 --- /dev/null +++ b/PublicationContent/tags.txt @@ -0,0 +1 @@ +Mutators diff --git a/PublicationContent/title.txt b/PublicationContent/title.txt new file mode 100644 index 0000000..1815df9 --- /dev/null +++ b/PublicationContent/title.txt @@ -0,0 +1 @@ +True Random Boss diff --git a/README.md b/README.md index 4570437..3735c74 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# KF2-TrueRandomBoss \ No newline at end of file +# True Random Boss + +[![Steam Workshop](https://img.shields.io/static/v1?message=workshop&logo=steam&labelColor=gray&color=blue&logoColor=white&label=steam%20)](https://steamcommunity.com/sharedfiles/filedetails/?id=3047331564) +[![Steam Downloads](https://img.shields.io/steam/downloads/3047331564)](https://steamcommunity.com/sharedfiles/filedetails/?id=3047331564) +[![Steam Favorites](https://img.shields.io/steam/favorites/3047331564)](https://steamcommunity.com/sharedfiles/filedetails/?id=3047331564) +[![MegaLinter](https://github.com/GenZmeY/KF2-TrueRandomBoss/actions/workflows/mega-linter.yml/badge.svg?branch=master)](https://github.com/GenZmeY/KF2-TrueRandomBoss/actions/workflows/mega-linter.yml) +[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/GenZmeY/KF2-TrueRandomBoss)](https://github.com/GenZmeY/KF2-TrueRandomBoss/tags) +[![GitHub](https://img.shields.io/github/license/GenZmeY/KF2-TrueRandomBoss)](LICENSE) + +## Description +Server-side mutator that makes bosses truly random. + +## Usage & Setup +[See steam workshop page](https://steamcommunity.com/sharedfiles/filedetails/?id=3047331564) + +## Build +**Note:** If you want to build/test/brew/publish a mutator without git-bash and/or scripts, follow [these instructions](https://tripwireinteractive.atlassian.net/wiki/spaces/KF2SW/pages/26247172/KF2+Code+Modding+How-to) instead of what is described here. +1. Install [Killing Floor 2](https://store.steampowered.com/app/232090/Killing_Floor_2/), Killing Floor 2 - SDK and [git for windows](https://git-scm.com/download/win); +2. open git-bash and go to any folder where you want to store sources: +`cd ` +3. Clone this repository and go to the source folder: +`git clone https://github.com/GenZmeY/KF2-TrueRandomBoss && cd KF2-TrueRandomBoss` +4. Download dependencies: +`git submodule init && git submodule update` +5. Compile: +`./tools/builder -c` +5. The compiled files will be here: +`C:\Users\\Documents\My Games\KillingFloor2\KFGame\Unpublished\BrewedPC\Script\` + +## Bug reports +If you find a bug, go to the [issue page](https://github.com/GenZmeY/KF2-TrueRandomBoss/issues) and check if there is a description of your bug. If not, create a new issue. +Describe what the bug looks like and how reproduce it. + +## License +[![license](https://www.gnu.org/graphics/gplv3-with-text-136x68.png)](LICENSE) diff --git a/TRB/Classes/KFGI_Access.uc b/TRB/Classes/KFGI_Access.uc new file mode 100644 index 0000000..1f25013 --- /dev/null +++ b/TRB/Classes/KFGI_Access.uc @@ -0,0 +1,40 @@ +class KFGI_Access extends Object + within KFGameInfo; + +public function SetRandomBoss() +{ + OverrideBossIndex(Rand(default.AIBossClassList.Length)); +} + +public function class Boss() +{ + return BossAITypePawn(BossIndex); +} + +public function OverrideBossIndex(int Index, optional bool Force = false) +{ + if (Index < 0 || Index >= default.AIBossClassList.Length) + { + return; + } + + if (!UseSpecificBossIndex(BossIndex) || Force) + { + BossIndex = Index; + } + + MyKFGRI.CacheSelectedBoss(BossIndex); +} + +public function class BossAITypePawn(int AIType) +{ + if (AIType < AIBossClassList.Length) + return AIBossClassList[AIType]; + else + return None; +} + +defaultproperties +{ + +} diff --git a/TRB/Classes/TRB.uc b/TRB/Classes/TRB.uc new file mode 100644 index 0000000..23879b9 --- /dev/null +++ b/TRB/Classes/TRB.uc @@ -0,0 +1,222 @@ +class TRB extends Info + config(TRB); + +const LatestVersion = 1; + +enum E_State +{ + S_WAIT, + S_WAVE, + S_TRADER, + S_END +}; + +var private config int Version; +var private config E_LogLevel LogLevel; +var private config float WavePollingDT; +var private config float TraderPollingDT; + +var private KFGameInfo KFGI; +var private KFGI_Access KFGIA; +var private KFGameReplicationInfo KFGRI; + +var private E_State GameState; + +public simulated function bool SafeDestroy() +{ + return (bPendingDelete || bDeleteMe || Destroy()); +} + +public event PreBeginPlay() +{ + `Log_Trace(); + + if (WorldInfo.NetMode == NM_Client) + { + `Log_Fatal("NetMode == NM_Client, Destroy..."); + SafeDestroy(); + return; + } + + Super.PreBeginPlay(); + + PreInit(); +} + +private function PreInit() +{ + `Log_Trace(); + + if (Version == `NO_CONFIG) + { + LogLevel = LL_Info; + WavePollingDT = 5.0f; + TraderPollingDT = 1.0f; + SaveConfig(); + } + + switch (Version) + { + case `NO_CONFIG: + `Log_Info("Config created"); + + case MaxInt: + `Log_Info("Config updated to version"@LatestVersion); + break; + + case LatestVersion: + `Log_Info("Config is up-to-date"); + break; + + default: + `Log_Warn("The config version is higher than the current version"); + `Log_Warn("Config version is" @ Version @ "but current version is" @ LatestVersion); + `Log_Warn("The config version will be changed to" @ LatestVersion); + break; + } + + if (LatestVersion != Version) + { + Version = LatestVersion; + SaveConfig(); + } + + if (LogLevel == LL_WrongLevel) + { + LogLevel = LL_Info; + `Log_Warn("Wrong 'LogLevel', return to default value"); + SaveConfig(); + } + `Log_Base("LogLevel:" @ LogLevel); +} + +public event PostBeginPlay() +{ + `Log_Trace(); + + if (bPendingDelete || bDeleteMe) return; + + Super.PostBeginPlay(); + + PostInit(); +} + +private function PostInit() +{ + `Log_Trace(); + + if (WorldInfo == None || WorldInfo.Game == None) + { + SetTimer(1.0f, false, nameof(PostInit)); + return; + } + + KFGI = KFGameInfo(WorldInfo.Game); + if (KFGI == None) + { + `Log_Fatal("Incompatible gamemode:" @ WorldInfo.Game $ ". Destroy..."); + SafeDestroy(); + return; + } + + if (KFGI.GameReplicationInfo == None) + { + SetTimer(1.0f, false, nameof(PostInit)); + return; + } + + KFGRI = KFGameReplicationInfo(KFGI.GameReplicationInfo); + if (KFGRI == None) + { + `Log_Fatal("Incompatible Replication info:" @ KFGI.GameReplicationInfo); + SafeDestroy(); + return; + } + + KFGIA = new(KFGI) class'KFGI_Access'; + if (KFGIA == None) + { + `Log_Fatal("Can't create KFGI_Access object"); + SafeDestroy(); + return; + } + + SetTimer(WavePollingDT, true, nameof(WavePolling)); +} + +private function WavePolling() +{ + `Log_Trace(); + + if (!StateChanged()) return; + + `Log_Debug("GameState:" @ GameState); + + switch (GameState) + { + case S_TRADER: + if (KFGRI.IsBossWaveNext()) + { + `Log_Debug("Prepare to boss wave"); + SetTimer(TraderPollingDT, true, nameof(TraderPolling)); + ClearTimer(nameof(WavePolling)); + } + break; + + case S_END: + `Log_Debug("Cleanup"); + ClearTimer(nameof(WavePolling)); + ClearTimer(nameof(TraderPolling)); + SafeDestroy(); + break; + + case S_WAIT: + case S_WAVE: + default: + break; + } +} + +private function TraderPolling() +{ + `Log_Trace(); + + if (KFGRI.GetTraderTimeRemaining() < class'KFGame.KFVoteCollector'.default.TimeAfterSkipTrader || KFGRI.bWaveStarted) + { + `Log_Debug("Prev boss:" @ String(KFGIA.Boss())); + KFGIA.SetRandomBoss(); + `Log_Info("New boss:" @ String(KFGIA.Boss())); + ClearTimer(nameof(TraderPolling)); + SetTimer(WavePollingDT, true, nameof(WavePolling)); + return; + } +} + +private function E_State GetState() +{ + `Log_Trace(); + + if (KFGRI.bMatchIsOver) return S_END; + if (!KFGRI.bMatchHasBegun) return S_WAIT; + if (KFGRI.bTraderIsOpen) return S_TRADER; + if (KFGRI.bWaveStarted) return S_WAVE; +} + +private function bool StateChanged() +{ + local E_State NewState; + local bool StateChanged; + + `Log_Trace(); + + NewState = GetState(); + StateChanged = (GameState != NewState); + GameState = NewState; + + return StateChanged; +} + +defaultproperties +{ + GameState = S_WAIT +} \ No newline at end of file diff --git a/TRB/Classes/TRB.upkg b/TRB/Classes/TRB.upkg new file mode 100644 index 0000000..ae6c83c --- /dev/null +++ b/TRB/Classes/TRB.upkg @@ -0,0 +1,4 @@ +[Flags] +AllowDownload=False +ClientOptional=False +ServerSideOnly=True diff --git a/TRB/Classes/TRBMut.uc b/TRB/Classes/TRBMut.uc new file mode 100644 index 0000000..e224ab0 --- /dev/null +++ b/TRB/Classes/TRBMut.uc @@ -0,0 +1,47 @@ +class TRBMut extends KFMutator + dependson(TRB); + +var private TRB TRB; + +public simulated function bool SafeDestroy() +{ + return (bPendingDelete || bDeleteMe || Destroy()); +} + +public event PreBeginPlay() +{ + Super.PreBeginPlay(); + + if (WorldInfo.NetMode == NM_Client) return; + + foreach WorldInfo.DynamicActors(class'TRB', TRB) + { + break; + } + + if (TRB == None) + { + TRB = WorldInfo.Spawn(class'TRB'); + } + + if (TRB == None) + { + `Log_Base("FATAL: Can't Spawn 'TRB'"); + SafeDestroy(); + } +} + +public function AddMutator(Mutator Mut) +{ + if (Mut == Self) return; + + if (Mut.Class == Class) + TRBMut(Mut).SafeDestroy(); + else + Super.AddMutator(Mut); +} + +defaultproperties +{ + +} \ No newline at end of file diff --git a/TRB/Classes/_Logger.uc b/TRB/Classes/_Logger.uc new file mode 100644 index 0000000..d9cfb52 --- /dev/null +++ b/TRB/Classes/_Logger.uc @@ -0,0 +1,20 @@ +class _Logger extends Object + abstract; + +enum E_LogLevel +{ + LL_WrongLevel, + LL_None, + LL_Fatal, + LL_Error, + LL_Warning, + LL_Info, + LL_Debug, + LL_Trace, + LL_All +}; + +defaultproperties +{ + +} diff --git a/TRB/Constants.uci b/TRB/Constants.uci new file mode 100644 index 0000000..432fa68 --- /dev/null +++ b/TRB/Constants.uci @@ -0,0 +1,2 @@ +// Constants +`define NO_CONFIG 0 diff --git a/TRB/Globals.uci b/TRB/Globals.uci new file mode 100644 index 0000000..4dcd4fb --- /dev/null +++ b/TRB/Globals.uci @@ -0,0 +1,3 @@ +// Imports +`include(Logger.uci) +`include(Constants.uci) diff --git a/TRB/Logger.uci b/TRB/Logger.uci new file mode 100644 index 0000000..de6576b --- /dev/null +++ b/TRB/Logger.uci @@ -0,0 +1,15 @@ +// Logger +`define Log_Tag 'TRB' + +`define LocationStatic "`{ClassName}::" $ GetFuncName() + +`define Log_Base(msg, cond) `log(`msg `if(`cond), `cond`{endif}, `Log_Tag) + +`define Log_Fatal(msg) `log("FATAL:" @ `msg, (LogLevel >= LL_Fatal), `Log_Tag) +`define Log_Error(msg) `log("ERROR:" @ `msg, (LogLevel >= LL_Error), `Log_Tag) +`define Log_Warn(msg) `log("WARN:" @ `msg, (LogLevel >= LL_Warning), `Log_Tag) +`define Log_Info(msg) `log("INFO:" @ `msg, (LogLevel >= LL_Info), `Log_Tag) +`define Log_Debug(msg) `log("DEBUG:" @ `msg, (LogLevel >= LL_Debug), `Log_Tag) + +`define Log_Trace(msg) `log("TRACE:" @ `Location `if(`msg) @ `msg`{endif}, (LogLevel >= LL_Trace), `Log_Tag) +`define Log_TraceStatic(msg) `log("TRACE:" @ `LocationStatic `if(`msg) @ `msg`{endif}, (LogLevel >= LL_Trace), `Log_Tag) diff --git a/builder.cfg b/builder.cfg new file mode 100644 index 0000000..06e70b0 --- /dev/null +++ b/builder.cfg @@ -0,0 +1,61 @@ +### Build parameters ### + +# If True - compresses the mutator when compiling +# Scripts will be stored in binary form +# (reduces the size of the output file) +StripSource="True" + +# Mutators to be compiled +# Specify them with a space as a separator, +# Mutators will be compiled in the specified order +PackageBuildOrder="TRB" + + +### Brew parameters ### + +# Packages you want to brew using @peelz's patched KFEditor. +# Useful for cases where regular brew doesn't put *.upk inside the package. +# Specify them with a space as a separator, +# The order doesn't matter +PackagePeelzBrew="" + + +### Steam Workshop upload parameters ### + +# Mutators that will be uploaded to the workshop +# Specify them with a space as a separator, +# The order doesn't matter +PackageUpload="TRB" + + +### Test parameters ### + +# Map: +Map="KF-Nuked" + +# Game: +# Survival: KFGameContent.KFGameInfo_Survival +# WeeklyOutbreak: KFGameContent.KFGameInfo_WeeklySurvival +# Endless: KFGameContent.KFGameInfo_Endless +# Objective: KFGameContent.KFGameInfo_Objective +# Versus: KFGameContent.KFGameInfo_VersusSurvival +Game="KFGameContent.KFGameInfo_Survival" + +# Difficulty: +# Normal: 0 +# Hard: 1 +# Suicide: 2 +# Hell: 3 +Difficulty="0" + +# GameLength: +# 4 waves: 0 +# 7 waves: 1 +# 10 waves: 2 +GameLength="0" + +# Mutators +Mutators="TRB.TRBMut" + +# Additional parameters +Args="" diff --git a/tools b/tools new file mode 160000 index 0000000..fb458ac --- /dev/null +++ b/tools @@ -0,0 +1 @@ +Subproject commit fb458ac61f7e6c6426b8dff366dd5e7499e0d95f