当前位置: 首页 > news >正文

简易星露谷模组二次开发之旅:捐赠追踪、颜色优化与动物状态警告

一、引言
作为《星露谷物语》的资深玩家,我一直很享受在鹈鹕镇打理农场、与村民建立友谊的悠闲时光。而在众多模组中,Lookup Anything 无疑是我最离不开的工具之一——按下 F1 就能查看任何东西的详细信息,简直像拥有了游戏内的百科全书。

但玩得越久,我越觉得有些信息如果能更直观地呈现就好了:这件文物我捐过博物馆了吗?今天抚摸过我的动物了吗?村民是否会喜欢这个礼物??这些问题的答案虽然都能查到,但总要花点时间去辨认。

于是,一个念头冒了出来:为什么不自己动手改一改呢?

作为一个完全没接触过星露谷模组开发的纯新小白。面对陌生的 C# 项目、复杂的 Harmony 补丁、还有各种编译错误,这一路走得并不轻松。从配置开发环境到理解项目结构,从反复编译失败到最终成功添加功能,每一步都伴随着困惑和坚持。

好在,最终我还是实现了三个想要的功能:

回头看,这段经历不仅让我得到了更顺手的工具,更让我深入了解了模组的工作原理,也体会到了开源社区的魅力。在这篇博客里,我想把从零开始的整个开发过程记录下来,包括遇到的问题、找到的解决方案,以及最终实现的代码。希望能给同样有兴趣的玩家一些启发和帮助。

如果你也想过“这个模组如果能加点功能就好了”,那就跟我一起开始这段二次开发之旅吧!^ - ^

二、在开始前你或许需要知道:

开发环境:

  1. 操作系统
    Windows 10/11

  2. 核心开发工具:

Visual Studio Code

扩展:

C#

