Compare commits

...

10 commits

Author SHA1 Message Date
f0275dccf3 Add comment + simplify syntax 2024-01-02 01:24:29 -05:00
af4ef66db5 Combine multiplayer and replay skin hijacking
Really nice simplification here, I could remove two patches at once and clear out some headaches. Now, the logic for switching skins in multiplayer and in replays is in the same place, which is great and will make it more expandable in the future
2024-01-02 01:22:31 -05:00
ea688c8bec Add option to show skin IDs in cosmetics menu 2023-10-25 00:50:21 -04:00
16559dfe5b Fix for when inCosmeticMenu == true && skinNameToHijack == * 2023-10-25 00:38:56 -04:00
6f7cf91d86 Fix isCosmeticMenu setting 2023-10-25 00:16:13 -04:00
2c43db37b5 Add license 2023-10-24 23:09:02 -04:00
56f56ef938 JSON auto formatting 2023-10-24 23:07:11 -04:00
e424692f58 Fix inAllReplays functionality 2023-10-06 20:54:57 -04:00
41a1e75946 Formatting/cleanup 2023-10-06 20:23:48 -04:00
efd0696145 Fix racing against yourself 2023-10-03 22:29:14 -04:00
14 changed files with 173 additions and 152 deletions

8
.editorconfig Normal file
View file

@ -0,0 +1,8 @@
# https://editorconfig.org/
root = true
[*.cs]
insert_final_newline = true
indent_style = tab
tab_width = 4

View file

@ -19,6 +19,7 @@ namespace CustomCosmeticLoader
public const string PROPERTY_IN_MAIN_MENU = "inMainMenu";
public const string PROPERTY_IN_COSMETIC_MENU = "inCosmeticMenu";
public const string PROPERTY_IN_ALL_REPLAYS = "inAllReplays";
public const string PROPERTY_SHOW_SKIN_IDS = "showSkinIds";
public const string PROPERTY_OVERRIDE_REPLAY_COSMETICS = "overrideReplayCosmetics";
public const string PROPERTY_OTHER_PLAYERS = "otherPlayers";
@ -28,6 +29,7 @@ namespace CustomCosmeticLoader
public static bool inMainMenu = true;
public static bool inCosmeticMenu = false;
public static bool inAllReplays = false;
public static bool showSkinIds = false;
public static bool overrideReplayCosmetics = false;
public static Dictionary<string, string> otherPlayers = new Dictionary<string, string>();
@ -85,6 +87,9 @@ namespace CustomCosmeticLoader
if (data[PROPERTY_IN_ALL_REPLAYS] != null)
inAllReplays = data[PROPERTY_IN_ALL_REPLAYS].AsBool;
if (data[PROPERTY_SHOW_SKIN_IDS] != null)
showSkinIds = data[PROPERTY_SHOW_SKIN_IDS].AsBool;
if (data[PROPERTY_OVERRIDE_REPLAY_COSMETICS] != null)
overrideReplayCosmetics = data[PROPERTY_OVERRIDE_REPLAY_COSMETICS].AsBool;
@ -115,13 +120,14 @@ namespace CustomCosmeticLoader
{
JSONNode node = new JSONClass
{
{ PROPERTY_ENABLED, enabled ? "true" : "false" },
{ PROPERTY_ENABLED, new JSONData(enabled) },
{ PROPERTY_CURRENT_SKIN, currentSkin },
{ PROPERTY_SKIN_NAME_TO_HIJACK, skinNameToHijack },
{ PROPERTY_IN_MAIN_MENU, inMainMenu ? "true" : "false" },
{ PROPERTY_IN_COSMETIC_MENU, inCosmeticMenu ? "true" : "false" },
{ PROPERTY_IN_ALL_REPLAYS, inAllReplays ? "true" : "false" },
{ PROPERTY_OVERRIDE_REPLAY_COSMETICS, overrideReplayCosmetics ? "true" : "false" },
{ PROPERTY_IN_MAIN_MENU, new JSONData(inMainMenu) },
{ PROPERTY_IN_COSMETIC_MENU, new JSONData(inCosmeticMenu) },
{ PROPERTY_IN_ALL_REPLAYS, new JSONData(inAllReplays) },
{ PROPERTY_SHOW_SKIN_IDS, new JSONData(showSkinIds) },
{ PROPERTY_OVERRIDE_REPLAY_COSMETICS, new JSONData(overrideReplayCosmetics) },
};
JSONNode otherPlayersData = new JSONClass();
@ -130,7 +136,7 @@ namespace CustomCosmeticLoader
node.Add(PROPERTY_OTHER_PLAYERS, otherPlayersData);
File.WriteAllText(GetConfigPath(), node.ToString());
File.WriteAllText(GetConfigPath(), JsonHelper.FormatJson(node.ToString(), "\t"));
}
public static void ensureSkinSelected()

