.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}在上海活了二十多年，确实比较少见..BOSS拉黑了所有强制要求今天上班，尤其是迟到罚款的公司...很难接受..感觉完全没有人文关怀.（很满意现在的公司，看到某前司还要到岗上班我真的释怀地笑了一下，嗯，确实是像他们干的出来的事情...）我在滨江这里，还可以，这几天其他地方很夸张了，我这里一点积水都没有。只是真的风大雨大，我下午骑车的时候都骑不了直线。路上难得人这么少...好累，刚刚锻炼好，肌肉好少..无氧好累.........
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}这个月终于收到了期待已久的小裙子，心情很好。刚收到时，版型还有一些不太合身的地方，于是我拿去改衣店做了调整。修改之后，整件衣服舒服了很多。陆续穿了大半个月，一件衣服好不好看虽然，但真正决定它能不能被频繁穿着的，还是是不是能真的穿得舒服。买都买了...天天穿才是真的回本。后来我还穿着它去拍了外景。不得不感叹，纯棉面料在高温天气里的体验确实很好。我本身属于非常容易出汗的体质，在接近四十摄氏度的天气里穿着它，体感居然和普通T恤差不多，甚至还要更凉快一点。可能是因为衣服本身有镂空设计，透气性很好，既保留了造型感，也没有牺牲太多舒适度。对我来说，这种兼顾美观与实穿性的衣服真的很难得。其实和我刻板印象里的lo裙完全不一样...这个月的MMD进度和原先的计划相差不多，只是中途出现了一件让我有些崩溃的事...官方发布了新模型。（另外！雷米真的发布以后更对胃口了，完全在Xp上。光速抽了2+1。哦！鲨鱼妹的手办也到了，真是双喜临门啊~~~接下来就是等挽昼或者这位像卡卡西的小伙！）我之前花了很长时间给翅膀制作IK，结果新模型发布时，官方直接提供了FK骨骼。看到消息的那一刻，真的有一种“人要碎掉了”的感觉。虽然IK和FK各有各的使用场景，之前做的工作也不能说完全没有价值，但这种刚投入大量精力解决完一个问题，转头就发现官方已经给出另一套方案的体验，多少还是有些微妙。不过，抛开这个小插曲，这次创作对我来说依然很重要。毕设结束以后，我已经很久没有像这样独立完成一次相对完整的创作流程了。从前期构思、模型处理，到动作、镜头、灯光和最终呈现，整个流程都由自己一点点推进。完整创作其实是一件很消耗人的事，它意味着你需要不断作出选择，也需要承担每一个选择带来的结果。但与此同时，它也会让人重新感受到一种久违的掌控感：脑海中的想法，正在通过自己的手，逐渐变成真实可见的东西。几天前我刷到了一段孙杨相关的采访切片。他的妻子曾经对着极光许愿：“希望我一直一直爱我的老公。我老公也会一直一直爱我。”很多人听到这个愿望时觉得有些奇怪。被爱不能由自己控制，所以许愿也合理。爱作为一种自主行为，为什么需要许愿。但我听到这句话时，心里却突然“咯噔”了一下，产生了一种很奇妙的共鸣。我希望自己可以一直拥有创造力。我希望自己能够一直鲜明地活着，一直保有表达欲，一直愿意观察、感受，并把那些难以言明的东西转化成作品。许愿一直拥有某样东西，并不一定是因为当下已经失去了它。恰恰可能是因为此刻真切地拥有着，所以才更加明白它有多珍贵，也更加害怕有一天会在不知不觉中失去。人似乎总会发生变化。热情可能被消耗，表达欲可能被现实磨平，曾经认为绝不会改变的东西，也可能在漫长的时间里逐渐变得模糊。所以，“希望一直拥有”并不是对当下的否定，而是一种近乎郑重的珍惜。最近，我对AI也产生了一些新的思考。从实际生产流程来看，pipeline最核心的需求往往还是质量与效率。假如能在两者之间找到一个合适的平衡点，AI确实可能成为非常有力的生产工具。目前很多AI工具的优势是速度快，但“生成得快”并不等同于“能够直接使用”。在专业项目中，产出通常需要满足明确的风格、结构、技术规范和交付标准。AI或许可以迅速给出一个结果，但这个结果距离真正可用，往往还有相当长的距离。在这种情况下，人的作用并没有消失，反而变得更加关键。如何定义问题、拆解需求、判断结果、及时纠偏，并把AI引导到正确的方向，都会直接影响最终质量。AI可以快速探索大量可能性，但什么是正确的方向、什么样的结果值得被保留，仍然需要人的经验与判断。AI在美术和编程领域也可能展现出不错的能力，但它的使用成本并不一定低。这里的成本不仅包括算力和金钱，也包括沟通、验证、修改，以及学习新工具所需要投入的时间。某些任务看似由AI迅速完成了，但如果后续需要花费大量精力修正，整体效率未必真的更高。因此，比“AI能不能做”更重要的问题，也许是：在什么环节使用AI最合适？怎样的任务应该交给AI，怎样的判断必须由人完成？如何让它真正融入工作流程，而不是制造新的返工？很多人面对AI带来的冲击，会感到焦虑。这种情绪当然可以理解，因为行业的变化确实正在发生，而且速度很快。但仅仅焦虑并不能让变化停下来。趋势已经出现，与其一直担心自己会不会被影响，不如尽早理解它、测试它，找到真正适合自己的使用方式。顺势而为并不意味着盲目追随，也不是把所有工作都交给AI，而是保持判断力，同时主动掌握新的工具。工具越强，人越需要知道自己究竟想做什么。也许最终真正重要的，仍然是人的审美、经验、选择，以及想要表达的东西。这个月还去朋友家吃了很好吃的海鲜和见手青，真的很满足。（可惜没见到小人）现在越来越觉得，身边有会做饭的人实在是一件非常幸福的事。好吃的饭不仅能填饱肚子，也会让人感觉自己被认真照顾着。以后要是结婚，我一定要找一个非常会做饭的人。毕竟生活已经够复杂了，回到家还能吃上一顿热腾腾又好吃的饭，应该会非常幸福吧。
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}严格来说，这并不是完全从零自制的，标题多少有一点“标题党”。这个项目基于 KDE 开源的 Markdown 编辑器 ghostwriter 进行修改。原项目地址如下：KDE/ghostwriterghostwriter 本身已经是一款相当成熟的 Markdown 编辑器。我主要是在原项目的基础上，根据自己的实际使用需求进行了一些功能调整和二次开发。与其说是“自制”，更准确的说法应该是：我给自己魔改了一款更顺手的 Markdown 编辑器。为什么需要一个 Markdown 编辑器？最近需要编写的 Markdown 文件越来越多。除了普通的项目文档以外，Codex 的提示词、Agent 配置、Skill 说明，以及各种工作流程和技术记录，基本都需要通过 Markdown 文件进行维护。随着文件数量增加，我对编辑工具的要求也逐渐明确：文件需要保存在本地必须能够方便地读取和插入本地图片,支持多种路径编辑体验要足够轻量拥有实时预览不要干扰现有的代码开发环境我之前也尝试过一些常见的工具。有道云笔记本身很好用，跨设备同步和在线编辑都很方便。但它首先是一款以“云端”为核心的笔记工具，主要使用场景仍然偏向线上内容管理。对于需要直接操作本地Markdown文件、本地目录和本地图片的工作流来说，它的体验并不理想。尤其是在读取本地图片、维护相对路径以及管理本地文件结构时，整个流程比较别扭。对于普通笔记来说，这些问题可能并不明显，但一旦涉及 Agent、Skill 或项目文档，就很容易增加额外的整理成本。感觉和Codex生态不是很搭配。Visual Studio Code 当然也可以编辑 Markdown，而且很好地解决了本地文件管理的问题。无论是目录结构、版本控制，还是 Markdown 插件生态，VS Code 都已经足够完善。但我一直希望代码工作区能够保持干净、清晰。代码、资源文件、提示词、个人笔记和长篇说明文档如果全部混在同一个编辑器里，很容易让工作区变得杂乱。虽然从功能上来说，VS Code 完全能够承担这些任务，但我不希望所有工作都挤在同一个软件中。我真的很烦开特别多同一个软件的窗口..我需要一个相对独立的写作环境，将“编写代码”和“整理文档”隔离开来。这种隔离并不是因为 VS Code 不够好，而是因为不同类型的工作需要不同的注意力模式。写代码时，我希望关注工程结构、逻辑和调试；写文档时，我更希望专注于内容、表达和信息组织。找到 ghostwriter带着这些需求，我开始在 GitHub 上寻找合适的 Markdown 编辑器，后来发现了 KDE 的 ghostwriter。使用一段时间后，我发现它基本符合我的预期：它以本地文件为核心，界面足够轻量，同时提供 Markdown 编辑和实时预览。相比功能复杂的笔记平台，它更接近一个纯粹的写作工具；相比完整的集成开发环境，它又不会带来过多与写作无关的界面和功能。整体使用体验已经很好，但在 Windows 环境下，它仍然存在几个影响我日常使用的问题。原版中遇到的几个问题1. 输入法候选框位置异常我遇到的第一个问题是中文输入法候选框的位置异常。在编辑文字时，输入法候选框不会出现在当前光标附近，而是始终显示在窗口左上角。这会导致视线不断在正文和屏幕左上角之间移动，长时间输入中文时非常影响体验。我查阅了一些相关信息，发现 Linux 版本似乎处理过类似问题，但我使用的是 Windows。后来尝试调整了多个与输入法和窗口定位有关的接口，最初都没有取得理想效果。这类问题看起来只是一个小的界面瑕疵，但对于以文字输入为核心的软件来说，候选框能否正确跟随光标，实际上会直接影响编辑效率。2. 编辑区与预览区不能同步滚动ghostwriter 的编辑区和预览区支持通过标题进行定位。点击对应标题后，两侧内容可以跳转到相应位置。但这并不是我真正想要的交互方式。我更希望编辑区和预览区能够根据当前阅读位置持续同步滚动：左侧编辑到哪里，右侧预览就跟到哪里；拖动右侧预览时，左侧也能够定位到对应的 Markdown 内容。对于较短的文档来说，标题跳转已经基本够用。但面对篇幅较长、标题层级较多的技术文档时，仅依靠标题定位仍然不够直观。特别是在调整段落、检查图片位置或修改局部排版时，同屏同步滚动能够明显减少来回查找内容的时间。3. 不支持直接拖入文件和图片第三个问题是文件和图片的拖放体验。原版并不支持按照我的预期，直接将本地文件或图片拖入编辑区域。对纯文字写作来说，这可能不是一个严重的问题，但我的很多文档都需要配合图片才能完整表达。例如：Agent 或工具的操作界面；节点结构和流程示意图；程序运行结果；美术效果对比；Bug 复现截图；参数配置说明。如果每次插入图片都需要手动复制文件、整理目录、查找路径，再编写 Markdown 图片语法，整个过程会产生很多重复操作。我希望编辑器能够主动完成这些机械步骤：接收拖入的文件，将图片放置到合适的位置，并生成可以直接使用的 Markdown 引用。我修改了什么？针对上述问题，我在 ghostwriter 的基础上进行了修改，主要完善了以下功能：输入法候选框跟随光标我调整了 Windows 环境下输入法候选窗口的定位逻辑，使候选框能够尽可能跟随当前文本光标显示。这样在输入中文时，视线可以始终停留在正在编辑的内容附近，不需要再频繁查看窗口左上角。这个改动从功能上看并不复杂，却是几项修改中体感最明显的一项。对于中文用户而言，输入法交互本身就是编辑体验的重要组成部分。编辑区与预览区同步滚动我补充了编辑区与预览区之间的滚动同步逻辑。现在查看长篇 Markdown 文档时，两侧内容能够更加稳定地对应。无论是编辑正文、调整图片，还是检查最终排版，都不再需要频繁依赖标题跳转。同步滚动真正困难的地方并不是简单地让两侧拥有相同的滚动条数值。Markdown 源文本和最终渲染结果的高度通常并不一致：标题、图片、代码块和列表经过渲染后，都会占据不同的空间。因此，更合理的处理方式是建立源文本位置与预览内容位置之间的对应关系，而不是机械地同步滚动百分比。支持拖入文件和图片我还增加了拖放相关的处理。现在可以将本地图片或文件直接拖入编辑区域，由编辑器生成相应的 Markdown 内容。对于需要大量使用截图和效果图的技术文档来说，这项功能能够减少很多重复操作。我的目标并不是把编辑器改造成一个复杂的资源管理工具，而是让“把图片放进文档”这件事变得足够自然：拖进去，然后继续写。演示开源地址我已经将修改后的版本开源到 GitHub，欢迎有类似需求的朋友下载使用，也欢迎提交 Issue 或参与改进。我的修改版：FxYaM/YumiMDWriter最后再次感谢 ghostwriter 原作者及 KDE 社区的开源贡献。我的版本是在原项目基础上进行的二次开发，并不是完全从零实现的独立编辑器。
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}今天湖光月影到了，试穿了一下赶感觉袖子不舒服，我还寻思是我太胖了呢！然后发现九十斤老鼠干姐妹都说不舒服，于是我果断决定改衣服。（而且这衣服巨透，需要内衬）很幸运，隔壁小区有一家改衣店。和老板沟通以后说八点之前过去。19：05背着电脑出公司，小雨。雨不大，我想没事，我十几分钟就能到家，淋十几分钟小雨是没关系的。19：08突然打雷，然后倾盆大雨落了下来。我自己倒是没关系，但是我背着电脑啊！我一下害怕极了，我这个surface pro 6买了六七年了，几乎是很难很难维修了，而且我完全不打算买新的电脑。现在它还是很流畅，屏幕颜色也很好。19：25到家了，赶紧拿上衣服，也不管全身都湿了，赶紧看电脑。竟然完全是干的！！！感谢Echo的选品！！！！！裙子套上隔水袋，穿了雨衣，就跑出门，还踩了煤球球一脚。19：40到了改衣店，老板说，可以改，周一能改好。我问，这个是比较难的操作吗？他说，也不是，明天就住院做手术了，肾结石了。我有点犹豫。我说，要是今天能改就好了...老板竟然答应了！然后我还付了其他盖衣服的定金，我想把一些改短一点，裤子短一点。最近瘦了一些衣服不合身了。好几件加起来才不到100！祝老板发财！祝老板明天手术顺利555520：00衣服改好了！特别合身！袖子真的舒服多了！！！！！！20：15洗了个热水澡，穿上完全合身的衣服，用着劫后余生的宝贝电脑，充满感恩的心写下这篇日记！
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}不是最佳配队，但是我手气太差了，贝拉2+1了，小斧头1+1了，猫猫和铁腕抽不出来啊，根本抽不出来但是思路都是一样的，可以按照这个逻辑去思考自己的队伍养成。说实话暂时感觉和P5x没有体量大很多，肉鸽不就是约等于x的列车pro max吗，我感觉就那样吧...没有网传的那么玄乎..除了感觉大脸币很缺以外其他其实还好。确实比较繁琐冗余，实在是搞得太复杂了，但是去理解体系的话就好一点？如果我未来退坑，多半是因为活动重复。本体当单机玩，不要攀比，不要赶进度，其实还不错的。另外拿完奖励目前来说就算全是最烂的角色都是绰绰有余的，不爬塔的话不会有强度焦虑，爬塔的蔷薇石也就几天日常的奖励，不如把日常做完。不结束航行日常不到15分钟是可以做完的。直接照抄版7/23更新：角色队伍定位海螺 / 船歌标签优先级优先职业同标签技能优先级骰子往日之影茉莉协战主 C暴击爆发全队核心输出暴击 / 爆伤 ×2↓额外行动 ×2转职完成后继续补暴击、爆伤标签5级：缠命人↓10级：讨债人↓15级：异香缠命人缠命人：紧缚怒火 ＞ 一击清算讨债人：别想逃 ＞ 收债倒计时最终携带：别想逃 + 紧缚怒火暴击率已经足够时改为：别想逃 + 收债倒计时高光蝶陨骰＋普通聚浪骰暴击率不足时，可将聚浪换为：雷云骰黑：安眠人偶紫：船之美小斧头追击副 C共演启动器玫瑰印记收割额外行动 ×2↓终结技 ×2资源优先级低于茉莉，优先保证行动频率5级：讨债人↓10级：蚁妈↓15级：首席讨债人讨债人：藏身雾中 ＞ 收债倒计时 ＞ 收割时间蚁妈阶段不必强行更换主动技能，最终携带：藏身雾中 + 收债倒计时高光叠云闪＋普通聚浪骰需要提高小斧头自身输出时，可改为：高光蝶陨骰黑：文明法则紫：白日下的梦铁腕统领主坦团队护盾嘲讽承伤受击反击护盾 ×2↓防御 ×2高压关卡优先让护盾职业成型5级：铁壁↓10级：笨壳↓15级：叹息之壁（还没出，先选静默据壳）铁壁：盾阵驰援 ＞ 其他单体护盾技能狮心者：无退路之盾 ＞ 纯反击伤害技能最终携带：盾阵驰援 + 无退路之盾高光磐石骰＋普通嘲弄骰黑：何人致哀之花紫：昂首之旗伊莎贝拉治疗增益附加伤害附加伤害 ×2↓治疗 ×2生存压力大时优先治疗5级：麻烦精↓10级：灵医↓15级：麻烦艺术家灵医 / 辅助：尽情欢呼吧 ＞ 庆典不散场最终携带：尽情欢呼吧 + 庆典不散场高光鲸落骰＋普通落雷骰黑：遗落宝石紫：被划掉的一页7/23之前：角色队伍定位海螺 / 船歌标签优先级优先职业同标签技能优先级骰子往日之影小斧头单体主 C追击收割额外行动 ×2↓终结技 ×25级：讨债人↓10级：蚁妈↓15级：首席讨债人讨债人：藏身雾中 ＞ 收割时间蚁妈阶段不换主动技能，最终仍带：藏身雾中 + 收割时间高光蝶陨骰＋普通聚浪骰追击节奏也可用：高光叠云闪黑：文明法则紫：船之美茉莉暗影暴击副 C暴击 / 爆伤 ×2↓额外行动 ×25级：缠命人↓10级：讨债人↓15级：异乡谗命人缠命人：紧缚怒火 ＞ 绝不手软最终携带：紧缚怒火 + 绝不手软高光重迫骰＋普通狂牙骰黑：安眠人偶懦夫主坦分摊伤害团队护盾受击 ×2↓护盾 ×2扛不住时可反过来先凑护盾5级：狮心者↓10级：铁壁↓15级：叹息之壁铁壁：传世壁垒 ＞ 巨盾立于此最终携带：传世壁垒 + 巨盾立于此高光磐石骰＋普通嘲弄骰黑：何人致哀之花紫：昂首之旗伊莎贝拉治疗增益附加伤害附加伤害 ×2↓治疗 ×2生存压力大时优先治疗5级：麻烦精↓10级：灵医↓15级：麻烦艺术家灵医 / 辅助：尽情欢呼吧 ＞ 庆典不散场最终携带：尽情欢呼吧 + 庆典不散场高光鲸落骰＋普通落雷骰黑：遗落宝石紫：被划掉的一页补位：角色队伍定位海螺 / 船歌标签优先级优先职业同标签技能优先级骰子往日之影饿猫负面辅助协攻副 C破架势 / 增伤负面 ×2↓强化骰 ×2队伍急缺破架势速度时，也可先凑强化骰5级：蜜丸↓10级：钓命儿↓15级：蜜糖魔统部分攻略写作“蜜糖魔铳”，名称以当前客户端显示为准蜜丸 / 负面：不结束回合的负面技能 ＞ 同时叠多种负面或易伤 ＞ 延长负面持续时间 ＞ 纯固定伤害钓命儿 / 强化骰：强化骰获取与循环 ＞ 拉条 / 加速 ＞ 不结束回合 ＞ 单次直伤最终优先保留：1个稳定挂负面的技能 + 1个强化骰循环技能高光共鸣骰＋普通碎碎骰黑：鱼骨串紫：偏差之时铁腕统领主坦团队护盾嘲讽反击伤害分摊护盾 ×2↓受击 ×2铁腕自身已经很擅长受击反击，通常先让团队护盾体系成型5级：铁壁↓10级：狮心者↓15级：叹息之壁铁壁 / 护盾：盾阵驰援 ＞ 无退路之盾敌方群体爆发很高时：无退路之盾 ＞ 盾阵驰援最终携带：盾阵驰援 + 无退路之盾高光磐石骰＋普通嘲弄骰黑：何人致哀之花紫：昂首之旗海螺/船歌到底怎么选构筑时不要看到“金色”“数值高”就随便拿，先锁定这个角色要转的职业。实战思路是：先确定目标职业。海螺优先凑这个职业需要的两枚同类小标签。三选一技能时，尽量拿与目标职业一致的大标签技能。已经凑齐第一职业后，再开始补第二职业，不要四五种标签平均拿。同标签出现多个技能时，再比较它们是否能改善角色的核心循环。最容易翻车的情况，是“每张卡看起来都不错”，最后却凑出一堆各一枚的标签，谁都转不了职。详细思路小斧头：主 C 构筑推荐路线第一职业：讨债人 海螺优先：额外行动 ×2小斧头与茉莉一起上场时，讨债人优先级最高。茉莉负责叠暗影，小斧头通过追击和额外行动把暗影转成爆发。同标签技能优先：藏身雾中目标身上暗影足够时可以触发追击。与茉莉的暗影叠层最直接，属于队伍核心联动。收割时间对有暗影的目标收益高。高血量目标还可以继续追击，适合开场压血线。其他额外行动技能优先“不结束回合、追击、回能、再行动”。纯面板伤害但不改善循环的卡放后面。第二职业：蚁妈 海螺优先：终结技 ×2第二职业补终结技强化。出现同标签卡时，按下面顺序选：降低终结技启动成本、返还能量、加快终结技循环；提高终结技最终伤害；普通一次性直伤。骰子稳定通用组合：高光蝶陨骰普通聚浪骰追击节奏更快的替代选择：高光叠云闪没有完全成型时，先保证高光骰能改善行动频率或爆发循环，再追求普通骰的精确搭配。往日之影黑色首选：文明法则紫色备选：船之美船之美适合频繁额外行动的角色；文明法则作为黑色核心优先级更高。茉莉：暗影副 C 构筑你这套队伍里已经有小斧头，所以茉莉不需要抢主 C 位，优先走暴击、爆伤和暗影辅助。推荐路线第一职业：缠命人 海螺优先：暴击/爆伤 ×2同标签技能优先：紧缚怒火提升自身暴击率；攻击能够给敌人追加暗影；暴击时还能额外叠暗影；不结束回合，循环价值非常高。绝不手软单体伤害高；降低目标暴击抗性；适合作为后续爆发铺垫。其他暴击/爆伤技能暴击率、暗影叠层、减暴抗优先；单纯暴伤而没有稳定暴击率时，优先级降低。第二职业：讨债人 海螺优先：额外行动 ×2有小斧头时，茉莉的讨债人是第二顺位；没有小斧头时，茉莉才更适合把讨债人放到第一顺位。讨债人技能可按：藏身雾中 / 收割倒计时；收割时间；普通追击或纯直伤。骰子高光重迫骰普通狂牙骰往日之影黑色首选：安眠人偶它与额外行动、暴击、暗影的联动高度一致，基本就是茉莉的毕业方向。懦夫：承伤坦克构筑懦夫的重点不是打伤害，而是：主动吃攻击；快速消耗胆怯；转化铠甲；触发个人技能和减伤；为队友分摊伤害；用终结技提供团队护盾。因此，选卡时不要被攻击倍率骗走。推荐路线第一职业：狮心者 海螺优先：受击 ×2懦夫被打本身就能推进核心机制，所以受击路线通常比先堆纯护盾更顺。同标签技能按效果选：稳定提高受击权重、嘲讽、替队友承伤；受击后获得铠甲、减伤或资源；反击伤害；只有伤害、没有承伤价值的卡。第二职业：铁壁 海螺优先：护盾 ×2护盾标签同技能优先：使用后不结束回合的护盾团队护盾护盾兼减伤、铠甲或回能单体自盾盾击类伤害卡什么时候反过来先拿护盾出现以下情况时，先凑铁壁：噩梦难度下懦夫经常一轮内暴毙；伊莎贝拉的治疗还没成型；敌人开场爆发特别高；小斧头和茉莉的输出已经够，只缺稳定生存。骰子高光磐石骰普通嘲弄骰这套是按懦夫的承伤机制与当前主坦通用路线匹配的，核心目标是提高稳定承伤和团队保护，而不是补输出。往日之影黑色首选：何人致哀之花紫色备选：昂首之旗何人致哀之花能同时改善体魄技能、受击权重、护盾和铠甲循环，非常适合主坦。伊莎贝拉：治疗兼补伤构筑伊莎贝拉并不是只会加血。她可以通过情报、附加伤害和队友增益，在保证续航的同时补输出。默认路线第一职业：麻烦精 海螺优先：附加伤害 ×2同标签技能按下面顺序选：治疗时也能造成附加伤害，或能同时兼顾治疗与输出；不结束回合、提高行动频率、加快情报叠层；提高队友输出或给敌人上易伤类效果；单纯一次附加伤害。第二职业：灵医 海螺优先：治疗 ×2治疗技能按：群体治疗；治疗兼回能、加速、增伤、减伤；单体急救；只有溢出治疗、没有额外收益的技能。生存压力大时把顺序改成：治疗 ×2 → 灵医附加伤害 ×2 → 麻烦精噩梦、越级挑战、懦夫装备没成型时，这样更稳。骰子高光鲸落骰普通落雷骰鲸落骰的群体恢复能力与伊莎贝拉定位很合拍；落雷骰用于补充伤害和行动收益。往日之影黑色首选：遗落宝石紫色备选：被划掉的一页遗落宝石更偏综合生存与治疗保护；被划掉的一页偏速度和治疗效率，是很优秀的过渡及副位选择。四人队的实际拿牌顺序开局到第一职业成型小斧头：优先额外行动，凑讨债人；茉莉：优先暴击/爆伤，凑缠命人；懦夫：优先受击，凑狮心者；伊莎贝拉：正常局先附加伤害，困难局先治疗。第一职业成型后小斧头补终结技，转蚁妈；茉莉补额外行动，转讨债人；懦夫补护盾，转铁壁；伊莎贝拉补另一个治疗/附伤职业。队伍战斗逻辑懦夫吸引攻击、分摊伤害；伊莎贝拉维持血线并提供增益；茉莉提高暴击并给敌人叠暗影；小斧头消耗暗影，连续追击完成收割。同标签三选一的通用判断公式不知道具体技能名字时，可以直接按这一条判断：改善核心循环的技能 > 不结束回合/额外行动 > 回能与资源 > 团队增益或减益 > 纯倍率伤害。但角色例外是：小斧头：追击、额外行动、暗影消耗最优先；茉莉：暴击率、暗影叠层、减暴抗最优先；懦夫：嘲讽、承伤、铠甲、群盾最优先；伊莎贝拉：群疗、治疗联动附伤、行动与情报效率最优先。彩蛋：在奇怪的地方世界线收束了..
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}简直了，让自己完全没时间摸鱼我愿称作高级牛马的自我修养——自己做皮鞭踩坑都是我自己遇到的，如果你没有遇到或者遇到别的可以飞书填写表单。可以帮你安装Hooks，爱发电购买玉米粒(6.6R)就可以！这篇文章记录一次从 0 到 1 搭建「VSCode Codex 执行完成后自动发送飞书单聊通知」的完整过程。最终效果是：在 VSCode 中使用 Codex 执行任务，任务结束后，飞书会自动收到一条来自应用机器人的单聊消息。消息中可以包含执行时间、电脑名、项目路径、模型名、Hook 事件类型，以及 Codex 最后一条回复。1. 总体思路与方案选择1.1 最终目标想实现的链路很简单：Codex 执行结束 ↓ Codex Stop Hook 触发 ↓ 本地 Python 脚本执行 ↓ 获取飞书 tenant_access_token ↓ 调用飞书发送消息接口 ↓ 飞书应用机器人单聊通知我最终飞书收到的消息类似：✅ Codex 执行完啦！ 时间：2026-06-18 16:09:09 电脑：DESKTOP-XXXXXXX 项目：D:\YourProject 模型：gpt-5.5 事件：Stop 最后回复： Hi. What do you want to work on?1.2 为什么使用 Codex Stop HookCodex Hooks 是 Codex 提供的生命周期扩展机制，可以在特定事件发生时执行本地脚本。这里最适合使用的是 Stop 事件，因为它会在 Codex 当前回合停止时触发。官方文档中提到，Hook 配置可以放在：~/.codex/hooks.json <repo>/.codex/hooks.json对于本文场景，我使用的是用户全局配置：C:\Users\YourName\.codex\hooks.json这样所有项目都可以复用同一套通知逻辑。1.3 为什么不用群机器人 Webhook一开始我考虑过飞书「自定义机器人 Webhook」。Webhook 方案很简单：Codex Hook → 调用 Webhook → 飞书群收到消息但自定义机器人更偏向群聊通知，如果目标是「机器人直接单聊发给我」，更适合使用飞书开放平台的应用机器人能力。所以最后采用的是：飞书自建应用机器人 + tenant_access_token + open_id + 发送消息 API这样可以让应用机器人直接给指定用户发送单聊消息。1.4 最终文件结构最后本地会有两个关键文件：C:\Users\YourName\.codex │ ├─ hooks.json │ └─ hooks └─ feishu_codex_done.py其中：hooks.json负责告诉 Codex：当 Stop 事件触发时，运行哪个命令。feishu_codex_done.py负责调用飞书接口，发送单聊通知。2. 飞书开放平台配置2.1 创建自建应用进入飞书开放平台，创建一个自建应用，例如：应用名称：Codex 应用类型：企业自建应用创建完成后，可以在「凭证与基础信息」中看到：App ID App Secret这里要注意：项目说明App ID应用 ID，可以写入环境变量App Secret应用密钥，相当于密码，不要公开open_id用户在当前应用下的身份 ID，后面需要获取如果 App Secret 曾经出现在截图、日志、博客草稿中，建议立刻重置。2.2 添加机器人能力在应用后台左侧找到：添加应用能力 → 机器人添加完成后，可以在飞书客户端中看到这个应用机器人。此时它已经具备作为机器人出现的基础能力，但还不能保证可以主动发消息。2.3 开通发送消息权限进入：权限管理 → 开通权限搜索：im:message需要确认至少有这个权限：以应用的身份发消息 im:message:send_as_bot如果已经显示「已添加」，就不需要重复添加。2.4 发布应用版本权限或机器人能力发生变更后，需要重新发布应用。路径一般是：应用发布 → 版本管理与发布 → 创建版本版本说明可以写：添加机器人发送消息权限如果页面提示「本次发布免审核」，保存并提交发布后即可使用。3. 获取接收人的 open_id3.1 为什么不用邮箱飞书发送消息接口支持多种 receive_id_type，例如：open_id user_id union_id email chat_id我这里不想用邮箱，而是希望机器人直接发给当前飞书用户，所以选择：receive_id_type = open_id3.2 open_id 的坑最开始我误用了 API 调试页面中的示例 open_id，结果发送失败，返回类似：{ "code": 99992351, "msg": "The request you send is not a valid {open_id} or not exists", "field_violations": [ { "field": "receive_id", "description": "id not exist" } ] }这个错误的意思是：receive_id 不是当前应用下真实存在的 open_id注意，open_id 是和应用相关的。不要随便复制别的页面、别的应用、示例代码里的 ou_xxx。3.3 正确获取 open_id可以通过飞书开放平台的 API 调试台获取自己的 open_id。常见方式是：进入发送消息相关接口 ↓ 根据排查建议进入获取 open_id 的 API 调试台 ↓ 选择当前应用 ↓ 选择成员为自己 ↓ 调用接口 ↓ 复制返回的 open_id正确的 open_id 通常长这样：ou_xxxxxxxxxxxxxxxxxxxxx3.4 在 API 调试台测试发送消息在「发送消息」接口中测试：POST /open-apis/im/v1/messages查询参数：receive_id_type = open_id请求头：Authorization = Bearer tenant_access_token请求体示例：{ "receive_id": "ou_xxxxxxxxxxxxxxxxxxxxx", "msg_type": "text", "content": "{\"text\":\"✅ Codex 单聊通知测试成功！\"}", "uuid": "codex-test-001" }这里有一个很容易踩的坑：content 必须是字符串，而不是普通 JSON 对象。正确："content": "{\"text\":\"hello\"}"错误："content": { "text": "hello" }测试成功后，飞书单聊中会收到应用机器人发来的消息。4. 本地环境准备4.1 配置环境变量本地脚本需要使用四个环境变量：FEISHU_APP_ID FEISHU_APP_SECRET FEISHU_RECEIVE_ID FEISHU_RECEIVE_ID_TYPEPowerShell 中执行：setx FEISHU_APP_ID "cli_xxxxxxxxxxxxxxxxx" setx FEISHU_APP_SECRET "你的 App Secret" setx FEISHU_RECEIVE_ID "ou_xxxxxxxxxxxxxxxxxxxxx" setx FEISHU_RECEIVE_ID_TYPE "open_id"执行后会看到：成功: 指定的值已得到保存。4.2 setx 的坑setx 写入的是后续进程的环境变量，当前 PowerShell 窗口不一定能立刻读取到。所以执行完 setx 后，需要：关闭当前 PowerShell 重新打开 PowerShell 重启 VSCode否则脚本或 Codex 可能读不到环境变量。4.3 安装 Python一开始本机没有可用的 Python，运行下面命令都失败：python --version python3 --version py -3 --version错误类似：Python was not found; run without arguments to install from the Microsoft Store...可以使用 winget 安装 Python：winget install --id Python.Python.3.12 -e安装完成后，关闭 PowerShell，重新打开，再检查：python --version成功时会显示类似：Python 3.12.104.4 Windows Store Python 别名问题如果安装后仍然提示 Microsoft Store，可以检查：Windows 设置 → 应用 → 高级应用设置 → 应用执行别名关闭：python.exe python3.exe5. 编写并测试 Python 通知脚本5.1 创建脚本目录创建 Hook 脚本目录：mkdir "$env:USERPROFILE\.codex\hooks" -Force创建 Python 脚本：notepad "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"5.2 完整 Python 脚本写入下面代码（这里是最简单的，只有干完了和时间项目的信息，可以根据自己的需要拓展）：import datetime import json import os import sys import traceback import urllib.error import urllib.request APP_ID = os.environ.get("FEISHU_APP_ID", "").strip() APP_SECRET = os.environ.get("FEISHU_APP_SECRET", "").strip() RECEIVE_ID = os.environ.get("FEISHU_RECEIVE_ID", "").strip() RECEIVE_ID_TYPE = os.environ.get("FEISHU_RECEIVE_ID_TYPE", "open_id").strip() LOG_PATH = os.path.join( os.path.expanduser("~"), ".codex", "hooks", "feishu_codex_done.log", ) def safe_unicode(text): if text is None: return "" if not isinstance(text, str): text = str(text) return text.encode("utf-8", "replace").decode("utf-8", "replace") def cut(text, max_len=2000): text = safe_unicode(text) if not text: return "" return text if len(text) <= max_len else text[:max_len] + "..." def log(message): try: os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True) now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") message = safe_unicode(message) with open(LOG_PATH, "a", encoding="utf-8", errors="replace") as f: f.write(f"[{now}] {message}\n") except Exception: pass def post_json(url, payload, headers=None): data = json.dumps(payload, ensure_ascii=False).encode("utf-8", "replace") req = urllib.request.Request( url, data=data, headers={ "Content-Type": "application/json; charset=utf-8", **(headers or {}), }, method="POST", ) try: with urllib.request.urlopen(req, timeout=20) as resp: raw = resp.read().decode("utf-8", errors="replace") log(f"POST OK url={url} status={resp.status} body={cut(raw, 3000)}") return json.loads(raw) except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", errors="replace") log(f"POST HTTP ERROR url={url} status={e.code} body={cut(raw, 3000)}") raise except Exception: log("POST EXCEPTION:\n" + traceback.format_exc()) raise def require_feishu_ok(name, result): if not isinstance(result, dict): raise RuntimeError(f"{name} 失败：返回不是 JSON：{result}") if result.get("code") != 0: raise RuntimeError(f"{name} 失败：{result}") def get_tenant_access_token(): result = post_json( "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", { "app_id": APP_ID, "app_secret": APP_SECRET, }, ) require_feishu_ok("获取 tenant_access_token", result) return result["tenant_access_token"] def send_feishu_message(token, text): text = safe_unicode(text) result = post_json( f"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={RECEIVE_ID_TYPE}", { "receive_id": RECEIVE_ID, "msg_type": "text", "content": json.dumps({"text": text}, ensure_ascii=False), }, headers={ "Authorization": f"Bearer {token}", }, ) require_feishu_ok("发送飞书消息", result) return result def read_hook_stdin(): try: raw_stdin = sys.stdin.read() log(f"HOOK RAW STDIN: {cut(raw_stdin, 3000)}") if not raw_stdin.strip(): return {} return json.loads(raw_stdin) except Exception: log("HOOK STDIN JSON PARSE FAILED:\n" + traceback.format_exc()) return {} def main(): try: hook_data = read_hook_stdin() if not APP_ID or not APP_SECRET or not RECEIVE_ID: log( "MISSING ENV: " f"APP_ID={bool(APP_ID)}, " f"APP_SECRET={bool(APP_SECRET)}, " f"RECEIVE_ID={bool(RECEIVE_ID)}, " f"RECEIVE_ID_TYPE={RECEIVE_ID_TYPE}" ) print(json.dumps({"continue": True}, ensure_ascii=False)) return cwd = safe_unicode(hook_data.get("cwd", "")) now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") text = ( "尊敬的老大！小的干完了！！！🙇🏻‍♂️🙇🏻‍♂️🙇🏻‍♂️\n\n" f"时间：{now}\n" f"项目：{cwd if cwd else '未知'}" ) token = get_tenant_access_token() result = send_feishu_message(token, text) log(f"FEISHU SEND SUCCESS: {result}") except Exception: log("FEISHU NOTIFY FAILED:\n" + traceback.format_exc()) print(json.dumps({"continue": True}, ensure_ascii=False)) if __name__ == "__main__": main()5.3 手动测试脚本不要直接运行：python "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"这样可能看起来「没反应」，因为脚本会等待 stdin。正确测试方式是传入一个空 JSON：echo {} | python "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"如果飞书收到消息，说明脚本已经跑通。5.4 模拟 Codex Hook 输入可以用下面命令模拟 Codex 传入的数据：'{"cwd":"D:\\TestProject","model":"codex","hook_event_name":"Stop","last_assistant_message":"Test from local PowerShell"}' | python "$env:USERPROFILE\.codex\hooks\feishu_codex_done.py"这样飞书通知里就会出现模拟项目路径、模型名和最后回复。6. 配置 Codex Stop Hook6.1 创建 hooks.json创建或打开 Hook 配置文件：notepad "$env:USERPROFILE\.codex\hooks.json"写入：{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "python \"C:\\Users\\YourName\\.codex\\hooks\\feishu_codex_done.py\"", "timeout": 15, "statusMessage": "Sending Feishu notification" } ] } ] } }6.2 为什么建议写绝对路径一开始我尝试写："command": "python \"%USERPROFILE%\\.codex\\hooks\\feishu_codex_done.py\""但在 Codex Hook 里，%USERPROFILE% 不一定会像普通 cmd 那样展开。最后改成绝对路径后稳定成功："command": "python \"C:\\Users\\YourName\\.codex\\hooks\\feishu_codex_done.py\""6.3 在 VSCode 中确认 Hook重启 VSCode 后，在 Codex 输入框中输入：/hooks如果配置被读取，会出现类似：Stop command: python "C:\Users\YourName\.codex\hooks\feishu_codex_done.py" timeout: 15 statusMessage: Sending Feishu notification如果 Hook 没有被信任，需要在这个界面里启用或信任。6.4 测试 Codex 自动通知让 Codex 执行一个很小的任务：请回复一句 hello，不要修改文件。等待 Codex 结束后，飞书应该自动收到一条通知。这一步成功后，说明完整链路已经跑通：Codex Stop Hook → Python 脚本 → 飞书应用机器人 → 单聊通知7. 踩坑复盘与安全收尾7.1 这次踩过的坑问题原因解决方式自定义机器人不能单聊Webhook 更适合群聊通知改用应用机器人发送消息 APIid not existopen_id 不是当前应用下真实 ID通过 API 调试台重新获取权限已添加但不能发权限变更后未重新发布重新发布应用版本content invalidcontent 写成了 JSON 对象改成 JSON 字符串python 不可用本机未安装 Python 或被 Store 别名拦截安装 Python 并检查执行别名手动运行脚本没反应脚本等待 stdin使用 `echo {}python ...`PowerShell 脚本乱码中文编码和引号复制问题改用 Python 版脚本Hook 没触发Hook 未信任或路径未展开/hooks 确认，改用绝对路径7.2 App Secret 安全处理如果 App Secret 曾经出现在截图、日志或文章草稿里，建议立即去飞书开放平台重置。重置后重新写入环境变量：setx FEISHU_APP_SECRET "新的 App Secret"然后重启 VSCode。7.3 不要提交的内容不要把下面内容提交到 Git：真实 App Secret 真实 open_id 真实项目路径 包含密钥的截图如果要做成团队可复用工具，建议把敏感信息放在：系统环境变量 .env 文件 凭据管理器 CI/CD Secret7.4 后续优化方向当前版本只实现了「执行完成通知」，后续可以继续扩展：通知中增加任务耗时。通知中区分成功 / 失败。通知中列出修改过的文件。通知里只显示项目名，而不是完整路径。不同项目发送到不同飞书会话。把脚本封装成一个可复用小工具。加入消息卡片，让通知更好看。8. 官方文档索引8.1 Codex 相关Codex Hooks 官方文档Codex IDE Extension Features8.2 飞书机器人与消息飞书机器人概述自定义机器人使用指南发送消息 API消息常见问题8.3 飞书鉴权与用户身份自建应用获取 tenant_access_token如何获取 Open ID用户身份概述发布与审核自建应用结论最终稳定方案是：Codex Stop Hook + Python 本地脚本 + 飞书应用机器人单聊 API这套方案跑通后非常实用。Codex 在 VSCode 里执行任务时，我不用一直盯着窗口。只要它停下来，飞书就会给我发消息。这次踩坑最多的地方不是代码本身，而是：飞书 open_id 权限发布 Python 环境 Codex Hook 路径把这些点处理好以后，整个链路其实很清晰，也很适合继续扩展成个人自动化通知系统。后话后来我又迭代了，现在是这样的。可以简单指令执行“修改”，“回退”等功能。
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}终于忙完了，外面还在下雨，刚晾好衣服，很喜欢新买的洗衣液的味道。最喜欢的味道还是桂花香。心情愉悦！感到幸福！六月中旬到现在一直一直在下雨，我真的要长鳃了...南方的梅雨季节真的好难受..感觉终于熬过去了二月那种感觉，飘忽不定的，惶恐不安的，走投无路，自我怀疑的感觉。过了将近半年时间我才摆脱…回到了比较平和的心态。搬家了一个月左右，人是真的应该有足够的生活空间才能好好活着。生活比活着真的难太多了..虽然这周工作还捅了篓子，但是很快能解决，也学会了打包之类的技能。有一个靠谱的引路人我真的什么路都敢走。我又想起我还有很多技术分享的文章还没写..等我这个月有时间先发布一些简单的吧....我终于搞定了我所有需要的工具，挂载在我的主页下，自己创建工具最好的就是可以百分百贴合自己心意。说实话我觉得要比买好几个软件的会员价格低很多。而且任何想要迭代的可以马上响应。但是在搞定功能的前提下我又开始美化了，ai直接生成的ui真的只是看起来能用而已..其实学习写网页ui也没那么难，比我想的简单很多，可能我的要求也没那么高吧。我最近忙还因为我有一个大计划！我想试试MMD，圆梦童年了属于是。希望我这次不要烂尾吧。哦，还有一件事，端午去徐州旅游了，感觉商业化的景区没有什么特别的地方，但是暂时脱离自己的生活环境还是比较有意思的。但是烧烤真的太强差人意了。我真的很吃不习惯，我觉得甜醋真的很怪。不是我喜欢的味道。烤串食材也很普通，调味是基本没有。回上海第二天就去想吃很久以前，但是那一天很久以前要排队110桌，我也是惊呆了，平时不会有这么多人的。今天去就很正常，甚至没有排队。终于吃上了。很久以前才是我的真爱啊..今天还去六院看了我的糖尿病..就...快点瘦下来吧...我真的还想活很久......而且看病好贵啊...如果真的有确证以后还能用的医保的话，求真实病友分享..魔兽前公会感觉真的不会再开团本了，集合石真难顶啊!找了一个社区，284装等奶爆了291，暂时感觉社区有好有坏，但是人多就是会有这样的问题吧。社长暂时相处感觉还可以（果然只有龙人带龙人玩，新社长也是个恩护）。但是确实比较年轻，没有上过班的人果然是不一样的。很多说的话感觉真的在工作中是大忌...也可能我岗位问题，沟通时候会想很多..无所谓了，和我又有什么关系呢。我只想不要打团体验那么差而已。想之前我还是被照顾的小朋友。现在我也是照顾别人带领新人的老玩家了啊...上个月规划好的3400也打到了，打得我累死了！
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}端午还是开心的！数值只代表当下主观体感6月19日10:00｜出发前：化妆品失踪事件具体事件： 发现同行的猪忘记带我的化妆品。人物数值变动：心情 -1出行安全感 -1队友可信度 -1上班ing｜平安上班，普通但稳定具体事件： 公司人不多，和以往一样平安上班。没有大事发生，也没有新的幺蛾子。最近很喜欢上班！感觉比以前有安全感！！！人物数值变动：心情 +1稳定感 +1社畜存活率 +120:30｜地铁时间控制成功具体事件： 坐地铁完美达到预期时间。猪疯狂道歉并试图弥补。 之后和陌生人成功拼单双人餐，大米牛奶味道有点怪怪的，但至少不饿了。人物数值变动：心情 +2饱腹 +1队友补救进度 +1社交勇气 +1吐槽： 然后在高铁疯狂拉肚子，但拼单成功属于旅行小成就。6月20日01:00｜抵达徐州具体事件： 凌晨一点到达徐州，非常困。深刻意识到，以后真的不要再坐晚上的高铁了。人物数值变动：心情 -1体力 -3对夜间高铁耐心 -501:20｜高铁门口第一顿，沙县米饭初见端倪具体事件： 在高铁门口吃了第一顿。沙县小吃的米饭竟然这么硬，只能泡在炖蛋里勉强吃一点点。人物数值变动：饱腹 +0.5对徐州米饭疑惑 +1牙齿压力 +1吐槽： 米饭硬到像是在提醒我：旅行不会太轻松。10:00｜起床化妆，自我欣赏时间具体事件： 起床化妆，并认真欣赏自己的妆容。人物数值变动：心情 +10自信 +5今日颜值状态 +5出门意愿 +211:00｜云龙山探索：不南不北，但不算白来具体事件： 去了云龙山。整体感觉不南不北，项羽故居还算有意思。 其他地方有点像开盲盒：你不知道打开的是上海小洋房，还是北京四合院。 考虑到徐州的地理位置，这种混合感又很合理。人物数值变动：心情 +1见识 +1饱腹 -1城市理解度 +112:00｜被地锅鸡价格创到具体事件： 门口的大饭店有点宰客。正常地锅鸡四五十，这里一百三，而且最后还是我买单了。 吃完还开始下雨。人物数值变动：心情 -5钱包 -3饱腹 +1对景区饭店警惕 +5吐槽备注： 地锅鸡吃进嘴里，价格扎进心里。15:00｜云龙山小吃回血，湖上冷风清醒具体事件： 云龙山小吃还可以，人都很好，价格也合理，至少比西湖和上海景区友善。 下午爬山爬了一会儿，觉得上观景台可能也看不到什么，于是去看湖。 说是有划龙舟，但没看到，只看到一个高台，不确定是不是。 湖上很冷，冷到人清醒，但在水上晃着晃着，心情确实放松了一点。人物数值变动：心情 +2饱腹 +2体感温度 -3放松度 +2对景区好感 +118:00｜被徐州交通创飞具体事件： 骑小电驴去找东西吃。这里交通太乱了，感觉人差点被创飞。 本来打算吃贾青烧烤，结果门口乌泱泱都是人，六点去已经没号了。人物数值变动：心情 -1饱腹 -1生命警觉 +3对热门店执念 -118:40｜吃大佬陈，烧烤不咋样，蒜爆鱼强势救场具体事件： 换了附近的大佬陈。烧烤比较接近“串说”，醋有特色，我还买了一瓶，但整体不算特别惊艳。 不过炒菜非常非常好吃，尤其是蒜爆鱼，真的很好吃！！！人物数值变动：心情 +3饱腹 +10对蒜爆鱼好感 +10对烧烤期待 -1旅行满足感 +26月21日10:00｜起不来具体事件： 早上起不来了，昨天淋雨导致身体不太舒服。 点了司机推荐的饣它汤，确实不错，是鸡汤加麦仁，和上海的早餐很不一样。 包子量也离谱，本来以为3块是一个包子的价格，结果是5个。没吃完，因为早上不想太撑。人物数值变动：心情 +1饱腹 +2体力 -1对徐州早餐认知 +2对包子数量判断 -111:00｜宝莲寺具体事件： 到宝莲寺，排队两小时吃到了素面。和上海龙华寺的素面很不一样。 E人开始凑单，排队聊天也挺开心的。人物数值变动：心情 +3饱腹 +1社交能量 +2排队耐力 +1素面图鉴 +113:00｜拜拜祈愿具体事件： 拜拜，祈愿国泰民安、风调雨顺、社会和谐、人民幸福。人物数值变动：心情 +5精神值 +5饱腹 -1功德感 +316:00｜怪味愚把子肉具体事件： 去吃了司机推荐的怪味愚把子肉。味道还可以，肥肉挺特别。 但是吃多了以后有点腻，身体开始发出“差不多得了”的提醒。人物数值变动：心情 +1饱腹 -3油腻负担 +3地方菜图鉴 +1吐槽备注： 好吃是好吃，但肥肉这种东西，快乐和腻只隔着几口。17:00｜进入千禧年代按摩店具体事件： 难受得不行，于是去做了按摩。按摩店很像千禧年代，氛围有点穿越。 按完以后舒服了一点。人物数值变动：心情 +1体力 +1身体舒适度 +2千禧年代既视感 +318:00｜高铁站米线收尾，顺手带速食具体事件： 走之前饿死了，在高铁站吃了米线，还不错。 顺便买了两袋速食，给旅行画上一个很实际的句号。人物数值变动：饱腹 +2心情 +1返程安全感 +1速食库存 +2本次徐州副本结算高光事件：妆容状态极佳，心情直接 +10蒜爆鱼非常好吃，强烈加分宝莲寺排队虽然久，但聊天和素面都算有收获湖上虽然冷，但确实有放松到扣分事件：化妆品没带，开局不利夜间高铁太累，下次慎选景区地锅鸡价格创人徐州交通让人感觉随时进入战斗状态
【魔兽世界】一键导出大版本成就进度-自制插件AchievementExport分享
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}2.0以后好用很多，祝大家早日清完成就，坐骑掉落，幻化毕业，拿满成就奖励！！！使用方法和详细说明：说明文档界面：网盘：通过网盘分享的文件：AchievementExport链接: https://pan.baidu.com/s/1Rd6LDbW6mrW26zqxSziE6w 提取码: Yumi
.NiceMapMarker{width:100%;height:300px}@media (min-width: 1024px) {.NiceMapMarker{height:450px}}记录“第一次真正改 UE 源码”的学习笔记。我不一定完全是对的。纯流水账碎碎念。后续改高度雾等等做了很多更复杂的事情了，但是那些不可能开源，所以想了想还是先分享我的起步吧~本次练习的最小目标是：在 DirectionalLightComponent 的 Details 面板中增加一个布尔开关 UseYumiColorfulLight。当它被勾选时，这盏平行光在 PC Deferred Renderer 路径下可以蹦迪。我选择切入的方向我其实搜了很多教程，每个平台都尽可能找到相关搜索。很多 UE 源码教程会从“如何下载源码、生成项目文件、编译 Editor”开始，但编译成功之后，下一步呢？源码这么大，我到底该改哪里？这一块我之前学AngelScript的时候已经很熟了，感觉是冗余信息我觉得最合理的方式是先做一个最小闭环：只让一条数据链跑通，只在一个渲染路径里验证成功。所以这次的范围应该明确收窄：项目本次是否处理原因Directional Light是目标明确，容易观察结果PC Deferred Renderer是UE 桌面端常见路径，适合作为第一次练习（最近在移动端导出踩坑，累死我了）Details 面板普通属性是用 UPROPERTY 即可，不需要先写复杂 Detail CustomizationMobile Renderer否移动端光照路径不同，容易扩大范围Forward Renderer否Forward 与 Deferred 的灯光组织方式不同Path Tracing否路径完全不同，不适合作为第一次练习，而且暂时没有项目需要Static baked lighting否静态烘焙涉及 Lightmass / 烘焙数据，不适合最小闭环第一次练习的验收标准应该非常简单：勾选 UseYumiColorfulLight，平行光开始蹦迪。只要这个闭环成立，已经摸到了 UE 源码修改里最重要的一条路：编辑器属性 → Runtime 组件 → SceneProxy → Shader 参数 → HLSL 着色。UE 源码的基础地图在改源码之前，最重要的不是背函数名，而是先知道“功能大概属于哪一层”。UE 源码非常大，如果没有层级地图，搜索出来一堆同名文件时会很容易迷路。可以先用下面这张简化地图理解 UE 源码结构：UnrealEngine/ ├─ Engine/ │ ├─ Source/ │ │ ├─ Runtime/ 游戏运行时核心代码 │ │ ├─ Editor/ 编辑器相关代码 │ │ ├─ Developer/ 开发工具、调试工具 │ │ ├─ Programs/ UBT、UHT、UnrealPak 等独立工具 │ │ └─ ThirdParty/ 第三方库 │ │ │ ├─ Plugins/ 引擎插件 │ ├─ Content/ 引擎资源 │ └─ Config/ 引擎配置 │ ├─ Samples/ ├─ Templates/ └─ FeaturePacks/对源码学习来说，最核心的位置是：Engine/Source/以后查 Actor、Component、Renderer、RHI、Editor 工具、构建系统，大部分时间都会在这里面转。UE 源码的七个基础层级1. 构建系统：决定“什么会被编译”这里在我之前记录自己添加函数的时候有项目说明，包括link的官方文档。这里不展开细说了。UE 的 C++ 构建主要依赖 UnrealBuildTool，也就是 UBT。可以先这样记：.Target.cs = 我要编译什么目标？例如 Editor、Game、Server .Build.cs = 这个模块依赖哪些模块？ .h / .cpp = 真实代码一个普通模块大概长这样：MyModule/ ├─ MyModule.Build.cs ├─ Public/ │ └─ MyClass.h └─ Private/ └─ MyClass.cppBuild.cs 很重要，因为 UE 是模块化编译。如果在一个模块里调用了另一个模块的类型，但没有在 Build.cs 里声明依赖，就可能出现编译错误、链接错误，或者 IntelliSense 能找到但实际编不过的情况。不过本次练习主要是修改 UE 已有模块，不是新建模块，所以 Build.cs 不是重点。只需要先知道：UE 编译不是普通 C++ 工程直觉，它背后有自己的模块和构建规则。2. 模块系统：UE 不是一个巨大的单体工程UE 源码不是所有代码混在一起，而是按模块组织。常见模块包括：Core CoreUObject Engine RenderCore Renderer RHI Slate UnrealEd Landscape Niagara UMG改源码时，本质上大多数情况是在改某个模块里的某个类。例如要看灯光、雾、大气、材质、组件，通常会碰到：Engine/Source/Runtime/Engine/ Engine/Source/Runtime/Renderer/ Engine/Source/Runtime/RenderCore/ Engine/Source/Runtime/RHI/ Engine/Shaders/如果做编辑器工具、Details 面板定制、菜单、资产编辑器，则更可能进入：Engine/Source/Editor/ Engine/Source/Developer/ Engine/Plugins/Editor/所以看到一个需求时，先判断它属于哪个模块方向,搜索会快一点。效率就高了。我电脑不是很好，这对我来说很重要了。3. Runtime 和 Editor 分层：先判断功能在哪个世界生效这是初学 UE 源码时非常关键的一层。Runtime = 游戏运行时也需要的代码 Editor = 只有编辑器里才需要的代码例如这些通常偏 Runtime：AActor UObject UStaticMeshComponent UMaterial ULightComponent UDirectionalLightComponent而这些通常偏 Editor：Details 面板 Asset Editor Content Browser 蓝图编辑器 材质编辑器界面 编辑器菜单栏本次练习同时碰到了两边：Details 面板显示一个 bool，属于编辑器可见的属性表现。这个 bool 对最终光照产生影响，属于 Runtime / Renderer / Shader 逻辑。这里有一个重要判断：如果只是让普通属性显示在 Details 面板里，通常不需要改 Editor/DetailCustomizations。只要属性是 UPROPERTY(EditAnywhere...)，UE 的 Details 面板就会自动显示。只有当需要自定义复杂 UI，例如自定义按钮、特殊折叠组、联动控件、搜索逻辑、动态图表，才更可能进入 Detail Customization。4. UObject 反射系统：为什么 UPROPERTY 会出现在 Details 面板UE 和普通 C++ 最大的区别之一，是 UObject 反射系统。普通 C++ 可能只是：class MyClass { public: float Value; };UE 里经常会写成：UCLASS() class UMyObject : public UObject { GENERATED_BODY() public: UPROPERTY(EditAnywhere, BlueprintReadWrite) float Value; UFUNCTION(BlueprintCallable) void DoSomething(); };这些宏不是装饰品。UCLASS、UPROPERTY、UFUNCTION 会被 UHT，也就是 Unreal Header Tool 解析并生成额外代码。可以粗暴理解为：UObject 系统 = 反射 + 序列化 + 蓝图暴露 + GC 垃圾回收 + 编辑器属性面板所以当给 UDirectionalLightComponent 增加一个 UPROPERTY(EditAnywhere...) 时，它会被 UE 的反射系统识别，并自动显示到 Details 面板里。但是注意：Details 面板能显示，只代表这个值存在于 UObject / Component 上，不代表它已经进入渲染线程，更不代表 Shader 能读到。这就是本次练习最核心的地方。5. Gameplay 框架：World、Actor、Component 的关系如果从游戏对象角度看，UE 常见关系可以简化成：UGameInstance ↓ UWorld ↓ ULevel ↓ AActor ↓ UActorComponent / USceneComponent / UPrimitiveComponent常见对象关系是：World 里有很多 Actor Actor 身上挂很多 Component Component 负责具体能力例如一个角色可能是：ACharacter ├─ CapsuleComponent ├─ SkeletalMeshComponent ├─ CharacterMovementComponent └─ Camera / SpringArm灯光也类似。在场景里看到的 Directional Light Actor，本质上会包含一个 UDirectionalLightComponent。真正与灯光参数、方向、颜色、强度相关的核心数据，一般都在 Component 上。所以本次功能的数据起点应该选在：UDirectionalLightComponent而不是直接从某个 Editor UI 文件开始。6. 渲染框架：从组件到 GPU 不是一步到位UE 的渲染可以先按下面这个层级理解：层级作用常见路径Engine 层场景对象、Actor、Component、灯光、材质等高层数据Engine/Source/Runtime/Engine/Renderer 层组织具体渲染流程，例如 Deferred Lighting、Fog、Post ProcessEngine/Source/Runtime/Renderer/RenderCore 层渲染基础设施、Shader 参数、Uniform Buffer 等Engine/Source/Runtime/RenderCore/RHI 层抽象 DX12 / Vulkan / Metal 等图形 APIEngine/Source/Runtime/RHI/Shader 层.ush / .usf 文件，最终 GPU 执行的 HLSLEngine/Shaders/如果之后要改雾、大气、后处理、灯光、shader 参数，这几个位置会反复出现。本次练习会跨过多个层级：UDirectionalLightComponent Engine 层，游戏线程 / 编辑器对象 FDirectionalLightSceneProxy 渲染线程代理 FLightRenderParameters C++ 渲染参数结构 DeferredLightUniforms Deferred 光照 shader 参数 FDeferredLightData HLSL 光照数据结构 DeferredLightPixelShaders.usf 最终像素着色逻辑这也是为什么“只是给平行光加了一个功能”，却会改多个文件。7. 插件系统：插件也是模块，但更适合独立发布插件可以理解为带 .uplugin 描述文件的一组模块和资源。它可以独立启用、禁用、打包和发布。一个插件大概是：MyPlugin/ ├─ MyPlugin.uplugin ├─ Source/ │ └─ MyPlugin/ │ ├─ MyPlugin.Build.cs │ ├─ Public/ │ └─ Private/ ├─ Content/ └─ Resources/本次练习是直接修改引擎源码，不是写插件。它更适合训练“引擎内部数据链路”。如果未来要做可发布工具，则要再判断功能能不能做成插件。有些功能适合插件，例如编辑器工具、资产处理、材质节点、蓝图节点、后处理材质模板。有些功能如果需要改 Renderer 内部 shader 参数和引擎结构体，可能就必须走源码修改，或者需要更复杂的插件扩展点。开始练习这次需求可以写成一句话：在 UDirectionalLightComponent 上添加一个可勾选的 bool，当它开启时，把这盏 Directional Light 的最终光照像吃了菌子一样出现幻彩。但从源码角度，它真正包含三件事：属性暴露：让 Details 面板能看到并编辑这个 bool。数据传递：把 Component 上的 bool 传到渲染线程，再传到 shader 参数。最终着色：在 Deferred Light Pixel Shader 中根据开关修改 LightData.Color。核心链路如下：Details Bool ↓ UDirectionalLightComponent ↓ FDirectionalLightSceneProxy / FLightSceneProxy ↓ FLightRenderParameters / FLightShaderParameters ↓ DeferredLightUniforms ↓ FDeferredLightData ↓ DeferredLightPixelShaders.usf ↓ Red Light这条链路非常重要。以后做很多引擎级渲染功能时，本质也是类似模式：美术可调参数 → C++ 组件 / 资源 → 渲染线程结构 → Shader 参数 → HLSL 实现第一步：让 Details 面板显示开关最开始可以在 DirectionalLightComponent.h 中声明一个属性。示例写法：// [Yumi] Expose a minimal test switch for the Directional Light source modification exercise. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light", meta=(DisplayName="UseYumiColorfulLight")) uint32 bUseYumiColorfulLight : 1;这一步的作用是：让编辑器 Details 面板出现一个可勾选的属性。但是这一步只负责“看得见、能勾选”。它不会自动影响渲染。可以把它理解成：在遥控器上加了一个按钮，但按钮还没有接到电视机内部电路里。所以如果只加了这个属性，然后进入场景勾选它，画面没有任何变化，这是正常的。为什么 Details 的值不能直接被 Shader 读取UE 的编辑器对象、游戏线程对象、渲染线程对象和 GPU Shader 不是同一个世界。简化理解如下：世界代表对象特点Editor / Game ThreadUDirectionalLightComponentUObject，可被 Details 面板编辑，可序列化Render ThreadFDirectionalLightSceneProxy / FLightSceneProxy渲染线程使用的轻量副本，不能随便读 UObjectShader ParameterUniform / Structured 参数CPU 准备好后传给 GPUGPU Shader.usf / .ush HLSL最终执行着色逻辑Shader 不会直接去读 UDirectionalLightComponent::bUseYumiColorfulLight。原因包括：UObject 在游戏线程 / 编辑器世界，Shader 在 GPU 世界。渲染线程通常使用 SceneProxy 保存渲染所需数据，避免直接访问 UObject。CPU 和 GPU 之间要通过明确的 shader 参数结构传值。HLSL 侧需要有对应字段，否则即使 C++ 有数据，shader 也不知道怎么读。所以这次练习我觉得最烦的就是把这个 bool 一层一层传下去。这里可以添加自己想要的，和之前一样，先声明有这玩意。先不写内容，只是显示出来debug一下这个声明是不是加对了吧..good，可以显示！第二步：实现串通文件不需要一上来就做那么复杂，可以只实现true的时候变红，代表联通了！关键文件职责总览具体文件名可能会因 UE 小版本和源码分支略有差异，所以不要死记行号。更可靠的方法是用符号搜索定位：UDirectionalLightComponent FDirectionalLightSceneProxy FLightSceneProxy FLightRenderParameters FLightShaderParameters DeferredLightUniforms FDeferredLightData SetupLightDataForStandardDeferred DeferredLightPixelShaders.usf DeferredLightingCommon.ush LightData.ush LightDataUniforms.ush本次修改大概率会涉及这些层：层级可能文件职责Component 属性层DirectionalLightComponent.h声明 Details 面板可见的 boolComponent 刷新层DirectionalLightComponent.cpp当属性变化时刷新渲染状态SceneProxy 层LightComponent.cpp / 相关 Light SceneProxy 定义把 Component 数据复制到渲染线程代理C++ Shader 参数层SceneManagement.h / LightRendering.cpp增加并填充传给 shader 的参数Shader 参数声明层LightShaderParameters.ush 等让 HLSL 侧知道参数存在Deferred Light 数据层LightData.ush / LightDataUniforms.ush / DeferredLightingCommon.ush把 uniform 数据转成像素着色时使用的 FDeferredLightData最终着色层DeferredLightPixelShaders.usf根据开关修改 LightData.Color不需要迷信“必须改这几个精确文件”。正确思路是：数据链路经过哪里，就在哪里补字段；最终在哪里生效，就在哪里写逻辑。关键改动 1：Component 保存 Details 面板的开关位置通常在：Engine/Source/Runtime/Engine/Classes/Components/DirectionalLightComponent.h或者所在版本中 UDirectionalLightComponent 的实际定义位置。示例代码// [Yumi] Expose a minimal render test toggle on Directional Light. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Light", meta=(DisplayName="UseYumiColorfulLight")) uint32 bUseYumiColorfulLight : 1;它读取了什么它不读取别人，它是数据源。美术或者开发者在 Details 面板中勾选这个属性，值会保存在 UDirectionalLightComponent 上。它传给谁下一步需要在创建 SceneProxy 时，把它复制给渲染线程使用的 proxy。因为这是整个数据链的起点。如果 Component 上没有这个属性，Details 面板没有输入入口，后面的 shader 参数就没有来源。关键改动 2：从 Component 复制到 SceneProxy为什么需要 SceneProxyUE 渲染时不会让渲染线程随便读取 UObject。对于场景物体、灯光、Primitive 等，UE 通常会创建对应的 SceneProxy。可以这样理解：UDirectionalLightComponent = 游戏线程 / 编辑器中的真实对象 FDirectionalLightSceneProxy = 渲染线程使用的影子副本如果只把 bool 放在 UDirectionalLightComponent 上，而没有复制到 SceneProxy，渲染线程就不知道这个开关存在。示例代码在 Directional Light 对应 SceneProxy 中增加成员：// [Yumi] Store the Details toggle on the render-thread light proxy. uint32 bUseYumiColorfulLight : 1;构造 proxy 时从 Component 复制：// [Yumi] Copy the component value when building the render-thread proxy. bUseYumiColorfulLight(Component->bUseYumiColorfulLight)它读取了什么读取：Component->bUseYumiColorfulLight它传给谁传给：FDirectionalLightSceneProxy / FLightSceneProxy也就是渲染线程使用的数据副本。为什么必须存在因为渲染线程和游戏线程是分开的。不能指望 shader 参数填充阶段直接去读 Details 面板上的 UObject 属性。关键改动 3：Details 修改后刷新 SceneProxy如果在 Details 面板里勾选了开关，但画面没有立刻变化，很可能是旧的 SceneProxy 还在被使用。这时需要在属性变化时标记渲染状态需要刷新：#if WITH_EDITOR void UDirectionalLightComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { const FName PropertyName = PropertyChangedEvent.Property ? PropertyChangedEvent.Property->GetFName() : NAME_None; Super::PostEditChangeProperty(PropertyChangedEvent); if (PropertyName == GET_MEMBER_NAME_CHECKED(UDirectionalLightComponent, bUseYumiColorfulLight)) { // [Yumi] Rebuild the scene proxy so the Details checkbox reaches the render thread immediately. MarkRenderStateDirty(); } } #endif注意：真实源码中可能已经有 PostEditChangeProperty。如果已有，不要重复定义一个同名函数，而是合并到已有逻辑里。它读取了什么读取当前变化的属性名：PropertyChangedEvent.Property->GetFName()判断是不是：bUseYumiColorfulLight它传给谁它不是直接传值，而是通知引擎：这个组件的渲染状态脏了，需要重建 / 更新渲染代理。为什么必须存在如果不调用 MarkRenderStateDirty()，可能出现：Details 面板值已经变了；Component 上的 bool 已经是新值；但渲染线程仍然使用旧 SceneProxy；shader 参数仍然是旧值。所以它是保证编辑器中实时反馈的重要刷新点。关键改动 4：把开关写入 C++ 渲染参数SceneProxy 有了 bool 之后，还要把它写入真正传给 shader 的灯光参数结构。通常会涉及类似：FLightRenderParameters FLightShaderParameters DeferredLightUniforms这里的名字可能因版本略有差异，但职责是类似的：它们都是 C++ 到 shader 之间的参数搬运站。为什么建议用 float，而不是 boolShader 参数里建议用：float YumiColorfulLightEnabled;而不是：bool bYumiColorfulLightEnabled;原因是 shader 参数涉及 CPU / GPU 数据布局、对齐和跨语言传递。float 或 uint 更稳定。很多渲染代码里也会用：0.0 = 关闭 1.0 = 开启来表达开关。示例代码在渲染参数结构中增加字段：// [Yumi] Pass the Directional Light test toggle to the deferred light shader. float YumiColorfulLightEnabled = 0.0f;填充参数时：// [Yumi] Directional lights are the only proxy type that can enable this shader test. LightParameters.YumiColorfulLightEnabled = bUseYumiColorfulLight ? 1.0f : 0.0f;其他灯光类型保持默认：// [Yumi] Other light proxy types keep the default 0.0 value.它读取了什么读取 SceneProxy 上保存的：bUseYumiColorfulLight它传给谁传给渲染参数结构，例如：FLightRenderParameters FLightShaderParameters DeferredLightUniforms为什么必须存在SceneProxy 上有值，不代表 shader 参数里有值。需要明确把它放进 shader 参数结构，否则后面的 HLSL 仍然无法读取。关键改动 5：把参数接到 HLSL 的 LightDataC++ 侧填了 shader 参数之后，HLSL 侧也必须有对应字段。这通常会涉及 .ush 文件。可以理解为：.cpp / .h = CPU 侧知道这个参数 .ush / .usf = GPU Shader 侧知道这个参数如果 C++ 侧加了字段，但 HLSL 侧没同步，可能会出现：shader 编译报错；参数结构不匹配；读不到字段；读到错位数据；功能没有效果。示例：在 FDeferredLightData 中增加字段struct FDeferredLightData { // existing fields... // [Yumi] 1.0 means this deferred directional light should be forced to red for the test. float YumiColorfulLightEnabled; };示例：从 uniform 拷贝到 LightData// [Yumi] Copy the deferred uniform value into the light data used by the pixel shader. Out.YumiColorfulLightEnabled = DeferredLightUniforms.YumiColorfulLightEnabled;它读取了什么读取 shader uniform 中的：DeferredLightUniforms.YumiColorfulLightEnabled它传给谁传给像素着色逻辑使用的：FDeferredLightData LightData为什么必须存在Deferred Light Pixel Shader 最后通常操作的是 LightData，不是最早的 C++ Component，也不是原始 uniform。中间这层如果不接上，数据链就会断。关键改动 6：在 Pixel Shader 中把灯变红最终真正改画面的地方通常在：DeferredLightPixelShaders.usf这里才是“功能生效”的地方。建议写法// [Yumi] Deferred pixel shader helper for the Directional Light test toggle. void ApplyYumiColorfulLight(inout FDeferredLightData LightData) { BRANCH if (!LightData.bRadialLight && LightData.YumiColorfulLightEnabled > 0.5f) { const float LightEnergy = max(max(LightData.Color.r, LightData.Color.g), LightData.Color.b); LightData.Color = float3(LightEnergy, 0.0f, 0.0f); } }然后在合适位置调用：// [Yumi] Apply after LightData is prepared and before lighting evaluation uses LightData.Color. ApplyYumiColorfulLight(LightData);为什么不直接写死 float3(1, 0, 0)如果直接写：LightData.Color = float3(1, 0, 0);会丢掉原始光强。这样不管原来的灯强还是弱，最后都变成同一个红色强度，测试结果会不够合理。更好的做法是保留原来的能量，只改色相：const float LightEnergy = max(max(LightData.Color.r, LightData.Color.g), LightData.Color.b); LightData.Color = float3(LightEnergy, 0.0f, 0.0f);这样效果是：原来很亮的光 → 很亮的红光 原来很弱的光 → 很弱的红光这比写死一个颜色更符合“测试开关只改变颜色，不破坏强度关系”的原则。为什么要判断 !LightData.bRadialLightPoint Light、Spot Light、Rect Light 等局部灯通常属于 radial / local light。Directional Light 不是 radial light。所以加上：!LightData.bRadialLight是为了避免这个测试逻辑影响其他灯光类型。不过更严谨地说，具体判断方式要以所在版本的 FDeferredLightData 字段为准。如果源码里有更明确的 light type 标识，也可以用更精确的判断。完整数据链复盘本次功能的数据链可以这样复盘：1. 用户在 Details 面板勾选 UseYumiColorfulLight ↓ 2. UDirectionalLightComponent::bUseYumiColorfulLight 保存这个值 ↓ 3. 创建 / 刷新 FDirectionalLightSceneProxy 时，把 Component 上的 bool 复制过去 ↓ 4. SceneProxy 填充 FLightRenderParameters / FLightShaderParameters ↓ 5. Deferred Light 渲染流程把参数写入 DeferredLightUniforms ↓ 6. HLSL 把 DeferredLightUniforms 拷贝进 FDeferredLightData ↓ 7. DeferredLightPixelShaders.usf 读取 LightData.YumiColorfulLightEnabled ↓ 8. 如果开启，就把 LightData.Color 改成红色，同时保留原光强这条链路里，任何一个节点漏掉，最终效果都可能不生效。常见断点如下：漏掉的位置可能现象只加了 UPROPERTYDetails 能勾选，但画面完全不变没复制到 SceneProxyComponent 有新值，但渲染线程不知道没调用 MarkRenderStateDirty()勾选后不刷新，可能重启或重新加载后才变化C++ shader 参数没加字段HLSL 收不到值，或者编译报错HLSL 结构没同步shader 编译失败或数据错位没拷贝到 FDeferredLightDatauniform 有值，但最终 pixel shader 的 LightData 没值Pixel Shader 调用位置不对代码写了，但实际光照计算没有用到修改后的颜色判断条件太宽其他灯光也被影响判断条件太窄Directional Light 没被命中第三步：吃菌子吧！知道在那实现以后就可以玩点有意思的了哈哈哈哈！后话在我的文章里骂了很多次vibe coding ，终于在一个评论里看到了我想表达的嘴替。就是这个意思。且不说需求能不能100%的理解，很多时候出现了偏差来排查是最讨厌的。不如自己干来的快..