Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

[English](CHANGELOG_EN.md) | 中文

## v1.8.1

- 新增音效标签 `trigger_on_hurt`,用于角色受伤时自动播放音效
- 新增 `search_found_item_quality_xxx` 系列音效标签,可在搜索完成时根据物品品质触发不同音效(支持 none/white/green/blue/purple/orange/red/q7/q8)
- 改进了事件订阅的清理逻辑,防止内存泄漏

## v1.8.0-fix1

- 修复了因修改加载方式导致的多语言加载失败问题
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

English | [中文](CHANGELOG.md)

## v1.8.1

- Added sound tag `trigger_on_hurt` for automatically playing sounds when a character is hurt
- Added `search_found_item_quality_xxx` tag series so different sounds can play after finishing a search based on the revealed item quality (supports none/white/green/blue/purple/orange/red/q7/q8)
- Improved event subscription cleanup logic to prevent memory leaks

## v1.8.0-fix1

- Fixed issue where multilingual loading failed due to changes in loading method
Expand Down
20 changes: 20 additions & 0 deletions DuckovCustomModel.Core/Data/SoundTags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,35 @@ public static class SoundTags
public const string Surprise = "surprise";
public const string Death = "death";
public const string Idle = "idle";
public const string TriggerOnHurt = "trigger_on_hurt";
public const string TriggerOnDeath = "trigger_on_death";
public const string SearchFoundItemQualityNone = "search_found_item_quality_none";
public const string SearchFoundItemQualityWhite = "search_found_item_quality_white";
public const string SearchFoundItemQualityGreen = "search_found_item_quality_green";
public const string SearchFoundItemQualityBlue = "search_found_item_quality_blue";
public const string SearchFoundItemQualityPurple = "search_found_item_quality_purple";
public const string SearchFoundItemQualityOrange = "search_found_item_quality_orange";
public const string SearchFoundItemQualityRed = "search_found_item_quality_red";
public const string SearchFoundItemQualityQ7 = "search_found_item_quality_q7";
public const string SearchFoundItemQualityQ8 = "search_found_item_quality_q8";

public static IReadOnlyCollection<string> ValidTags =>
[
Normal,
Surprise,
Death,
Idle,
TriggerOnHurt,
TriggerOnDeath,
SearchFoundItemQualityNone,
SearchFoundItemQualityWhite,
SearchFoundItemQualityGreen,
SearchFoundItemQualityBlue,
SearchFoundItemQualityPurple,
SearchFoundItemQualityOrange,
SearchFoundItemQualityRed,
SearchFoundItemQualityQ7,
SearchFoundItemQualityQ8,
];
}
}
14 changes: 2 additions & 12 deletions DuckovCustomModel/Constant.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace DuckovCustomModel
namespace DuckovCustomModel
{
public static class Constant
{
public const string ModID = "DuckovCustomModel";
public const string ModName = "Duckov Custom Model";
public const string ModVersion = "1.8.0-fix1";
public const string ModVersion = "1.8.1";
public const string HarmonyId = "com.ritsukage.DuckovCustomModel";

public static readonly JsonSerializerSettings JsonSettings = new()
{
TypeNameHandling = TypeNameHandling.Auto,
Formatting = Formatting.Indented,
Converters = [new StringEnumConverter()],
};
}
}
78 changes: 78 additions & 0 deletions DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Collections.Generic;
using Duckov;
using Duckov.UI;
using DuckovCustomModel.Core.Data;
using DuckovCustomModel.MonoBehaviours;
using HarmonyLib;
using ItemStatsSystem;