View file

@ -1,5 +1,4 @@
using MIU;
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
@ -150,6 +149,20 @@ namespace CustomCosmeticLoader
return "Custom skins in all replays " + (Config.inAllReplays ? "enabled" : "disabled");
}
[ConsoleCommand(description = "Enable/disable showing the underlying skin ID in the cosmetic menu", paramsDescription = "[true/false]", hidden = true)]
public static string cclShowSkinIds(params string[] args)
{
if (args.Length == 0)
return "cclShowSkinIds: " + (Config.showSkinIds ? "true" : "false");
if (args.Length != 1 || (args[0] != "true" && args[0] != "false"))
return "Requires a true or false argument";
Config.showSkinIds = args[0] == "true";
return "Skin IDs in cosmetic menu " + (Config.showSkinIds ? "enabled" : "disabled");
}
[ConsoleCommand(description = "Enable/disable overriding a replay's cosmetics with your cosmetics", paramsDescription = "[true/false]", hidden = true)]
public static string cclOverrideReplayCosmetics(params string[] args)
{

34
JsonHelper.cs Normal file
View file

@ -0,0 +1,34 @@
using System;
using System.Linq;
namespace CustomCosmeticLoader
{
public class JsonHelper
{
/*
* Code taken from: https://stackoverflow.com/a/57100143
*/
public static string FormatJson(string json, string indent = " ")
{
var indentation = 0;
var quoteCount = 0;
var escapeCount = 0;
var result =
from ch in json ?? string.Empty
let escaped = (ch == '\\' ? escapeCount++ : escapeCount > 0 ? escapeCount-- : escapeCount) > 0
let quotes = ch == '"' && !escaped ? quoteCount++ : quoteCount
let unquoted = quotes % 2 == 0
let colon = ch == ':' && unquoted ? ": " : null
let nospace = char.IsWhiteSpace(ch) && unquoted ? string.Empty : null
let lineBreak = ch == ',' && unquoted ? ch + Environment.NewLine + string.Concat(Enumerable.Repeat(indent, indentation)) : null
let openChar = (ch == '{' || ch == '[') && unquoted ? ch + Environment.NewLine + string.Concat(Enumerable.Repeat(indent, ++indentation)) : ch.ToString()
let closeChar = (ch == '}' || ch == ']') && unquoted ? Environment.NewLine + string.Concat(Enumerable.Repeat(indent, --indentation)) + ch : ch.ToString()
select colon ?? nospace ?? lineBreak ?? (
openChar.Length > 1 ? openChar : closeChar
);
return string.Concat(result);
}
}
}

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2023-present Terry Hearst
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,5 +1,4 @@
using HarmonyLib;
using System;
using System.IO;
using System.Reflection;
using UnityEngine;

View file

@ -0,0 +1,29 @@
using HarmonyLib;
using System;
namespace CustomCosmeticLoader.Patches
{
/*
* Override the marble cosmetic to be the hijacked marble if configured to appear in cosmetic menu.
*/
[HarmonyPatch(typeof(CosmeticPanel), nameof(CosmeticPanel.SelectCosmetic), new Type[] { typeof(Cosmetic), typeof(CosmeticType), typeof(bool) })]
internal class CosmeticPanelSelectCosmeticPatch
{
static void Postfix(Cosmetic c, CosmeticType ctype)
{
if (!Config.enabled)
return;
if (ctype == CosmeticType.Skin && Config.inCosmeticMenu)
{
MarbleHolder holder = CosmeticPanel.cosmHolder;
if (Config.skinNameToHijack != "*")
holder.SetMarble(Shared.SkinToHijack);
Shared.ApplyCustomTexture(holder.currentMarble);
}
if (Config.showSkinIds)
CosmeticPanel.instance.CosmTitle.text += " (" + c.Id + ")";
}
}
}

View file

@ -1,10 +1,13 @@
using HarmonyLib;
using MIU;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace CustomCosmeticLoader.Patches
{
/*
* Hijack the marble of a ghost marble if it's in our custom player list, and apply the custom texture
*/
[HarmonyPatch(typeof(GhostRaceMarbleController), nameof(GhostRaceMarbleController.ApplyCosmetics))]
internal class GhostRaceMarbleControllerApplyCosmeticsPatch
{
@ -28,7 +31,13 @@ namespace CustomCosmeticLoader.Patches
if (Config.skinNameToHijack != "*")
mHolder.SetMarble(Shared.SkinToHijack);
Shared.ApplyCustomTexture(mHolder.currentMarble, Config.skins[Config.otherPlayers[replayName]]);
Texture2D skin;
if (replayName == Player.Current.Name)
skin = Config.skins[Config.currentSkin];
else
skin = Config.skins[Config.otherPlayers[replayName]];
Shared.ApplyCustomTexture(mHolder.currentMarble, skin);
}
}
}

