Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 47 additions & 29 deletions Code/Config/CharacterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,50 @@ public enum MaskModes {
Red = 0, Green = 1, Blue = 2, Grayscale = 3
}

public CharacterConfig() {
}

public static CharacterConfig For(Image target) {
string rootPath = getAnimationRootPath(target);
if (!_Instance.TryGetValue(target, out CharacterConfig config) || config.SourcePath != rootPath) {

ModAsset asset = GetAssetOnSprite<AssetTypeYaml>(target, _ConfigName);
config = AssetIntoConfig<CharacterConfig>(asset) ?? new();
config.Source = asset;
config.attached = target;
config.SourcePath = rootPath;

if (target is PlayerSprite playerSprite) {
config.ModeInitialize(playerSprite.Mode);
}
// SilhouetteMode and TintGrayscaleWithHair are the almost same and conflicting. only the latter work when
if (config.TintMaskWithHair) {
config.SilhouetteMode = false;

} else if (config.SilhouetteMode == true) {
config.LowStaminaFlashHair = true;
}
config.ParticleModifierInit();

_Instance.AddOrUpdate(target, config);
}
public CharacterConfig() {
}

internal static CharacterConfig BindCharacterConfig(Image target) {
ModAsset asset = GetAssetOnSprite<AssetTypeYaml>(target, _ConfigName);
CharacterConfig config = AssetIntoConfig<CharacterConfig>(asset) ?? new();

config.Source = asset;
config.attached = target;
config.SourcePath = getAnimationRootPath(target);
config.LastCheckedTexture = target.Texture;

if (target is PlayerSprite playerSprite) {
config.ModeInitialize(playerSprite.Mode);
}
// SilhouetteMode and TintGrayscaleWithHair are almost the same and conflict.
// Only the latter should win when both are enabled.
config.RefreshConflict();
config.ParticleModifierInit();

// CharacterConfig contains runtime state, so every Image gets its own instance even
// when several sprites resolve to the same YAML asset.
_Instance.AddOrUpdate(target, config);
return config;
}

internal static void InvalidateAll() {
_Instance = new();
}

public static CharacterConfig For(Image target) {
if (!_Instance.TryGetValue(target, out CharacterConfig config)) {
config = BindCharacterConfig(target);
} else if (!ReferenceEquals(config.LastCheckedTexture, target.Texture)
&& !(target is Sprite boundSprite && SpriteDataCache.TryGetValue(boundSprite, out _))) {
// Ordinary Images can have their Texture replaced directly. A different texture
// only requires a new config when it also resolves to a different resource root.
string currentRootPath = getAnimationRootPath(target);
if (config.SourcePath == currentRootPath) {
config.LastCheckedTexture = target.Texture;
} else {
config = BindCharacterConfig(target);
}
}
if (target.Entity != config.lastEntity) {
config.lastEntity = target.Entity;
// Avoid multiple EntityTweaks works, make sure this target is the first of its entity.
Expand Down Expand Up @@ -84,8 +101,9 @@ public void RefreshConflict() {
#region Values
private Image attached;
private Entity lastEntity;
private ModAsset Source;
private string SourcePath;
private ModAsset Source;
private string SourcePath;
private MTexture LastCheckedTexture;

/// <summary> uses when TintMaskWithHair is true </summary>
internal Color effect_hairColor = Color.White;
Expand Down
50 changes: 25 additions & 25 deletions Code/Content/ParticleModify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,13 @@ private static void ilEntityList_Update(ILContext il) {
cursor.Emit(OpCodes.Ldloc_1);
cursor.EmitDelegate(Redirect);
}
if (cursor.TryGotoNext(MoveType.Before, instr => instr.MatchLeaveS(out _))) {
cursor.Emit(OpCodes.Ldnull);
cursor.Emit(OpCodes.Stsfld, typeof(ParticleModify).GetField("Tracked", BindingFlags.NonPublic | BindingFlags.Static));
}
void Redirect(Entity entity) {
switch (entity) {
case NPC05_Badeline npc05:
Tracked = npc05.shadow; // BadelineOldsite.P_Vanish
return;
case CS10_HubIntro cs10:
Tracked = cs10.booster; // Booster.Appear
return;
default:
Tracked = entity;
return;
}
};
if (cursor.TryGotoNext(MoveType.Before, instr => instr.MatchLeaveS(out _))) {
cursor.Emit(OpCodes.Ldnull);
cursor.Emit(OpCodes.Stsfld, typeof(ParticleModify).GetField("Tracked", BindingFlags.NonPublic | BindingFlags.Static));
}
void Redirect(Entity entity) {
Tracked = entity;
};
}
private static bool PlayerCollider_Check(On.Celeste.PlayerCollider.orig_Check orig, PlayerCollider self, Player player) {
OverrideTracked = self.Entity;
Expand Down Expand Up @@ -168,13 +158,23 @@ private static void onParticleSystem_Emit_PtclV2FC(On.Monocle.ParticleSystem.ori



private static bool ParticleReplace(ParticleType ptcl, out ParticleType ptcl2) {
// if ((OverrideTracked ?? Tracked) != null &&
// (ptcl == NPC01_Theo.P_YOLO)) {
// Log($"{Tracked} : {OverrideTracked} : {getAnimationRootPath((OverrideTracked ?? Tracked).Get<Sprite>())}");
// }
return ParticleReplace(ptcl, OverrideTracked ?? Tracked, out ptcl2);
}
private static bool ParticleReplace(ParticleType ptcl, out ParticleType ptcl2) {
// if ((OverrideTracked ?? Tracked) != null &&
// (ptcl == NPC01_Theo.P_YOLO)) {
// Log($"{Tracked} : {OverrideTracked} : {getAnimationRootPath((OverrideTracked ?? Tracked).Get<Sprite>())}");
// }
Entity tracked = Tracked;
if (OverrideTracked is null) {
// These exceptions only affect particle replacement. Resolve them here instead of
// doing two type checks for every entity in every EntityList.Update.
tracked = tracked switch {
NPC05_Badeline npc05 => npc05.shadow, // BadelineOldsite.P_Vanish
CS10_HubIntro cs10 => cs10.booster, // Booster.Appear
_ => tracked
};
}
return ParticleReplace(ptcl, OverrideTracked ?? tracked, out ptcl2);
}

public static bool ParticleReplace(ParticleType ptcl, Entity entity, out ParticleType ptcl2) {
if (entity?.Get<Sprite>() is Sprite sprite) {
Expand All @@ -189,4 +189,4 @@ public static bool ParticleReplace(ParticleType ptcl, Entity entity, out Particl
return false;
}
}
}
}
100 changes: 60 additions & 40 deletions Code/Content/SkinsSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,25 @@ public static void LoadContent(bool firstLoad) {
#endregion

#region Config Initialize
private static void EverestContentUpdateHook(ModAsset oldAsset, ModAsset newAsset) {
if (newAsset != null) {
if (newAsset.PathVirtual.StartsWith("SkinModHelperConfig")) {
ConfigInsert(newAsset);
Logger.Log(LogLevel.Warn, "SkinModHelper", $"If the new skins's content does not load, please enter the save slot menu to refresh it");
}
}
}
private static void EverestContentUpdateHook(ModAsset oldAsset, ModAsset newAsset) {
if (newAsset != null) {
if (newAsset.PathVirtual.StartsWith("SkinModHelperConfig")) {
ConfigInsert(newAsset);
Logger.Log(LogLevel.Warn, "SkinModHelper", $"If the new skins's content does not load, please enter the save slot menu to refresh it");
}
}
if (IsCharacterConfigAsset(oldAsset) || IsCharacterConfigAsset(newAsset)) {
CharacterConfig.InvalidateAll();
}
}

private static bool IsCharacterConfigAsset(ModAsset asset) {
string path = asset?.PathVirtual;
return path != null
&& (path.EndsWith(CharacterConfig._ConfigName, StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(CharacterConfig._ConfigName + ".yaml", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(CharacterConfig._ConfigName + ".yml", StringComparison.OrdinalIgnoreCase));
}
public static void ReloadSettings() {
Logger.Log(LogLevel.Info, "SkinModHelper", $"Skins loading... Settings Initializing...");

Expand Down Expand Up @@ -273,23 +284,17 @@ private static Sprite SpriteBankCreateHook(On.Monocle.SpriteBank.orig_Create ori
string newId = skinbank.GetCurrentSkin(id);
if (self.Has(newId))
id = newId;
}
Sprite sprite = orig(self, id);
if (sprite != null) {
var data = self.SpriteData[id];
List<object> objs = new() { data.Atlas };
for (int i = 0; i < data.Sources.Count; i++) {
SpriteDataSource source = data.Sources[i];
objs.Add(source.OverridePath);
objs.Add(source.Path);
}
SpriteDataCache.AddOrUpdate(sprite, objs);
}
return sprite;
}
private static Sprite SpriteBankCreateOnHook(On.Monocle.SpriteBank.orig_CreateOn orig, SpriteBank self, Sprite sprite, string id) {
if (sprite.Entity is OuiFileSelectSlot && SaveFilePortraits)
return orig(self, sprite, id);
}
Sprite sprite = orig(self, id);
UpdateSpriteBinding(self, sprite, id);
return sprite;
}
private static Sprite SpriteBankCreateOnHook(On.Monocle.SpriteBank.orig_CreateOn orig, SpriteBank self, Sprite sprite, string id) {
if (sprite.Entity is OuiFileSelectSlot && SaveFilePortraits) {
Sprite portraitSprite = orig(self, sprite, id);
UpdateSpriteBinding(self, portraitSprite, id);
return portraitSprite;
}

if (RespriteBankModule.SearchInstance(self, out var skinbank)) {
string newId = skinbank.GetCurrentSkin(id);
Expand All @@ -307,16 +312,28 @@ private static Sprite SpriteBankCreateOnHook(On.Monocle.SpriteBank.orig_CreateOn
OnceLog(LogLevel.Warn, $"PlayerSprite used '{id}' but that from the custom SpriteBank/Xml... Cannot CreateFramesMetadata and fill it with the possible animations");
}
}
var data = self.SpriteData[id];
List<object> objs = new() { data.Atlas };
for (int i = 0; i < data.Sources.Count; i++) {
SpriteDataSource source = data.Sources[i];
objs.Add(source.OverridePath);
objs.Add(source.Path);
}
SpriteDataCache.AddOrUpdate(sprite, objs);
return orig(self, sprite, id);
}
Sprite result = orig(self, sprite, id);
UpdateSpriteBinding(self, result, id);
return result;
}

private static void UpdateSpriteBinding(SpriteBank bank, Sprite sprite, string id) {
if (sprite == null) {
return;
}

SpriteData data = bank.SpriteData[id];
List<object> objs = new() { data.Atlas };
for (int i = 0; i < data.Sources.Count; i++) {
SpriteDataSource source = data.Sources[i];
objs.Add(source.OverridePath);
objs.Add(source.Path);
}
SpriteDataCache.AddOrUpdate(sprite, objs);
// CreateOn may repopulate the same Sprite from a different source. Always replace the
// per-Sprite config after the final SpriteData has been established.
CharacterConfig.BindCharacterConfig(sprite);
}
#endregion

#region RespriteBank Reload
Expand Down Expand Up @@ -452,10 +469,13 @@ private static void _RefreshSkins(bool Xmls_refresh, bool inGame) {
FailedXml_record.Clear();
RespriteBank_Reload();

build_warning = false;
Logger.SetLogLevel("Atlas", logLevel);
}
if (DelayRefreshForPlayer && _Player != null) {
build_warning = false;
Logger.SetLogLevel("Atlas", logLevel);
}
// A refresh can replace CharacterConfig.yaml even when the selected SpriteBank id and
// source paths stay the same. Explicitly invalidate all live bindings.
CharacterConfig.InvalidateAll();
if (DelayRefreshForPlayer && _Player != null) {
Player_Skinid_verify = -1;
PlayerSkinSystem.RefreshPlayerSpriteMode();
} else {
Expand Down Expand Up @@ -796,4 +816,4 @@ public static void OnceLog(LogLevel logLevel, string log) {
private static HashSet<(LogLevel, string)> onceLog = new();
#endregion
}
}
}