namespace DuckovCustomModel.HarmonyPatches
{
[HarmonyPatch]
internal class ItemDisplayPatches
{
private static readonly IReadOnlyDictionary<DisplayQuality, string> QualitySoundTags =
new Dictionary<DisplayQuality, string>
{
{ DisplayQuality.None, SoundTags.SearchFoundItemQualityNone },
{ DisplayQuality.White, SoundTags.SearchFoundItemQualityWhite },
{ DisplayQuality.Green, SoundTags.SearchFoundItemQualityGreen },
{ DisplayQuality.Blue, SoundTags.SearchFoundItemQualityBlue },
{ DisplayQuality.Purple, SoundTags.SearchFoundItemQualityPurple },
{ DisplayQuality.Orange, SoundTags.SearchFoundItemQualityOrange },
{ DisplayQuality.Red, SoundTags.SearchFoundItemQualityRed },
{ DisplayQuality.Q7, SoundTags.SearchFoundItemQualityQ7 },
{ DisplayQuality.Q8, SoundTags.SearchFoundItemQualityQ8 },
};

private static readonly HashSet<Item> RecordedItems = [];

[HarmonyPatch(typeof(ItemDisplay), nameof(ItemDisplay.Setup))]
[HarmonyPostfix]
// ReSharper disable once InconsistentNaming
private static void ItemDisplay_Setup_Postfix(ItemDisplay __instance, Item target)
{
if (target == null) return;
if (RecordedItems.Contains(target)) return;
if (!target.NeedInspection) return;

RecordedItems.Add(target);
target.onDestroy += OnItemDestroyed;
}

[HarmonyPatch(typeof(ItemDisplay), nameof(ItemDisplay.OnTargetInspectionStateChanged))]
[HarmonyPostfix]
private static void ItemDisplay_OnTargetInspectionStateChanged_Postfix(Item item)
{
if (item == null) return;
if (item.Inspecting || !item.Inspected) return;
if (!RecordedItems.Contains(item)) return;

item.onDestroy -= OnItemDestroyed;
RecordedItems.Remove(item);
Comment thread
BAKAOLC marked this conversation as resolved.

var mainPlayer = CharacterMainControl.Main;
if (mainPlayer == null) return;

var modelHandler = mainPlayer.GetComponent<ModelHandler>();
if (modelHandler == null || !modelHandler.IsInitialized) return;
if (!modelHandler.IsModelAudioEnabled) return;
if (!modelHandler.HasAnySounds()) return;

if (!QualitySoundTags.TryGetValue(item.DisplayQuality, out var soundTag)) return;

var soundPath = modelHandler.GetRandomSoundByTag(soundTag);
if (string.IsNullOrEmpty(soundPath)) return;

AudioManager.PostCustomSFX(soundPath);
}

private static void OnItemDestroyed(Item item)
{
if (item == null) return;
if (!RecordedItems.Contains(item)) return;

RecordedItems.Remove(item);
}
}
}
24 changes: 23 additions & 1 deletion DuckovCustomModel/MonoBehaviours/ModelHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ private void LateUpdate()
}
}

private void OnDestroy()
{
if (CharacterMainControl == null) return;
if (CharacterMainControl.Health == null) return;
CharacterMainControl.Health.OnHurtEvent.RemoveListener(OnHurt);
CharacterMainControl.Health.OnDeadEvent.RemoveListener(OnDeath);
}