View file

@ -1,14 +1,9 @@
using HarmonyLib;
using MIU;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace CustomCosmeticLoader.Patches
{
/*
* Hijack the cosmetic of the marble in single player. This doesn't apply the custom texture, just swaps out the
* marble with the designated marble to hijack.
* Hijack the cosmetic of the marble in single player, then apply the custom skin texture.
*/
[HarmonyPatch(typeof(MarbleController), nameof(MarbleController.ApplyMyCosmetics))]
internal class MarbleControllerApplyMyCosmeticsPatch
@ -29,53 +24,9 @@ namespace CustomCosmeticLoader.Patches
MarbleHolder mHolder = __instance.MHolder;
string actualSkinId = mHolder.CosmeticSet.skin;
mHolder.SetMarble(Shared.SkinToHijack);
Shared.ApplyCustomTexture(mHolder.currentMarble);
// Setting the skin id here allows others in multiplayer/replays to see your normal skin
mHolder.CosmeticSet.skin = actualSkinId;
}
}
/*
* Hijack the cosmetic of the marble when watching Replays. This doesn't apply the custom texture, just swaps out
* the marble with the designated marble to hijack. If overrideReplayCosmetics is enabled, we swap out all cosmetics
* to match our current configuration to view the replay that way.
*/
[HarmonyPatch(typeof(MarbleController), nameof(MarbleController.ApplyCosmetics))]
internal class MarbleControllerApplyCosmeticsPatch
{
static void Prefix(MarbleController __instance, ref ReplayCosmetics cosmetics)
{
// Actually, we can let this one run even if the custom skins are disabled
//if (!Config.enabled)
// return;
if (Config.overrideReplayCosmetics)
{
cosmetics.Skin = CosmeticManager.MySkin.Id;
cosmetics.Trail = CosmeticManager.MyTrail.Id;
cosmetics.Respawn = CosmeticManager.MyRespawn.Id;
cosmetics.Hat = CosmeticManager.MyHat.Id;
cosmetics.Blast = CosmeticManager.MyBlast.Id;
}
if (!Config.enabled || Config.skinNameToHijack == "*")
return;
string replayName = null;
GamePlayManager.Get().GetCurrentReplays(delegate (List<Replay> replays)
{
if (replays.Count > 0)
replayName = replays[0].Player;
});
Shared.Log("MarbleControllerApplyCosmeticsPatch Replay name: " + replayName);
if (Config.otherPlayers.ContainsKey(replayName) || replayName == Player.Current.Name)
cosmetics.Skin = Config.skinNameToHijack;
// HACK - call this early so that SetMarble is aware of it
// Normally it would be called right AFTER ApplyCosmetics
__instance.AddMode(MarbleController.ReplayMode);
}
}
}

