diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..569c208 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +testing.ini +3rd-party-bin diff --git a/MskGs/Classes/MskGsGFxMenu_Trader.uc b/MskGs/Classes/MskGsGFxMenu_Trader.uc new file mode 100644 index 0000000..64a4456 --- /dev/null +++ b/MskGs/Classes/MskGsGFxMenu_Trader.uc @@ -0,0 +1,8 @@ +class MskGsGFxMenu_Trader extends KFGFxMenu_Trader + dependsOn(MskGsGFxTraderContainer_Store); + +defaultproperties +{ + SubWidgetBindings.Remove((WidgetName="shopContainer",WidgetClass=class'KFGFxTraderContainer_Store')) + SubWidgetBindings.Add((WidgetName="shopContainer",WidgetClass=class'MskGsGFxTraderContainer_Store')) +} diff --git a/MskGs/Classes/MskGsGFxMoviePlayer_Manager.uc b/MskGs/Classes/MskGsGFxMoviePlayer_Manager.uc new file mode 100644 index 0000000..35fa26c --- /dev/null +++ b/MskGs/Classes/MskGsGFxMoviePlayer_Manager.uc @@ -0,0 +1,8 @@ +class MskGsGFxMoviePlayer_Manager extends KFGFxMoviePlayer_Manager + dependsOn(MskGsGFxMenu_Trader); + +defaultproperties +{ + WidgetBindings.Remove((WidgetName="traderMenu",WidgetClass=class'KFGFxMenu_Trader')) + WidgetBindings.Add((WidgetName="traderMenu",WidgetClass=class'MskGsGFxMenu_Trader')) +} diff --git a/MskGs/Classes/MskGsGFxTraderContainer_Store.uc b/MskGs/Classes/MskGsGFxTraderContainer_Store.uc new file mode 100644 index 0000000..0657d3d --- /dev/null +++ b/MskGs/Classes/MskGsGFxTraderContainer_Store.uc @@ -0,0 +1,35 @@ +class MskGsGFxTraderContainer_Store extends KFGFxTraderContainer_Store; + +var bool GroupMember; + +function Initialize(KFGFxObject_Menu NewParentMenu) +{ + local OnlineSubsystemSteamworks OnlineSub; + local UniqueNetId GroupID; + + super.Initialize(NewParentMenu); + + OnlineSub = OnlineSubsystemSteamworks(class'GameEngine'.static.GetOnlineSubsystem()); + class'OnlineSubsystem'.Static.StringToUniqueNetId("0x017000000223386E", GroupID); + GroupMember = OnlineSub.CheckPlayerGroup(GroupID); +} + +function bool IsItemFiltered(STraderItem Item, optional bool bDebug) +{ + if (KFPC.GetPurchaseHelper().IsInOwnedItemList(Item.ClassName)) + return true; + if (KFPC.GetPurchaseHelper().IsInOwnedItemList(Item.DualClassName)) + return true; + if (!KFPC.GetPurchaseHelper().IsSellable(Item)) + return true; + if (!GroupMember && Item.WeaponDef.default.SharedUnlockId != SCU_None && !class'KFUnlockManager'.static.IsSharedContentUnlocked(Item.WeaponDef.default.SharedUnlockId)) + return true; + if (Item.WeaponDef.default.PlatformRestriction != PR_All && class'KFUnlockManager'.static.IsPlatformRestricted(Item.WeaponDef.default.PlatformRestriction)) + return true; + + return false; +} + +defaultproperties +{ +} diff --git a/MskGs/Classes/MskGsMut.uc b/MskGs/Classes/MskGsMut.uc new file mode 100644 index 0000000..6e5282c --- /dev/null +++ b/MskGs/Classes/MskGsMut.uc @@ -0,0 +1,46 @@ +Class MskGsMut extends KFMutator; + +simulated event PostBeginPlay() +{ + super.PostBeginPlay(); + + if (WorldInfo.Game.BaseMutator == None) + WorldInfo.Game.BaseMutator = Self; + else + WorldInfo.Game.BaseMutator.AddMutator(Self); + + if (bDeleteMe) + return; + + Initialize(); +} + +function Initialize() +{ + if (MyKFGI == None || MyKFGI.MyKFGRI == None) + { + SetTimer(2.f, false, nameof(Initialize)); + return; + } + + MyKFGI.KFGFxManagerClass = class'MskGsGFxMoviePlayer_Manager'; + MyKFGI.MyKFGRI.VoteCollectorClass = class'MskGsVoteCollector'; + MyKFGI.MyKFGRI.PostBeginPlay(); + + `Log("[MskGsMut] Mutator loaded."); +} + +function AddMutator(Mutator Mut) +{ + if (Mut == Self) + return; + + if (Mut.Class == Class) + Mut.Destroy(); + else + Super.AddMutator(Mut); +} + +defaultproperties +{ +} diff --git a/MskGs/Classes/MskGsMut.upkg b/MskGs/Classes/MskGsMut.upkg new file mode 100644 index 0000000..29cb156 --- /dev/null +++ b/MskGs/Classes/MskGsMut.upkg @@ -0,0 +1,4 @@ +[Flags] +AllowDownload=True +ClientOptional=False +ServerSideOnly=False diff --git a/MskGs/Classes/MskGsVoteCollector.uc b/MskGs/Classes/MskGsVoteCollector.uc new file mode 100644 index 0000000..4645ab1 --- /dev/null +++ b/MskGs/Classes/MskGsVoteCollector.uc @@ -0,0 +1,241 @@ +class MskGsVoteCollector extends KFVoteCollector; + +var private const array ImportantPersonList; +var private array PunishList; + +function ServerStartPunishment() +{ + local KFGameReplicationInfo KFGRI; + local KFGameInfo KFGI; + local KFPlayerController KFPC; + local int i; + + if (PunishList.Length == 0) + return; + + KFGRI = KFGameReplicationInfo(WorldInfo.GRI); + KFGI = KFGameInfo(WorldInfo.Game); + + for (i=0; i < PunishList.Length; i++) + { + KFPC = PunishList[i]; + if (KFGRI.bMatchHasBegun) + { + KFPC.Suicide(); + } + else if (KFGI.AccessControl != none) + { + KFAccessControl(KFGI.AccessControl).ForceKickPlayer(KFPC, KFGI.AccessControl.KickedMsg); + KFGI.BroadcastLocalized(KFGI, class'KFLocalMessage', LMT_KickVoteSucceeded, CurrentKickVote.PlayerPRI); + } + } + PunishList.Length = 0; +} + +function bool ImportantKickee(PlayerReplicationInfo PRI_Kickee, PlayerReplicationInfo PRI_Kicker) +{ + local string PunishMessage; + local KFPlayerController KFPC_Kicker; + + if (ImportantPersonList.Find(class'OnlineSubsystem'.Static.UniqueNetIdToString(PRI_Kickee.UniqueId)) != -1) + { + KFPC_Kicker = KFPlayerController(PRI_Kicker.Owner); + if (PunishList.Find(KFPC_Kicker) == -1) + { + PunishMessage = PRI_Kicker.PlayerName@"tried to kick"@PRI_Kickee.PlayerName@", but sat down on the bottle instead (^_^)"; + WorldInfo.Game.Broadcast(KFPC_Kicker, PunishMessage); + + PunishList.AddItem(KFPC_Kicker); + SetTimer(2.0f, false, 'ServerStartPunishment', self); + } + return true; + } + return false; +} + +function ServerStartVoteKick(PlayerReplicationInfo PRI_Kickee, PlayerReplicationInfo PRI_Kicker) +{ + local int i; + local array PRIs; + local KFGameInfo KFGI; + local KFPlayerController KFPC, KickeePC; + + KFGI = KFGameInfo(WorldInfo.Game); + KFPC = KFPlayerController(PRI_Kicker.Owner); + KickeePC = KFPlayerController(PRI_Kickee.Owner); + + // Kick voting is disabled + if(KFGI.bDisableKickVote) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteDisabled); + return; + } + + // Spectators aren't allowed to vote + if(PRI_Kicker.bOnlySpectator) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteNoSpectators); + return; + } + + // Not enough players to start a vote + if( KFGI.NumPlayers <= 2 ) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteNotEnoughPlayers); + return; + } + + // Maximum number of players kicked per match has been reached + if( KickedPlayers >= 2 ) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteMaxKicksReached); + return; + } + + // Bottling + if (ImportantKickee(PRI_Kickee, PRI_Kicker)) + { + return; + } + + // Can't kick admins + if(KFGI.AccessControl != none) + { + if(KFGI.AccessControl.IsAdmin(KickeePC)) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteAdmin); + return; + } + } + + // Last vote failed, must wait until failed vote cooldown before starting a new vote + if( bIsFailedVoteTimerActive ) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteRejected); + return; + } + + // A kick vote is not allowed while another vote is active + if(bIsSkipTraderVoteInProgress) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_OtherVoteInProgress); + return; + } + + if( !bIsKickVoteInProgress ) + { + // Clear voter array + PlayersThatHaveVoted.Length = 0; + + // Cache off these values in case player leaves before vote ends -- no cheating! + CurrentKickVote.PlayerID = PRI_Kickee.UniqueId; + CurrentKickVote.PlayerPRI = PRI_Kickee; + CurrentKickVote.PlayerIPAddress = KickeePC.GetPlayerNetworkAddress(); + + bIsKickVoteInProgress = true; + + GetKFPRIArray(PRIs); + for (i = 0; i < PRIs.Length; i++) + { + PRIs[i].ShowKickVote(PRI_Kickee, VoteTime, !(PRIs[i] == PRI_Kicker || PRIs[i] == PRI_Kickee)); + } + KFGI.BroadcastLocalized(KFGI, class'KFLocalMessage', LMT_KickVoteStarted, CurrentKickVote.PlayerPRI); + WorldInfo.Game.Broadcast(KFPC, PRI_Kicker.PlayerName@"starts voting for kick"@PRI_Kickee.PlayerName); + SetTimer( VoteTime, false, nameof(ConcludeVoteKick), self ); + // Cast initial vote + RecieveVoteKick(PRI_Kicker, true); + } + else if(PRI_Kickee == CurrentKickVote.PlayerPRI) + { + RecieveVoteKick(PRI_Kicker, false); + } + else + { + // Can't start a new vote until current one is over + KFPlayerController(PRI_Kicker.Owner).ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteInProgress); + } +} + +reliable server function RecieveVoteKick(PlayerReplicationInfo PRI, bool bKick) +{ + local KFPlayerController KFPC; + + if(PlayersThatHaveVoted.Find(PRI) == INDEX_NONE) + { + //accept their vote + PlayersThatHaveVoted.AddItem(PRI); + if(bKick) + { + yesVotes++; + } + else + { + noVotes++; + } + + KFPC = KFPlayerController(PRI.Owner); + if(KFPC != none) + { + if(bKick) + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteYesReceived, CurrentKickVote.PlayerPRI); + WorldInfo.Game.Broadcast(KFPC, PRI.PlayerName@"vote: Yes"); + } + else + { + KFPC.ReceiveLocalizedMessage(class'KFLocalMessage', LMT_KickVoteNoReceived, CurrentKickVote.PlayerPRI); + WorldInfo.Game.Broadcast(KFPC, PRI.PlayerName@"vote: No"); + } + + } + + if( ShouldConcludeKickVote() ) + { + ConcludeVoteKick(); + } + else + { + ReplicateKickVotes(); + } + } +} + +function int GetNextMap() +{ + local KFGameInfo KFGI; + local array ActiveMapCycle; + local array AviableMaps; + local string Map; + + if(MapVoteList.Length > 0) + { + return MapVoteList[0].MapIndex; + } + else // random default map that exists in the active map cycle and allowed for current gamemode + { + KFGI = KFGameInfo(WorldInfo.Game); + if (KFGI == None) return -1; + + ActiveMapCycle = KFGI.GameMapCycles[KFGI.ActiveMapCycle].Maps; + + foreach class'KFGameViewportClient'.default.TripWireOfficialMaps(Map) + if (ActiveMapCycle.Find(Map) != -1 && KFGI.IsMapAllowedInCycle(Map)) + AviableMaps.AddItem(Map); + + foreach class'KFGameViewportClient'.default.CommunityOfficialMaps(Map) + if (ActiveMapCycle.Find(Map) != -1 && KFGI.IsMapAllowedInCycle(Map)) + AviableMaps.AddItem(Map); + + if (AviableMaps.Length > 0) + return ActiveMapCycle.Find(AviableMaps[Rand(AviableMaps.Length)]); + } + + return -1; +} + +DefaultProperties +{ + ImportantPersonList.Add("0x0110000103143A23") // Spazm* + ImportantPersonList.Add("0x011000010276FBCB") // GenZmeY + ImportantPersonList.Add("0x011000010F661D88") // Janis +} diff --git a/PublicationContent/description.txt b/PublicationContent/description.txt new file mode 100644 index 0000000..16b56fa --- /dev/null +++ b/PublicationContent/description.txt @@ -0,0 +1 @@ +Who are you? What are you doing here? O_o \ No newline at end of file diff --git a/PublicationContent/preview.png b/PublicationContent/preview.png new file mode 100644 index 0000000..2263ae2 Binary files /dev/null and b/PublicationContent/preview.png differ diff --git a/PublicationContent/tags.txt b/PublicationContent/tags.txt new file mode 100644 index 0000000..80d4cf5 --- /dev/null +++ b/PublicationContent/tags.txt @@ -0,0 +1 @@ +Mutators \ No newline at end of file diff --git a/PublicationContent/title.txt b/PublicationContent/title.txt new file mode 100644 index 0000000..ca5c0bd --- /dev/null +++ b/PublicationContent/title.txt @@ -0,0 +1 @@ +MSK-GS Mutator \ No newline at end of file diff --git a/README.md b/README.md index 79d0e9d..3c16ec7 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# KF2-MSK-GS \ No newline at end of file +# KF2-MSK-GS +Mut list: ++ FriendlyHUD (workshop); ++ TAWOD (workshop); ++ DefNextMap (workshop or custom); ++ DLC for group members (custom); ++ No anonymous voting (custom). diff --git a/make.sh b/make.sh new file mode 100644 index 0000000..d9f10e7 --- /dev/null +++ b/make.sh @@ -0,0 +1,220 @@ +#!/bin/bash + +# Requirements: git-bash +# https://git-scm.com/download/win + +set -Eeuo pipefail +trap cleanup SIGINT SIGTERM ERR + +function winpath2unix () # $1: win path +{ + echo "$*" | \ + sed -r 's|^(.):|\\\1|' | \ + sed 's|\\|/|g' +} + +function unixpath2win () # $1: unix path +{ + echo "$*" | \ + sed -r 's|^/(.)|\1:|' | \ + sed 's|/|\\|g' +} + +function reg_readkey () # $1: path, $2: key +{ + winpath2unix $( + reg query "$1" //v "$2" | \ + grep -F "$2" | \ + awk '{ $1=$2=""; print $0 }' ) +} + +function show_help () +{ + echo "$ScriptName" + echo "Usage:" + echo "${ScriptName} OPTION" + echo "Options:" + echo " -c, --compile" + echo " -b, --brew" + echo " -bu, --brew-unpublished" + echo " -u, --upload" + echo " -t, --test" + echo " -h, --help" +} + +function cleanup() +{ + trap - SIGINT SIGTERM ERR + restore_kfeditorconf +} + +function get_latest_multini () +{ + local ApiUrl="https://api.github.com/repos/GenZmeY/multini/releases/latest" + local LatestTag=$(curl --silent "$ApiUrl" | grep -Po '"tag_name": "\K.*?(?=")') + local DownloadUrl="https://github.com/GenZmeY/multini/releases/download/$LatestTag/multini-windows-amd64.exe" + + mkdir -p "$ThirdPartyBin" + curl -LJs "$DownloadUrl" -o "$ThirdPartyBin/multini.exe" +} + +function backup_kfeditorconf () +{ + cp "$KFEditorConf" "$KFEditorConfBackup" +} + +function restore_kfeditorconf () +{ + if [[ -f "$KFEditorConfBackup" ]]; then + mv -f "$KFEditorConfBackup" "$KFEditorConf" + fi +} + +function setup_modpackages () +{ + multini --set "$KFEditorConf" 'ModPackages' 'ModPackages' 'MskGs' + multini --set "$KFEditorConf" 'ModPackages' 'ModPackagesInPath' "$(unixpath2win "$MutSource")" +} + +function compiled () +{ + test -f "$MutStructScript/MskGs.u" +} + +function compile () +{ + if ! command -v multini &> /dev/null; then + get_latest_multini + fi + + backup_kfeditorconf && setup_modpackages + + rm -rf "$MutUnpublish" + mkdir -p \ + "$MutUnpublish" \ + "$MutStructScript" + + CMD //C "$(unixpath2win "$KFEditor")" make -useunpublished & + local PID="$!" + while ps -p "$PID" &> /dev/null + do + if compiled; then + kill "$PID"; break + fi + sleep 2 + done + + restore_kfeditorconf + + if ! compiled; then + echo "Compilation failed" + return 1 + fi +} + +function brew () +{ + echo "brew command is broken. Use --brew-unpublished or brew from WorkshopUploadToolGUI instead of this." + # CMD //C "$(unixpath2win "$KFEditor")" brewcontent -platform=PC ServerExt ServerExtMut -useunpublished +} + +function brew_unpublished () +{ + rm -rf "$MutPublish" + if ! compiled; then + compile + fi + cp -rf "$MutUnpublish" "$MutPublish" +} + +function generate_wsinfo () # $1: package dir +{ + local Description=$(cat "$MutPubContent/description.txt") + local Title=$(cat "$MutPubContent/title.txt") + local Preview=$(unixpath2win "$MutPubContent/preview.png") + local Tags=$(cat "$MutPubContent/tags.txt") + local PackageDir=$(unixpath2win "$1") + echo "\$Description \"$Description\" +\$Title \"$Title\" +\$PreviewFile \"$Preview\" +\$Tags \"$Tags\" +\$MicroTxItem \"false\" +\$PackageDirectory \"$PackageDir\" +" > "$MutWsInfo" +} + +function upload () +{ + PackageDir=$(mktemp -d -u -p "$KFDoc") + cp -rf "$MutPublish"/* "$PackageDir" + generate_wsinfo "$PackageDir" + CMD //C "$(unixpath2win "$KFWorkshop")" "$MutWsInfoName" + rm -rf "$PackageDir" + rm -f "$MutWsInfo" +} + +function create_default_testing_ini () +{ +echo "Map=\"KF-Outpost\" +Game=\"KFGameContent.KFGameInfo_Survival\" +Difficulty=\"0\" +GameLength=\"0\" +Mutators=\"TAWOD.TAWODMut\" +Args=\"\"" > "$MutTestingIni" +} + +function game_test () +{ + if ! [[ -r "$MutTestingIni" ]]; then + create_default_testing_ini + fi + source "$MutTestingIni" + CMD //C "$(unixpath2win "$KFGame")" ${Map}?Difficulty=${Difficulty}?GameLength=${GameLength}?Game=${Game}?Mutator=${Mutators}?${Args} -useunpublished -log +} + +ScriptFullname=$(readlink -e "$0") +ScriptName=$(basename "$0") +ScriptDir=$(dirname "$ScriptFullname") + +SteamPath=$(reg_readkey "HKCU\Software\Valve\Steam" "SteamPath") +DocumentsPath=$(reg_readkey "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" "Personal") + +KFPath="$SteamPath/steamapps/common/killingfloor2" +KFBin="$KFPath/Binaries" +KFEditor="$KFBin/Win64/KFEditor.exe" +KFGame="$KFBin/Win64/KFGame.exe" +KFWorkshop="$KFBin/WorkshopUserTool.exe" + +KFDoc="$DocumentsPath/My Games/KillingFloor2" +KFConfig="$KFDoc/KFGame/Config" + +KFEditorConf="$KFConfig/KFEditor.ini" +KFEditorConfBackup="${KFEditorConf}.backup" + +MutSource="$ScriptDir" +MutPubContent="$MutSource/PublicationContent" +MutUnpublish="$KFDoc/KFGame/Unpublished" +MutPublish="$KFDoc/KFGame/Published" + +MutStructScript="$MutUnpublish/BrewedPC/Script" +MutStructPackages="$MutUnpublish/BrewedPC/Packages" +MutStructLocalization="$MutUnpublish/BrewedPC/Localization" + +MutTestingIni="$MutSource/testing.ini" +MutWsInfoName="wsinfo_serverext.txt" +MutWsInfo="$KFDoc/$MutWsInfoName" + +ThirdPartyBin="$MutSource/3rd-party-bin" + +export PATH="$PATH:$ThirdPartyBin" + +if [[ $# -eq 0 ]]; then show_help; exit 0; fi +case $1 in + -h|--help ) show_help ; ;; + -c|--compile ) compile ; ;; + -b|--brew ) brew ; ;; + -bu|--brew-unpublished ) brew_unpublished ; ;; + -u|--upload ) upload ; ;; + -t|--test ) game_test ; ;; + * ) echo "Command not recognized: $1"; exit 1;; +esac