public void Initialize(CharacterMainControl characterMainControl, ModelTarget target = ModelTarget.Character)
{
if (IsInitialized) return;
Expand Down Expand Up @@ -199,7 +207,11 @@ public void Initialize(CharacterMainControl characterMainControl, ModelTarget ta
RecordOriginalHeadCollider();
RecordOriginalSoundMaker();

if (CharacterMainControl.Health != null) CharacterMainControl.Health.OnDeadEvent.AddListener(OnDeath);
if (CharacterMainControl.Health != null)
{
CharacterMainControl.Health.OnHurtEvent.AddListener(OnHurt);
CharacterMainControl.Health.OnDeadEvent.AddListener(OnDeath);
Comment thread
BAKAOLC marked this conversation as resolved.
}

ModLogger.Log("ModelHandler initialized successfully.");
IsInitialized = true;
Expand Down Expand Up @@ -761,6 +773,16 @@ private void ForceUpdateHealthBar()
healthBar.RefreshOffset();
}

private void OnHurt(DamageInfo damageInfo)
{
if (!IsModelAudioEnabled) return;

var soundPath = GetRandomSoundByTag(SoundTags.TriggerOnHurt);
if (string.IsNullOrEmpty(soundPath)) return;

AudioManager.PostCustomSFX(soundPath);
}

private void OnDeath(DamageInfo damageInfo)
{
if (!IsModelAudioEnabled) return;
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,9 @@ Animator Controller 可以使用以下参数:
- `"surprise"`:惊讶音效,用于 AI 惊讶状态
- `"death"`:死亡音效,用于 AI 死亡状态
- `"idle"`:待机音效,用于角色自动播放(可通过配置控制哪些角色类型允许自动播放)
- `"trigger_on_hurt"`:受伤触发音效,用于角色受到伤害时自动播放
- `"trigger_on_death"`:死亡触发音效,用于角色死亡时自动播放
- `"search_found_item_quality_xxx"`:搜索完成时发现指定品质物品会触发音效,`xxx` 可为 `none`、`white`、`green`、`blue`、`purple`、`orange`、`red`、`q7`、`q8`
- 可以同时包含多个标签,表示该音效可用于多个场景
- 未指定标签时,默认为 `["normal"]`

Expand All @@ -633,6 +635,7 @@ Animator Controller 可以使用以下参数:
- `"normal"`:AI 普通状态时触发
- `"surprise"`:AI 惊讶状态时触发
- `"death"`:AI 死亡状态时触发
- `"trigger_on_hurt"`:角色受到伤害时自动播放(适用于所有角色类型)
- `"idle"`:启用了自动播放的角色会在随机间隔时间自动播放待机音效
- `"trigger_on_death"`:角色死亡时自动播放(适用于所有角色类型)
- 播放间隔可在 `IdleAudioConfig.json` 中配置
Expand All @@ -642,6 +645,12 @@ Animator Controller 可以使用以下参数:
- 默认情况下,AI 角色和宠物允许自动播放,玩家角色不允许(可通过配置启用)
- 如果指定标签的音效不存在,将使用原版事件(不会回退到其他标签)

#### 搜索发现触发

- 玩家完成物品搜索或检查后(UI 展示品质信息的那一刻)会触发对应品质的音效
- 使用 `search_found_item_quality_xxx` 标签,`xxx` 与 `Tags` 描述相同:`none`、`white`、`green`、`blue`、`purple`、`orange`、`red`、`q7`、`q8`
- 若模型未配置对应品质的音效,则不会播放,保持与原版一致

### 音效文件要求

- 音效文件应放置在模型包文件夹内
Expand Down
11 changes: 11 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@ Sounds can be configured in `ModelInfo` within `bundleinfo.json`:
- `"surprise"`: Surprise sound, used for AI surprise state
- `"death"`: Death sound, used for AI death state
- `"idle"`: Idle sound, used for automatic playback by characters (can be controlled through configuration to determine which character types are allowed to automatically play)
- `"trigger_on_hurt"`: Hurt trigger sound, automatically plays when the character takes damage
- `"trigger_on_death"`: Death trigger sound, automatically plays when the character dies
- `"search_found_item_quality_xxx"`: Plays when a searched item of the specified quality is revealed; `xxx` can be `none`, `white`, `green`, `blue`, `purple`, `orange`, `red`, `q7`, or `q8`
- Can contain multiple tags, indicating the sound can be used in multiple scenarios
- Defaults to `["normal"]` when no tags are specified

Expand All @@ -626,14 +629,22 @@ Sounds can be configured in `ModelInfo` within `bundleinfo.json`:
- `"normal"`: Triggered during AI normal state
- `"surprise"`: Triggered during AI surprise state
- `"death"`: Triggered during AI death state
- `"trigger_on_hurt"`: Automatically plays when the character takes damage (applies to all character types)
- `"idle"`: Characters with automatic playback enabled will automatically play idle sounds at random intervals
- `"trigger_on_death"`: Automatically plays when the character dies (applies to all character types)
- Play interval can be configured in `IdleAudioConfig.json`
- Default interval is 30-45 seconds (random)
- Will not play when the character is dead
- Which character types are allowed to automatically play can be controlled through `EnableIdleAudio` and `AICharacterEnableIdleAudio` configurations
- By default, AI characters and pets are allowed to automatically play, while player characters are not (can be enabled through configuration)
- If a sound with the specified tag doesn't exist, the original game event will be used (no fallback to other tags)

#### Search Discovery Trigger

- When the player finishes searching or inspecting an item and its quality is revealed, the corresponding sound tag is triggered
- Use `search_found_item_quality_xxx`, where `xxx` matches the same quality suffix listed in `Tags`: `none`, `white`, `green`, `blue`, `purple`, `orange`, `red`, `q7`, `q8`
- If no sound is configured for that quality, nothing plays and the vanilla behavior remains unchanged

### Sound File Requirements

- Sound files should be placed inside the model bundle folder
Expand Down