View file

@ -1,44 +1,39 @@
using HarmonyLib;
using MIU;
using Parse.Common.Internal;
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace CustomCosmeticLoader.Patches
{
/*
* Private fields/values for MarbleHolder to be used in the patches
*/
internal static class MarbleHolderValues
{
private static FieldInfo _mbcField = typeof(MarbleHolder).GetField("mbc", BindingFlags.NonPublic | BindingFlags.Instance);
public static FieldInfo MbcField
{
get
{
return _mbcField;
}
}
internal static FieldInfo mbcField = typeof(MarbleHolder).GetField("mbc", BindingFlags.NonPublic | BindingFlags.Instance);
}
/*
* Applies the custom skin texture to the marble in both singleplayer and multiplayer, if the cosmetic chosen is the
* one we're hijacking. In multiplayer, do some extra checks to only apply the custom texture to our marble
* In multiplayer and Replays, hijack the current marble and set it to be the marble we're hijacking, then apply the skin
*/
[HarmonyPatch(typeof(MarbleHolder), nameof(MarbleHolder.SetMarble))]
internal class MarbleHolderSetMarblePatch
[HarmonyPatch(typeof(MarbleHolder), nameof(MarbleHolder.CheckSet))]
internal class MarbleHolderCheckSetPatch
{
static void Postfix(MarbleHolder __instance, Cosmetic marbleObject)
static void Postfix(MarbleHolder __instance, Cosmetic.Set cs)
{
if (!Config.enabled)
return;
if (marbleObject.Id != Config.skinNameToHijack && Config.skinNameToHijack != "*")
if (!NetworkManager.IsMultiplayer && !MarbleManager.Replaying)
return;
if (!Config.inCosmeticMenu && __instance == CosmeticPanel.cosmHolder)
// Don't hijack the soccer ball or zombie skins
if (cs.skin == "SoccerBall_V4" || cs.skin == "Zombie")
return;
MarbleController controller = MarbleHolderValues.MbcField.GetValue(__instance) as MarbleController;
MarbleController controller = MarbleHolderValues.mbcField.GetValue(__instance) as MarbleController;
if (controller == null)
return;
@ -52,78 +47,39 @@ namespace CustomCosmeticLoader.Patches
}
else
{
if (!Config.otherPlayers.ContainsKey(controller.nickname))
return;
skin = Config.skins[Config.otherPlayers[controller.nickname]];
if (Config.otherPlayers.ContainsKey(controller.nickname))
skin = Config.skins[Config.otherPlayers[controller.nickname]];
}
}
else
}
else if (MarbleManager.Replaying) // Should always be true
{
if (MarbleManager.Replaying)
string replayName = null;
GamePlayManager.Get().GetCurrentReplays(delegate (List<Replay> replays)
{
string replayName = null;
if (replays.Count > 0)
replayName = replays[0].Player;
});
GamePlayManager.Get().GetCurrentReplays(delegate (List<Replay> replays)
{
if (replays.Count > 0)
replayName = replays[0].Player;
});
Shared.Log("MarbleHolderSetMarblePatch Replay name: " + replayName);
if (replayName == Player.Current.Name)
{
skin = Config.skins[Config.currentSkin];
}
else
{
if (Config.otherPlayers.ContainsKey(replayName))
skin = Config.skins[Config.otherPlayers[replayName]];
}
}
else
Shared.Log("MarbleHolderCheckSetPatch Replay name: " + replayName);
if (replayName == Player.Current.Name)
{
skin = Config.skins[Config.currentSkin];
}
else
{
if (Config.otherPlayers.ContainsKey(replayName))
skin = Config.skins[Config.otherPlayers[replayName]];
}
}
if (skin != null)
{
Shared.ApplyCustomTexture(__instance.currentMarble, skin);
}
}
}
/*
* In multiplayer, hijack the current marble and set it to be the marble we're hijacking. Later, the actual texture
* will be replaced by the patch above
*/
[HarmonyPatch(typeof(MarbleHolder), nameof(MarbleHolder.CheckSet))]
internal class MarbleHolderCheckSetPatch
{
static void Postfix(MarbleHolder __instance, Cosmetic.Set cs)
{
if (!Config.enabled)
return;
if (!NetworkManager.IsMultiplayer)
return;
MarbleController controller = MarbleHolderValues.MbcField.GetValue(__instance) as MarbleController;
if (controller == null)
return;
if (!controller.isMyClientMarble())
{
if (!Config.otherPlayers.ContainsKey(controller.nickname))
return;
}
// Don't hijack the soccer ball or zombie skins
if (cs.skin == "SoccerBall_V4" || cs.skin == "Zombie")
if (skin == null)
return;
__instance.SetMarble(Shared.SkinToHijack);
Shared.ApplyCustomTexture(__instance.currentMarble, skin);
// Reset this to what it should be incase it's used later
__instance.CosmeticSet.skin = cs.skin;
}
}