.NET SDK 6.0(编译 C# 模组的必备运行时)

Git(用于克隆 GitHub 仓库,也可直接下载 ZIP)

  1. 游戏与模组基础

Stardew Valley 1.6.14(当前最新版)

SMAPI 4.5.1(星露谷物语模组加载器,用于运行和调试模组)

  1. 源代码与依赖

Lookup Anything 源代码

NuGet 包:

Pathoschild.Stardew.ModBuildConfig(自动配置游戏引用路径)

Pathoschild.Stardew.ModTranslationClassBuilder(用于多语言支持)

  1. 辅助工具

GitHub(代码托管平台,获取开源模组源码)

Nexus Mods(模组发布平台,用于参考和下载原版模组)

  1. 调试与测试

SMAPI 控制台(实时查看模组加载日志和错误信息)

游戏内测试(按 F1 触发查询,验证新增功能)

7.更多教程与帮助
【星露谷模组开发教程#1 配置开发环境】 https://www.bilibili.com/video/BV1hr421K79d/?share_source=copy_web&vd_source=df43935c5ba91d8069b7138b97fac4a1

三、功能重构思路
在动手修改之前,我花了不少时间理解 Lookup Anything 的代码结构。这个模组的设计非常清晰:每个可查询的实体(物品、动物、村民等)都有一个对应的 Subject 类,负责收集并返回显示信息;而信息的渲染则由各种 Field 类完成。我的三个功能正好分别涉及这两种类型的修改。

image

四、功能实现

  • 博物馆捐赠追踪:在物品查询中添加字段
if (this.Target is SObject museumObj){bool isDonatable = museumObj.Type == "Artifact" || museumObj.Category == SObject.GemCategory || museumObj.Category == SObject.mineralsCategory;if (isDonatable){bool donated = false;if (museumObj.Type == "Artifact")donated = Game1.player.archaeologyFound.TryGetValue(museumObj.ItemId, out int[]? data) && data != null && data[0] == 1;else // 矿物或宝石donated = Game1.player.mineralsFound.ContainsKey(museumObj.ItemId);string status = donated ? "✓ 已捐赠" : "✗ 未捐赠";yield return new GenericField("博物馆捐赠", status);}}

我们可以看到在模组修改之前,矿石没有显示是否被捐赠这一栏:

屏幕截图 2026-03-10 001638

而在修改之后则会多显示一栏:

屏幕截图 2026-03-10 002401

这样的效果是:无论捐赠与否,文字都是默认的黑色,在密密麻麻的信息中不太醒目。为了让我们能一眼就能看出哪些物品已经捐过、哪些还需要捐,我决定给状态文本加上颜色——已捐赠用绿色,未捐赠用红色。

而做这部分的修改需要我们理解Lookup Anything的富文本支持。
查阅模组代码发现,GenericField 的构造函数其实有两个重载:一个接受 string,另一个接受 IFormattedText[]。后者可以让我们传递带格式的文本片段,包括颜色、加粗等。
IFormattedText 是一个接口,而项目中有一个现成的实现类 FormattedText,它的构造函数可以接受文本和颜色(Microsoft.Xna.Framework.Color)。

点击查看代码
 public GenericField(string label, string? value, bool? hasValue = null){this.Label = label;this.Value = this.FormatValue(value);this.HasValue = hasValue ?? this.Value?.Any() == true;}/// <summary>Construct an instance.</summary>/// <param name="label">A short field label.</param>/// <param name="value">The field value.</param>/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>public GenericField(string label, IFormattedText value, bool? hasValue = null): this(label, [value], hasValue) { }/// <summary>Construct an instance.</summary>/// <param name="label">A short field label.</param>/// <param name="value">The field value.</param>/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>public GenericField(string label, IEnumerable<IFormattedText> value, bool? hasValue = null){this.Label = label;this.Value = value.ToArray();this.HasValue = hasValue ?? this.Value?.Any() == true;}

于是我将原来的代码进行了一点调整:

string text = donated ? "✓ 已捐赠" : "✗ 未捐赠";
Color color = donated ? Color.Green : Color.Red;
var formattedText = new FormattedText(text, color);
yield return new GenericField("博物馆捐赠", new IFormattedText[] { formattedText });

因为用到了 Microsoft.Xna.Framework.Color,需要在文件顶部添加:
using Microsoft.Xna.Framework;
好在 ItemSubject.cs 中已经引用了这个命名空间(因为其他地方也用到了颜色),所以不需要额外添加。

再次运行,我们可以看到:

屏幕截图 2026-03-10 003815
捐赠过的文物可以显示绿色提示啦!(苯博主现在暂时没有持有但未捐赠的文物所以这里不予展示(。ì _ í。))
(下面是完整的修改后的源代码,可以直接复制粘贴)

点击查看代码
if (this.Target is SObject museumObj){bool isDonatable = museumObj.Type == "Artifact" || museumObj.Category == SObject.GemCategory || museumObj.Category == SObject.mineralsCategory;if (isDonatable){bool donated = false;if (museumObj.Type == "Artifact")donated = Game1.player.archaeologyFound.TryGetValue(museumObj.ItemId, out int[]? data) && data != null && data[0] == 1;else // 矿物或宝石donated = Game1.player.mineralsFound.ContainsKey(museumObj.ItemId);string text = donated ? "✓ 已捐赠" : "✗ 未捐赠";Color color = donated ? Color.Green : Color.Red;var formattedText = new FormattedText(text, color);yield return new GenericField("博物馆捐赠", new IFormattedText[] { formattedText });}}

同理,有了第一个功能的运行经验,我们可以做其他小小的调整和改动:

  • 动物健康警告:添加红色高亮提示
var warnings = new List<string>();if (animal.fullness.Value < 200)warnings.Add("未喂食");if (animal.happiness.Value < 150)warnings.Add("心情差");if (!animal.wasPet.Value)warnings.Add("未抚摸");if (warnings.Count > 0){string warningText = string.Join(",", warnings);var formattedWarning = new FormattedText(warningText, Color.Red);yield return new GenericField("⚠ 健康警告", new IFormattedText[] { formattedWarning });}

运行后效果如下:

屏幕截图 2026-03-10 011016

  • 村民送礼颜色优化:让礼物列表更直观

在构造函数中,将传入的 showTaste 参数赋值给该字段。
添加一个颜色映射方法 GetColorForTaste,根据口味返回对应的 Color 值。

修改 GetText 方法:原来它根据物品是否在背包中分别赋予不同颜色,现在改为统一使用 baseColor(由口味决定),同时保留“未揭示”和“未拥有”的灰色汇总文本。

在CharacterGiftTastesField.cs 文件中,类的顶部添加:
private readonly GiftTaste ShowTaste;
然后在构造函数中赋值:

  public CharacterGiftTastesField(string label, IDictionary<GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste showTaste, bool showUnknown, bool highlightUnrevealed, bool onlyOwned, IDictionary<string, bool> ownedItemsCache): base(label){this.ShowTaste = showTaste; // 新增的赋值ItemRecord[] allItems = this.GetGiftTasteRecords(giftTastes, showTaste, ownedItemsCache);this.TotalItems = allItems.Length;this.Value = this.GetText(allItems, showUnknown, highlightUnrevealed, onlyOwned).ToArray();this.HasValue = this.Value.Length > 0;}

最后再修改GetText方法:

private IEnumerable<IFormattedText> GetText(ItemRecord[] items, bool showUnknown, bool highlightUnrevealed, bool onlyOwned)
{if (!items.Any())yield break;int unrevealed = 0;int unowned = 0;Color baseColor = this.GetColorForTaste(this.ShowTaste);for (int i = 0, last = items.Length - 1; i <= last; i++){var entry = items[i];if (!showUnknown && !entry.IsRevealed){unrevealed++;continue;}if (onlyOwned && !entry.IsOwned){unowned++;continue;}string text = i != last? entry.Item.DisplayName + I18n.Generic_ListSeparator(): entry.Item.DisplayName;bool bold = highlightUnrevealed && !entry.IsRevealed;yield return new FormattedText(text, baseColor, bold);}if (unrevealed > 0)yield return new FormattedText(I18n.Npc_UndiscoveredGiftTaste(count: unrevealed), Color.Gray);if (unowned > 0)yield return new FormattedText(I18n.Npc_UnownedGiftTaste(count: unowned), Color.Gray);
}

效果如下:

屏幕截图 2026-03-10 012403

我们可以点开显示更多:

屏幕截图 2026-03-10 013851

可能遇到的问题:
最初修改后,发现讨厌和厌恶的列表根本不出现,后来意识到需要在配置文件中启用它们。打开 Mods/LookupAnything/config.json,将 ShowGiftTastes.Disliked 和 Hated 设为 true 即可。
颜色效果在游戏中非常明显,查询潘姆等村民时,一眼就能看出该送什么、不该送什么。

屏幕截图 2026-03-10 111814

屏幕截图 2026-03-10 111857

不过我没想到物品内容会有这么多......这部分的显示我计划再精简一下,今天就不多赘述了。

(以下是CharacterGiftTastesField.cs修改后的完整源代码)

点击查看代码
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Models;
using StardewValley;namespace Pathoschild.Stardew.LookupAnything.Framework.Fields;/// <summary>A metadata field which shows which items an NPC likes receiving.</summary>
internal class CharacterGiftTastesField : GenericField
{private readonly GiftTaste ShowTaste;/*********** Accessors*********//// <summary>The total number of items shown (including the sum of grouped entries like "11 unrevealed tastes").</summary>public int TotalItems { get; }/*********** Public methods*********//// <summary>Construct an instance.</summary>/// <param name="label">A short field label.</param>/// <param name="giftTastes">The items by how much this NPC likes receiving them.</param>/// <param name="showTaste">The gift taste to show.</param>/// <param name="showUnknown">Whether to show gift tastes the player hasn't discovered yet.</param>/// <param name="highlightUnrevealed">Whether to highlight items which haven't been revealed in the NPC profile yet.</param>/// <param name="onlyOwned">Whether to only show gift tastes for items which the player owns somewhere in the world.</param>/// <param name="ownedItemsCache">A lookup cache for owned items, as created by <see cref="GetOwnedItemsCache"/>.</param>public CharacterGiftTastesField(string label, IDictionary<GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste showTaste, bool showUnknown, bool highlightUnrevealed, bool onlyOwned, IDictionary<string, bool> ownedItemsCache): base(label){this.ShowTaste = showTaste; // 新增的赋值ItemRecord[] allItems = this.GetGiftTasteRecords(giftTastes, showTaste, ownedItemsCache);this.TotalItems = allItems.Length;this.Value = this.GetText(allItems, showUnknown, highlightUnrevealed, onlyOwned).ToArray();this.HasValue = this.Value.Length > 0;}/// <summary>Get a lookup cache for owned items indexed by <see cref="Item.QualifiedItemId"/>.</summary>/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>public static IDictionary<string, bool> GetOwnedItemsCache(GameHelper gameHelper){return gameHelper.GetAllOwnedItems().GroupBy(entry => entry.Item.QualifiedItemId).ToDictionary(group => group.Key, group => group.Any(p => p.IsInInventory));}/*********** Private methods*********//// <summary>Get the items that can be listed for the current gift taste, ignoring filter options.</summary>/// <param name="giftTastes">The items by how much this NPC likes receiving them.</param>/// <param name="showTaste">The gift taste to show.</param>/// <param name="ownedItemsCache">A lookup cache for owned items, as created by <see cref="GetOwnedItemsCache"/>.</param>private ItemRecord[] GetGiftTasteRecords(IDictionary<GiftTaste, GiftTasteModel[]> giftTastes, GiftTaste showTaste, IDictionary<string, bool> ownedItemsCache){if (!giftTastes.TryGetValue(showTaste, out GiftTasteModel[]? entries))return [];// get datareturn(from entry in entrieslet item = entry.Itemlet ownership = ownedItemsCache.TryGetValue(item.QualifiedItemId, out bool rawVal) ? rawVal : null as bool? // true = in inventory, false = owned elsewhere, null = none foundlet isOwned = ownership is not nulllet inInventory = ownership is trueorderby inInventory descending, isOwned descending, item.DisplayNameselect new ItemRecord(item, inInventory, isOwned, entry.IsRevealed)).ToArray();}/// <summary>Get the text to display.</summary>/// <param name="items">The items that can be listed for the current gift taste, ignoring filter options.</param>/// <param name="showUnknown">Whether to show gift tastes the player hasn't discovered yet.</param>/// <param name="highlightUnrevealed">Whether to highlight items which haven't been revealed in the NPC profile yet.</param>/// <param name="onlyOwned">Whether to only show gift tastes for items which the player owns somewhere in the world.</param>private Color GetColorForTaste(GiftTaste taste)
{return taste switch{GiftTaste.Love => Color.Gold,GiftTaste.Like => Color.Green,GiftTaste.Neutral => Color.Gray,GiftTaste.Dislike => Color.Orange,GiftTaste.Hate => Color.Red,_ => Color.Black};
}private IEnumerable<IFormattedText> GetText(ItemRecord[] items, bool showUnknown, bool highlightUnrevealed, bool onlyOwned)
{if (!items.Any())yield break;int unrevealed = 0;int unowned = 0;Color baseColor = this.GetColorForTaste(this.ShowTaste);for (int i = 0, last = items.Length - 1; i <= last; i++){var entry = items[i];if (!showUnknown && !entry.IsRevealed){unrevealed++;continue;}if (onlyOwned && !entry.IsOwned){unowned++;continue;}string text = i != last? entry.Item.DisplayName + I18n.Generic_ListSeparator(): entry.Item.DisplayName;bool bold = highlightUnrevealed && !entry.IsRevealed;yield return new FormattedText(text, baseColor, bold);}if (unrevealed > 0)yield return new FormattedText(I18n.Npc_UndiscoveredGiftTaste(count: unrevealed), Color.Gray);if (unowned > 0)yield return new FormattedText(I18n.Npc_UnownedGiftTaste(count: unowned), Color.Gray);
}/// <summary>An item that can be shown in the list.</summary>/// <param name="Item">The item instance.</param>/// <param name="IsInventory">Whether this item is in the player's inventory.</param>/// <param name="IsOwned">Whether the player owns at least one of this item somewhere in the world.</param>/// <param name="IsRevealed">Whether the player has discovered this gift taste in-game.</param>private record ItemRecord(Item Item, bool IsInventory, bool IsOwned, bool IsRevealed);
}

总结与收获
从最初只是想“给星露谷加点小功能”,到最终成功修改了 Lookup Anything 模组,这段二次开发之旅远比我想象的曲折,但也远比我想象的收获丰富。

开始我满怀信心地下载了源文件,却发现满屏的文件和代码我无从下手,学习了几个教程之后我试着上手建立自己的第一个框架,很高兴它运行成功了。接着我开始研究本次项目的整个框架和数据,说实话找寻数据的过程同样十分艰难,因为我并不是非常熟悉星露谷的数据存放。

我经历了反编译、配置环境、修复无数编译错误的“至暗时刻”,按下“dotnet build”的那一刻你永远会等来几个报错还是“成功”。好在努力没有白费,最终让 Lookup Anything 成功运行,那一刻的成就感难以言喻。三个功能——博物馆捐赠追踪、动物健康警告、送礼颜色优化——逐个在游戏中呈现出来。虽然只是添加了几行信息,但每次在游戏中看到自己亲手加入的绿色“已捐赠”或红色警告,那种满足感是单纯玩游戏无法比拟的。

面对一个又一个编译错误,从最初的烦躁到后来冷静分析,我学会了如何阅读错误信息、如何搜索解决方案。而Lookup Anything 清晰的代码设计让我明白,一个好的项目结构能大大降低二次开发的门槛。每个实体对应一个 Subject,每种信息由 Field 渲染,这种设计值得学习。能站在作者 Pathoschild 的肩膀上,阅读他精心编写的代码,本身就是一种学习。感谢所有为社区贡献的开源作者!

最后

这次经历让我对模组开发有了更深的理解,也激发了我继续探索的热情。接下来,我打算尝试优化更多功能(最好把我很喜欢但作者不再更新的模组优化到适配当前游戏版本),甚至构思一个自己的独立模组。

感谢你读到这里,希望我的分享能给你带来一些启发。如果你也在进行类似的尝试,遇到了问题,或者只是想交流心得,欢迎留言讨论!(ˆ꜆ . ω . ). ω . ꜀ˆ)

http://www.jsqmd.com/news/458288/

相关文章:

  • 【2025最新】基于SpringBoot+Vue的船舶维保管理系统管理系统源码+MyBatis+MySQL
  • ros1科学安装方法
  • 行星减速器装配图CAD图纸
  • 超详细:数据库的基本架构
  • 禁止使用存储过程
  • 【毕业设计】SpringBoot+Vue+MySQL 船运物流管理系统平台源码+数据库+论文+部署文档
  • ROS1中的package.xml文件的作用:
  • 5G-A 定位精度提升深度解析
  • flask-django基于python的线上博物馆门票预约以及活动报名系统的设计与实现
  • 基于Java+SSM+Flask基于BS模式的直播电商交流平台(源码+LW+调试文档+讲解等)/BS模式/直播电商/交流平台/在线直播/电商直播/实时互动/商务直播/网络直播/直播互动/直播销售
  • 嘘!我在公司电脑上玩游戏,连客户端都没装!
  • Kafka 被收购,国产替代势在必行
  • flask-django基于python的台球开台系统
  • 自动驾驶---E2E架构演进
  • 铣床夹具CAD图纸
  • 03-content-creator
  • OpenClaw 超级 AI 实战专栏【入门与环境】(一)OpenClaw 是什么?一文看懂核心能力与应用场景
  • 写论文省心了 10个一键生成论文工具深度测评:自考毕业论文+学术写作全攻略
  • AI正在重塑企业运营方式:为什么电商行业正在率先拥抱智能客服
  • WPS表格图表
  • 谷歌seo外链和内链区别?核心玩法与避坑指南
  • AI写论文就选这些!4款AI论文生成工具解决写职称论文痛点
  • 谷歌seo外链重要还是内容重要?底层逻辑与实操拆解
  • 基于Java+SSM+Flask课程辅助教学网站(源码+LW+调试文档+讲解等)/课程辅助工具/在线教学平台/课程学习网站/教学辅助软件/网络教学资源/课程资料下载/在线辅导服务/学习辅助网站
  • 企业级大学生选修选课系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】
  • Simulink环境下基于MATLAB平台的智能电网微网运行控制与并网仿真研究:逆变器控制及下...
  • 黑客工具包武器化技术演进与防御范式重构研究
  • linux 内核 stop_machine函数
  • 基于SpringBoot+Vue的spring大学生双创竟赛项目申报与路演管理系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】
  • AI+flask老年人社区健康互助平台