Releases: A8Chann/dsh-pet-live2d
Release list
v3.0.0
3.0.0
桌面端正式随插件一起发布。 装完插件、在设置里选「桌面」,她就在桌面上——
不用去下别的任何东西(Windows x64 自带平台二进制;其它平台装插件时会被 npm 静默跳过,
插件照常工作,只是"桌面"那一项不可用)。
新:桌面端二进制随包分发
dsh-pet-live2d-desktop-win32-x64 作为可选依赖随主包安装。以前它是悬空的
(那个包在 npm 上根本不存在),所以桌面端只能靠设置里那个「下载桌面端」按钮现拉一份;
现在装完就有,下载那条路保留作为兜底。
修:她固定在自己那块屏上
用户报的:"宠物应该是在固定位置,现在我鼠标在不同屏幕上宠物居然会跟随我的鼠标所在的屏幕。"
以前窗口每帧跟着光标换屏。现在:启动时落到光标所在屏,之后由你决定——
托盘菜单里有「屏幕」子菜单,列出每块屏的分辨率与位置,当前那块打点。
指针移到别的屏,她待在原地、只是视线贴到屏幕边缘。
修:只在焦点在她身上时才跟随鼠标
用户报的:"只有焦点在宠物上才有跟随。" 桌面端原来靠 pointermove,而那个事件只在指针
落在她窗口上时才会送到页面 —— 你在别的程序里打字时,她的视线是死的。
现在由壳每 33ms 读一次全局光标喂给页面,所以不管焦点在哪她都跟着你。
跟随范围也改成一道正圆距离曲线(一个半径说了算):范围内完全跟随,
超出后平滑收回、并回到她自己的正前方(原来是方形硬边界 + 到边缘就侧着一直盯着)。
修:托盘菜单四项都失效
用户报的四条,三条是同一个根因:壳发的窗口事件根本到不了页面。
| 你看到的 | 真相 |
|---|---|
| 「设置…」点了没效果 | Tauri 的 window.emit() 不发 DOM 事件、只走 IPC,而页面里没有 window.__TAURI__ |
| 「归位」点了没反应 | 同上 |
| 「藏起来」过一会儿又自己出来 | 没记住"是用户藏的",显示循环一秒后又把她显示出来了 |
| 「屏幕」里选中的圆点不更新 | 菜单是启动时建好就定住的 |
现在给页面送动作走命令队列(HTTP,页面每 33ms 取一次),用户的显隐意图单独记着,
菜单每次状态变化重建。
修:右下角那片"空白却摸得到"的区域
用户报的:"触摸区域在右下角有一大片奇怪的可触摸区域,那里明明什么都没有。"
尾巴那一层原来是几何并集(包围盒 128×162px,占舞台 43%×54%),而"算不算落在她身上"
用的是栅格(还带一格膨胀)。两套几何不一致,差集就是你摸得到、判定却不认的那片空白。
现在尾巴也按同一套栅格重采(实测那片区域 14 格 → 0 格)。
修:摸头/摸尾巴不再打断你正在看的东西
用户报的:"摸头和摸尾巴不要还原当前动作啊,随机到哪一个插槽里的就放哪一个插槽里的就行了。"
以前摸一下头,反应结束时会把身体交还待机 —— 你刚选好的「掏出手机」就没了。
现在反应优先走"只换随机到的那个槽位",动作类的演完会接回原来的动作。
另外反应留下来的表情/情绪过 12 秒自己收回默认(摸三下头,最后一下之后才开始数)。
修:「自拍」右手不抬
用户报的:"动作里的自拍动画会动,但装扮里的不会动。" 而且一直是右手没抬起来。
查下来是"保姿势录像"每帧把当前动作的曲线压住了:自拍的抬手曲线(phone5)在跑
(实测涨到 9.7),但「掏出手机」定格时录下的那一帧里它是 0,而那一帧每帧被写回去 ——
写在动作之后,曲线就白跑了。现在当前动作自己写的参数不再被回放覆盖。
修:重锤出击前先清掉冲突的表情
用户要求:"重锤出击动作应该判断一下当前眼部是不是 晕晕/呆呆眼,情绪是不是 开心兴奋/闭眼口水,
如果是应该先把眼部或情绪还原为默认。"
挥锤是清醒有力的动作,而 Hammer 只写手臂参数、脸一概不管 —— 眼部停在「晕晕」、
情绪停在「闭眼口水」时挥锤,画面上就是"一个晕乎乎、闭着眼流口水的人在奋力挥锤"。
现在开演前只清列出来的那几个选项(你选的其它眼睛不会被这一锤抹掉),清单可在设置里改。
修:桌面端的设置与 DSH 里的设置不同步
用户报的:"桌面的设置与 dsh 里的设置没有同步。"
根因是两边根本不在同一个 localStorage 里:桌面端页面是
http://127.0.0.1:<随机端口>,DSH 是 http://127.0.0.1:3080,浏览器按 origin 隔离。
现在三类跨窗口该一致的设置(手感滑杆 / 相位池子与开关 / 装扮)放在
%DSH_HOME%\pet-settings.json,两个界面都读写它,3 秒内互相跟上。
位置与大小仍然各窗口各一份(她贴屏幕右下角还是贴面板右下角,本来就是两件事)。
v2.3.4
2.3.4
修:摸尾巴"很难点到" —— 尾巴的判定区域和可点区域都收紧了
用户的原话:"尾巴一直在摆动,摸尾巴的事件现在很难点到。"查下来是两件事叠在一起,
而且第二件才是主因。
① 尾巴判定把 11 个"可换配件"也算成了尾巴。
hitsTail 原来按名字挑部件(名字里带 尾|鳍|翅|翼),这只模型里有 15 个部件 / 16 块
drawable 命中 —— 但其中 11 块是可选配件(狐狸尾 / 猫尾 / 狼尾 / 天使翅膀 / 恶魔翅膀…),
几何一直留在原地、横跨从头顶到腰腹的整个角色。实测(24×24 网格):
| 修之前 | 修之后 | |
|---|---|---|
| "算尾巴"的格子占角色 | 22.0% | 5.2% |
| 其中同时算头(会被摸头优先吃掉) | 86.6% | ~55% |
| 只算尾巴、真正可点的格子 | ~17 | ~59 |
于是你点在看得见的尾鳍上,拿到的是摸头反应(重锤出击)。
现在按贴图收窄:texture_00.png 上鲸鱼尾鳍只占 x 3..444, y 953..1397 那一块,
落在那里的正好是 ArtMesh38/56/58/59/60 五块(全属于部件 尾巴(蒙皮))。UV 读不到时
不做收窄,退回旧行为 —— "更精确"不能变成"整个失效"。
② 可点区域(轮廓快照)跟不上摆动。
摸头/摸尾巴的判定每次点击都现读几何,所以"点的是不是尾巴"是准的;但决定事件归谁
的那层 clip-path 轮廓是开机从画布抓一次的静态快照。尾鳍一直在摆,摆出快照的那
一瞬间,点击直接穿透到页面(实测 elementFromPoint 返回 HTML,而判定说她是)。
现在尾巴有一块跟着摆动实时更新的可点矩形(每 ~120ms,模型空间 → 舞台按 y 轴方向
换算),并进 [data-hit] 的 clip-path;JS 侧的 hitsMask 用同一套(当前尾巴盒子
或静态快照,取或)—— 两边必须一致,否则就是"事件进得来、判定说不在她身上"这种点了没反应。
顺序也踩过坑:hitsMask 一度改成纯几何,结果"只有几何、没有像素"的点被算成在她身上,
而 DOM 并不覆盖它 —— cdp-passthrough 立刻红。
顺带:onModel("算不算落在她身上")以前只认静态快照,现在也认尾巴当前盒子。
诊断:一批新读口
lastPress()—— 按住那一刻路由用的三个答案(onModel/onHead/onTail)+ 时间戳。
"点了没反应"要先分清是事件没进来、还是判定说不是她;从"她说了什么"倒推是分不出来的。tailDebug()—— 尾巴判定的收窄结果(收窄前后各是哪些、贴在哪块贴图上)与尾巴实时盒子。hitsMaskStatic()—— 只按静态快照判定。要证明"这一点在快照外、却在宽限之内",
就得能把两层分开读。uvRaw()/uvTables()—— 贴图坐标表本身(哪块 drawable 画在贴图的哪一块)。modelToStage()—— 模型空间 → 舞台坐标,尾巴那块实时矩形靠它换算。
v2.3.3
2.3.3
修:互动反应补上内置默认值 —— 宠物没声明那三组也演得出来
「摸头 / 摸尾巴 / 转圈」演什么,之前只认宠物 pet.json 里的 patReactions /
tailReactions / spinReactions,插件代码里一个默认值都没有。于是自己做的宠物
(最小 pet.json 里根本没有这三个键)上:
| 互动 | 以前 | 现在(没声明时) |
|---|---|---|
| 摸头 | 只有台词,不演重锤出击 | 重锤出击 / 问号 / 星星眼 随机一个 |
| 摸尾巴 | 只有台词 | 吐魂 / 问号 |
| 转圈转晕 | 演不出来 | 晕晕 |
兜底顺序和台词、摸鱼池是同一套:用户改过的 <- 宠物声明的 <- 内置默认。
宠物自己声明了就一直用宠物那份(这只鲸鱼娘一字未变),只是"没写"不再等于"没反应"。
设置页「互动」卡里那三组候选现在显示的就是有效那一组(和运行时会演的完全一致),
行尾会标明来历:(宠物默认) / (内置默认) / (已改过) —— 看到的是内置值时
你就知道"改宠物也能改这一组"。
修:随包宠物的默认值终于能到老用户手里(原来装过一次就再也不会更新)
installBundledPets() 以前是"目标目录存在就跳过"。插件目录随 dsh plugin add / npm
更新,用户的宠物目录 %DSH_HOME%\pets\<id>\ 不会 —— 于是宠物自己的默认值永远停在
装它的那一天:2.3.0 往 pet.json 里加的那批(台词 / 互动反应候选 / 摸鱼槽位 /
asking·helper·queued 三个相位、自拍等三个槽位)一个都没到过桌面。
症状和你这次报的一模一样,而且看起来完全像插件的 bug:互动只有台词不演反应、
相位台词一句不弹、装扮里少三个槽位、会话相位 列表里少三个状态。
现在按内容判定,三条:
| 宠物目录里那份 | 行为 |
|---|---|
| 不存在 | 装一份,并把版本与内容指纹记进 pets/.synced.json |
| 就是我们装下去的那一份(指纹逐字节相同) | 整份更新到随包版本 |
| 你自己改过(指纹对不上) | 一个字都不碰(日志里会说一句"跳过更新") |
冷启动(同步记录出现之前装的副本,也就是所有老装机)靠一张历史内容指纹表认人:
与历史上任何一次随包分发逐字节相同才升级 —— 改过名字、槽位、任何一个字,哈希就不同,
所以"用户自己改过的宠物永远优先"这条没有被削弱。
升级会在宿主日志里说清楚:
[live2d-pet] ds-whale-girl:宠物默认值更新到 1.1.0(原先 1.0.1)
顺带把随包宠物的版本抬到 1.1.0。升级判定只看内容指纹、不看版本号 ——
同一个版本号下补默认值也能发出去(这次就是)。
修:互动候选「改不动」的隐患
PHASE_OVERRIDES(设置覆盖的存放处)初始化时漏了 interactions 这个键,而
applyOverride() 是直接往这个对象上写的 —— 也就是说第一次点候选时覆盖会被静默丢掉。
读的地方都写了 ?.,所以它一直"读得出来、写不进去",只有正面写它的设置界面会踩到。
现在五个键(phases / fidget / relations / interactions / lines)都在初始化时给出来,
并加了一条断言盯着"点一下候选 → 覆盖真的落盘"。
诊断:新增 reactionDiagnostics()
三组候选各自的来历(user / pet / builtin / none)、内置默认值、以及最终生效值。
"点了她没反应"这类问题一眼就能分出是"用户覆盖成了空数组"、"宠物没声明"还是"连兜底都没了"
—— 只看 effectiveReactions() 是分不出来的。
2.3.2
2.3.2
修:摸尾巴又摸不到了(这次是真的)
用户报的:"现在又摸不到尾巴了。"(还发了一张圈出尾巴位置的截图,帮了大忙 —— 尾巴是
右侧那只鲸鱼尾鳍,不是翅膀。)
真正的根因:判定每块 drawable 时用的是映射时算好并存下来的下标(entry.index),
而不是调用时按 id 现查的下标。这两者只要有一处对不上,取到的顶点就是别人的 ——
表现是"每块单独测都能命中,聚合起来一个都不中"。
单块测(drawableProbe)之所以一直正常,正是因为它每次都按 id 现查下标。
现在聚合路径也这么做了:hitsPartsGeometry 在循环里用 im.getDrawableIndex(entry.id)
重新解一次,解不出来才退回缓存值。
修好后同一网格上的读数:hitsTail 从 0 → 509 命中(48×48),头部 1113、重叠 689
—— 重叠仍然存在(翅膀的几何本来就伸到头顶上方),所以摸头优先于摸尾巴那条继续保留:
点头部给摸头,尾巴自己的区域照样摸得到。
一段该记下来的教训:坏掉的诊断比没有诊断更糟
这轮我查了很久,很大一部分时间是被自己写的诊断函数骗了:partHitCounts 里调了
api.drawableProbe(...),而 api 是控制器返回出去的对象,控制器内部根本看不到它
—— 每次调用都抛 api is not defined,被外面的 try/catch 吞掉,于是 hitsAll/hitsVisible
恒为 0。外面看到的却是"这个部件完全没有几何",一条看起来像产品坏了的读数。
代价:我据此排除了三个正确假设(透明度过滤、下标空间、包围盒预筛),还改坏过一次
(把顶点整条换成 core 层,连头部判定都变成 0 命中,已还原)。
教训:诊断函数自己也要先自证(拿一个已知能命中的样本去测它),读数与事实矛盾时
先怀疑测量工具,再怀疑被测对象。
2.3.1
2.3.1
修:摸头会出摸尾巴的效果
用户报的:"现在摸头也是出现的摸尾巴的效果,而且这是 live2d 会动的,这个判断网格似乎是静态的。"
两个原因叠在一起:
- 这只宠物的尾巴/翅膀是三选一的可选配件(狐狸尾 / 猫尾 / 狼尾 / 天使翅膀…),
同一时刻只有一个显形,其余几何还留在原地 → 判定区域会重叠。实测 1112 个头部
命中点里有 697 个同时命中尾巴。 - 点击路由原来先判尾巴、再判头 → 重叠区的点击一律算摸尾巴。
现在摸头优先于摸尾巴:点头部一定给摸头的台词/反应,只有"没命中头部"才看尾巴。
顺带把一条太弱的测试补强:原来只断言"存在只命中尾巴的点"(在 697 点重叠下依然是绿的),
现在断的是路由(点头部给的必须是摸头的台词)—— 这才是用户实际遇到的问题。
关于"判断网格似乎是静态的"
查证结果(都有读数支撑):
- 头部判定是跟着动的:同一网格隔 2.5 秒重采,命中格子会变(模型呼吸/摆动)。
每次点击都是现读当前帧的顶点,不是缓存的网格。 - 尾巴/翅膀的几何在没戴配件时是退化的(缩成一点 → 被面积下限排掉),所以
"摸尾巴"在没有尾巴的造型下本来就该没反应——你之前能看到反应,是模型当时正戴着
翅膀/尾巴。测试里现在有一条一致性断言把这件事写死:有几何就该有命中,
几何全退化就该一个都不中。 - 真正"静态"的是点击穿透用的那层粗网格遮罩(剪影),它按定时重建、粒度也粗 ——
它只决定"这一下算不算落在角色身上",不参与摸头/摸尾巴的判定。
试过用透明度过滤隐藏配件,结果把真尾巴一起滤掉了(
visibleDrawables: 4/11):
隐藏是靠缩放、而opacities表在渲染期未必是最终值,拿它当闸门会误杀正在显形的那一个。
所以放弃了这条思路,改用"路由优先级"解决重叠 —— 保留的诊断读口
partsDebug()/partHitCounts()仍会报"过滤前 vs 过滤后",方便下次判断。
2.3.0
2.3.0
摸头判定改成按模型自己的几何(不再是手绘方框)
用户:"看看摸头的区域能否精准匹配头部的模型而不是手绘区域。" 查下来比想象的糟:
老实现用英文正则 face|eye|mouth|nose|brow|cheek|head 去猜哪些 drawable 是头,
而这只模型的 drawable 叫 Part46 / lianhong / Face_line 这种 —— 一个都匹配不上,
判定静默退化成"点哪都算头",摸哪都在撒娇。
现在的路是三段,全用模型自己的数据,没有猜的:
- cdi3 的部件名:作者给每个部件起了中文名(
Part46= 脸蛋、Part57/58= 眼睛、
Part14= 头发、Part45/68= 耳朵…),宿主按名字挑出头部部件(这只宠物 21 个); - 部件 → drawable:cdi3 的
Parts是部件 id,而getDrawableIndex()认的是
drawable id —— 两个命名空间不同名(实测 21 个部件一个都解不出来)。引擎原始表里有
drawables.parentPartIndices+parts.ids,两边一接就得到 96 个头部 drawable; - 点在三角形内:每次点击现读那批 drawable 的顶点与三角形索引(顶点是当前帧变形
后的,头歪着、身体摆着也准),先做包围盒排除再逐三角形判定。
同一个网格上,新判定和旧方框有 325 个格子不同:新的是圆顶、带耳朵尖、下巴收窄的
头部轮廓,旧的是个完美矩形。
踩到的坑:顶点在包装层(
internalModel.getDrawableVertices),三角形索引在
core 层(internalModel.coreModel.getDrawableVertexIndices)—— 只查一个就会
静默 null、悄悄退回方框。所以测试里每一段单独断言(部件数 / drawable 下标数 /
顶点可读 / 与旧规则不同 / 自身包围盒四角不全中),而不是只验最后那个 true/false。
新互动:摸尾巴、鼠标绕圈转晕
- 摸尾巴:同一套几何判定,部件换成 cdi3 里命名为 尾/鳍/翅/翼 的那 15 个
(狐狸尾 / 猫尾 / 狼尾 / 大翅膀…都是可换配件)。默认反应:吐魂 / 问号。 - 转圈转晕:鼠标绕着角色转够圈数(默认 2 圈 / 1.6 秒内)就演「晕晕」。看的是
累计转角而不是"位置绕了几圈" —— 前者对半径不敏感,贴着角色转小圈和远远转大圈都算。 - 三个互动(摸头 / 摸尾巴 / 转圈)各自可开关,反应候选在设置里点选,
候选就是宠物自己的动作中文名与表情名(不用记 id)。
气泡:会话相位会说一句话,而且全部可配
- 会话相位现在会弹一句台词(思考中 / 用工具 / 等你批准 / 等你回答 / 叫了帮手 / 消息排队 /
完成 / 出错),来源还是 DSH 的真实事件。 - 所有气泡文本都能在设置里改(问候 / 点击 / 摸头 / 摸尾巴 / 转晕 / 归位 / 加载失败 +
每个相位)。一组用|分隔多个变体、随机挑一句;留空 = 用宠物默认
(pet.json的live2d.lines),所以"看一眼"不会把默认改掉。 - 位置可配:左右 / 上下偏移两个滑杆,外加"一句话停留多久"。
- 总开关:关掉之后任何台词都不弹。
顺手修掉两个潜伏的问题
- 退化三角形:
pointInTriangle对三个顶点几乎重合(面积 0)的三角形会返回 true ——
而隐藏的配件(缩放成一点)正好是这种,会让"每个点都算命中"。已加面积下限。 - 转圈时间窗:第一版写成
now - 上次触发 > 窗口,那个值一开始恒大于窗口,于是
每次移动都把累计清零,永远攒不够圈数(诊断读口里total一直停在 0.07)。
现在窗口从这轮累计开始算起。
2.2.0
2.2.0
多接三个会话状态(对着 DSH 0.1.7 的事件表挑的)
0.1.7 里能订阅的事件有 100 个,但只有一部分能翻译成"宠物该演什么"。挑的标准是
它填的是不是一个真实的空档:
| 新相位 | 来自 | 以前的样子 |
|---|---|---|
asking |
user-questions/request |
它问你问题、等你回答的整段时间里,宠物还在演"干活" —— 最大的一个空档 |
helper |
subagent/start / subagent/end |
子代理是长活(几分钟),和一次普通工具调用混在一起看不出区别 |
queued |
agent/inbox/inserted |
你发的话在它忙的时候排队,宠物毫无反应 |
asking 挂的是 waterfall:链一直挂到你答完才 resume,所以这个相位正好等于"卡在等你"
的那一段,不用自己计时。queued 是短促反应(1.6 秒),回落到排队前那个相位而不是
一律回 idle —— 否则一个正在跑工具的回合看起来像停了。
没接的(评估过,理由写在 lib/index.js 的注释里):fs/write-intent / edit-intent
(tool 相位已经演「写本本」了,再拆一个 editing 和它重复)、workflow/*(工作流本身就是
工具调用,已被 tool 覆盖)、agent/status(只有 idle / running 两个值,给不出更细的)。
2.1.0
2.1.0
会话相位改成真正的"接管"
用户报的:"我在爱心眼冒爱心掏出手机自拍的时候进入会话状态,右手会停在手机状态且一直冒爱心。"
原来相位只接管它点名的槽位,其余槽位继续用你自己的选择 —— 于是那套"手机 + 爱心"
在整场会话里一直挂着。现在相位一开始,宠物换相位那一套样子:
- 相位点名的槽位用它抽到的;
- 相位没点名的槽位一律让位(收回);挂在槽位上的定格动作(掏出手机)也跟着
交还身体,手放下来 —— 手机停着不放是有两个原因叠在一起:槽位没让位,而且
"替别的槽位保姿势"那份名单相位走不到,一直每帧把手机姿势写回去; - 装扮槽永远归你(眼镜 / 发饰 / 魔爪 / 巴菲 / 桌布 / 手机换色),相位不碰;
- 会话进行中手点某个槽位会临时盖过相位的那一格(以前点了没反应,因为相位压着它);
- 会话结束后你原来的样子回来 —— 让位不是删除。
右键面板的「装扮」跟着相位走
用户报的:"会话的状态没有在右键菜单的装扮里同步 button。"
面板原来读的是你自己的选择,相位接管后画面上是相位的样子、面板还显示你上次手选的。
现在两边读同一个"此刻显示什么"(effectiveSlotChoice):相位点名用相位的,会话中没点名
的整格空着,装扮槽永远显示你自己的。
2.0.2
2.0.2
只改文档(宠物自带的那份 README,pets/ds-whale-girl/README.md)。它落后得比主 README
还多,用户指出"两个 readme 哦"之后一起收干净:
- 「Cubism Core 需要自行放置」→ 改成实际行为(宿主自动去官方 CDN 取并缓存,只有内网机器才要手动放)
- 宠物包的名字还写着
@linxin666/dsh-pet→ 改成dsh-pet-live2d - 渲染器写
Cubism 3/4→ 这只模型是 Cubism 5 - 槽位数写 17 → 20(自拍独立成槽、氛围拆成三个之后)
- 「六个槽位可以同时生效」→ 槽位之间都可以同时生效(六个是装扮槽的数量)
- 宠物契约表补上新增的
live2d.fidgetSlots,并写明前提在手动点选时由插件补齐 - 引擎层面的叠加原理(为什么绕开引擎的表情管理器、写在帧内哪个缝隙)移出 README,
按文档分工归到 skillcubism-engine
2.0.1
2.0.1
只改文档。 README 里"必须自备 Cubism Core"的说法早就过时了 —— 插件第一次用到它时会由
宿主半区去 Live2D 官方 CDN
取一份(校验后缓存到 %DSH_HOME%\pets\.runtime\),之后离线也能用;
只有访问不了外网的机器才需要手动放一份。2.0.0 的说明还写着"必须自备",会让人白折腾一趟。