View file

@ -19,9 +19,9 @@ namespace CustomCosmeticLoader.Patches
return true;
Cosmetic skin = Config.skinNameToHijack == "*" ?
(CosmeticManager.MySkin == null ? CosmeticManager.Skins[0] : CosmeticManager.MySkin) :
(CosmeticManager.MySkin ?? CosmeticManager.Skins[0]) :
Shared.SkinToHijack;
Cosmetic hat = CosmeticManager.MyHat == null ? CosmeticManager.Hats[0] : CosmeticManager.MyHat;
Cosmetic hat = CosmeticManager.MyHat ?? CosmeticManager.Hats[0];
__instance.SetCosmetic(skin, hat);
Shared.ApplyCustomTexture(__instance.cosmeticDisplay.gameObject);

View file

@ -1,5 +1,4 @@
using HarmonyLib;
using System;
using UnityEngine;
namespace CustomCosmeticLoader.Patches

View file

@ -1,6 +1,4 @@
using System;
using System.Runtime.Remoting.Messaging;
using UnityEngine;
using UnityEngine;
namespace CustomCosmeticLoader
{

View file

@ -1,10 +1,10 @@
<Project>
<PropertyGroup>
<!--remove this property-->
<UserPropertiesNotSetUp>True</UserPropertiesNotSetUp>
<!--Insert path to MIUU game directory here-->
<GameDir></GameDir>
<!--Insert path to where your mod should build to (it will build to "out" directory by default)-->
<ModOutputDir>$(GameDir)\Mods\CustomCosmeticLoader</ModOutputDir>
</PropertyGroup>
<PropertyGroup>
<!-- Remove this property -->
<UserPropertiesNotSetUp>True</UserPropertiesNotSetUp>
<!-- Insert path to MIUU game directory here -->
<GameDir></GameDir>
<!-- Change path if you want to change to where the mod should build -->
<ModOutputDir>$(GameDir)\Mods\CustomCosmeticLoader</ModOutputDir>
</PropertyGroup>
</Project>