896501dcd8c99b730a8e0a74308a079644fa69e7
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
20af2bbe6c |
Торговец снова становится призраком после выгрузки чанка
Баг со стрима: торговец, ставший чёрно-белым и полупрозрачным, наутро снова обычный человек. Догадка была про обновление ассортимента - не подтвердилась, и это стоит записать, потому что по часам лавки модель не трогает ВООБЩЕ ничего: TraderData при сбросе переписывает только PrimaryInventory и lastInventoryUpdate, а TraderArea.SetClosed ходит по блокам TraderOnOff - двери, замки, BlockLight, динамик - и до сущности торговца не дотягивается. Настоящая причина в EntityFactory.CreateEntityOperation.CompleteEntity: `entity.entityId = ecd.id`, то есть сохранённый айди ВОССТАНАВЛИВАЕТСЯ. Торговцы стримятся на подходе и точно так же выгружаются, когда игрок уходит на ночь. Возвращается он - торговец собран заново: новый GameObject, новые рендереры, ванильные материалы, ТОТ ЖЕ entityId. А в патче стоял HashSet<int> Ghosted - "этот айди я уже обработал". Айди в наборе есть, свип проходит мимо, торговец остаётся живым человеком до конца сессии. Отсюда и "на следующее утро": симптом идёт не за часами лавки, а за чанком, в котором лавка стоит. Ghosted стал Dictionary<int, GhostBody>, где GhostBody держит массив рендереров, которые патч взял себе. IsIntact проверяет их: у Unity уничтоженный объект сравнивается с null, так что подмена модели видна прямо, и та же проверка бесплатно закрывает любую другую пересборку, не только выгрузку чанка. Prune чистит Converted и TintedMaterials от уничтоженных объектов. Без него оба списка росли бы на одного торговца за каждую пересборку, а Retint/Reapply ходили бы по обломкам. Материал, выданный через renderer.materials, принадлежит рендереру и умирает вместе с ним - одного прохода хватает на оба списка. В лог добавлена строка "entity <id> came back with a new model" - ровно та, которой не хватало, чтобы найти это за один заход вместо разбора в декомпиляторе. Счётчик в necroghost переименован: со словарём он означает "торговцев под присмотром сейчас", а не "id, которые когда-либо видели" - он теперь падает и растёт. Не проверено в игре: нужен заход к торговцу, выгрузка лавки и возвращение. --- Traders go back to being ghosts after a chunk unload Reported from the stream: a trader who had gone black-and-white and translucent was an ordinary person again the next morning. The guess was the restock - it was wrong, and that is worth recording, because nothing on the shop's clock touches the model at all: TraderData's reset rewrites PrimaryInventory and lastInventoryUpdate only, and TraderArea.SetClosed walks TraderOnOff blocks - doors, locks, BlockLight, speaker - and never reaches the trader entity. The real cause is in EntityFactory.CreateEntityOperation.CompleteEntity: `entity.entityId = ecd.id`, so the saved id is RESTORED. Traders are streamed in on approach and streamed out the same way when the player leaves for the night. On return the trader is rebuilt from scratch - new GameObject, new renderers, the game's own materials - carrying THE SAME entityId. The patch held a HashSet<int> Ghosted, meaning "this id is done". The id was still in the set, the sweep skipped him, and he stayed an ordinary person for the rest of the session. Hence "the next morning": the symptom follows the chunk the shop sits in, not the shop's clock. Ghosted is now a Dictionary<int, GhostBody>, the value holding the renderers the patch took over. IsIntact tests them: Unity's destroyed objects compare equal to null, so a swapped model is directly visible, and the same check covers any other rebuild for free. Prune drops destroyed entries from Converted and TintedMaterials. Without it both lists would grow by one trader's worth per rebuild and Retint/Reapply would be walking the wreckage. A material assigned through renderer.materials is owned by that renderer and dies with it, so one pass settles both. A log line was added - "entity <id> came back with a new model" - the exact line that was missing to find this in one visit rather than in a decompiler. The necroghost counter was reworded: with a dictionary it means "traders held as ghosts right now", not "ids ever seen" - it now falls as well as rises. Not tested in game: needs a visit, a shop unload and a return. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MnwP2Dt1vk8bUPJ452EoVL |
||
|
|
7768541f12 |
Торговцы-призраки: настоящая прозрачность тела, матовость, консоль
Прозрачность у торговцев управляла только бородой. Причина была напечатана
зондом ещё при первом заходе: у шейдера тела (Game/Character) нет НИ цвета с
альфой, НИ режима смешивания - оба рычага ApplyTransparency существуют только
у шейдера волос (Game/Autodesk). Никакое значение альфы тело бы не тронуло.
Сначала добавлен третий рычаг - _Fade, собственный рычаг игры:
EntityModel.SetFade пишет ровно это свойство и отбирает материалы проверкой
HasProperty("_Fade") && shader.name.Contains("Game/Character"), а стоящий рядом
SetVisible(renderFade > 0.01f) закрывает направление: 1 = сплошной, 0 = исчез.
В игре выяснилось, что шейдер реализует его screen-door дизерингом - "тело
гаснет, но идёт мелкой сеточкой". Рычаг рабочий, но пиксели не смешиваются, а
выбрасываются, и никакое число этого не исправит. Оставлен как режим dither.
РАБОЧЕЕ РЕШЕНИЕ - перешивка тела на шейдер волос, у которого есть прозрачный
проход. Доказательство лежало на той же модели в том же кадре: борода всё это
время гасла плавно. Шейдер берётся С МОДЕЛИ - первый материал, умеющий
смешивать (есть цвет с альфой И режим смешивания); Shader.Find оставлен
запасным путём, потому что мод уже дважды получал null/негатив от семейства
Unlit. Решение принимается по способностям материала, имя шейдера нигде не
зашито. Переносятся только альбедо и нормаль: _RMOE - упакованная
roughness/metallic/occlusion/emission, а _MetallicGlossMap ждёт металл в R и
гладкость в A, каналы не совпадают, и связывание "на авось" даёт мокрый пластик
или негатив. Цена названа прямо: тело теряет собственную световую модель
шейдера персонажа и получает стандартную.
МАТОВОСТЬ по просьбе пользователя - три рычага, ломаются по-разному:
_Glossiness в 0 (чистая математика шейдера, работает всегда - несущая
половина); _SpecularHighlights и _GlossyReflections в 0 ВМЕСТЕ с ключевыми
словами _SPECULARHIGHLIGHTS_OFF / _GLOSSYREFLECTIONS_OFF, потому что это
[ToggleOff]-пара и запись одного флоата не делает ничего; карты глянца
очищаются, если непусты, - карта побеждает флоат. Оба keyword'а -
shader_feature, Unity вырезает такие на сборке, если их не выставляет ни один
материал игры, поэтому шершавость сделана основным рычагом, а не запасным.
Применяется ко всем призрачным материалам: волосы нарисованы тем же шейдером и
сохраняли свой блеск, а матовое тело под глянцевой бородой читалось бы хуже.
КОНСОЛЬНАЯ КОМАНДА necroghost (алиас necrotrader): проценты прозрачности,
blend/dither, reset. Балансировать рендер можно только глядя на него, а шаг без
команды стоил пересборки, перезапуска и ~4 минут пешком до торговца. Проценты,
а не альфа: это единица, в которой просьба формулировалась, и они идут в разные
стороны. Регистрации нет и не требуется - SdtdConsole.RegisterCommands ищет
через ReflectionHelpers.FindTypesImplementingBase, который обходит
ModManager.GetLoadedAssemblies(), а LoadMods() стоит на три строки раньше
RegisterCommands(). IsExecuteOnClient = true: команда меняет пиксели.
Две мины, обе реальные. Повторное применение не должно умножать: материалы
кэшируются, и sharedMaterials после первого прохода возвращает наши же клоны,
так что наивный повторный свип дал бы 0.9, потом 0.8 = 0.72; запоминается
базовое значение, живое всегда base * GhostAlpha. Запятая: StringParsers не
зависит от локали, но читает "," как разделитель ТЫСЯЧ, и "necroghost 12,5"
молча стало бы 125 - запятая заменяется на точку до парсинга.
DefaultGhostAlpha 0.9 -> 0.3: 70% прозрачности, найденные в игре. Путь был
1% -> 10% -> 70%, и последний скачок не смена вкуса - на 10% тело ещё
дизерилось, а дизеринг терпим лишь пока слаб. Поэтому же переписана заметка
"ниже ~0.7 силуэт разваливается": предел принадлежал технике, а не глазу.
---
Ghost traders: the body really blends, matte finish, console command
The transparency only ever reached the beard. The probe had already printed
why: the body's shader (Game/Character) has NEITHER a colour with an alpha NOR
a blend mode - both of ApplyTransparency's levers exist only on the hair's
shader (Game/Autodesk). No value of the alpha was ever going to touch it.
A third lever was added first - _Fade, the game's own: EntityModel.SetFade
writes exactly that property and guards it with HasProperty("_Fade") &&
shader.name.Contains("Game/Character"), and the SetVisible(renderFade > 0.01f)
sitting next to it settles the direction: 1 = solid, 0 = gone. In game the
shader turned out to implement it as screen-door dithering - "the body fades,
but goes to a fine grid". The lever works, but pixels are thrown away rather
than blended, and no number fixes that. It is kept as the dither mode.
WHAT ACTUALLY WORKS is re-shading the body onto the hair's shader, which does
have a transparent pass. The existence proof was on the same model in the same
frame: the beard had been fading smoothly all along. The shader is taken OFF
THE MODEL - the first material that can blend (a colour with an alpha AND a
blend mode); Shader.Find is kept only as a fallback, because this mod has twice
been handed null or a negative by the Unlit family. The decision is made on
what a material can do; no shader name is hard-coded. Only albedo and normal
are carried over: _RMOE is a packed roughness/metallic/occlusion/emission map
while _MetallicGlossMap wants metallic in R and smoothness in A - the channels
do not line up, and wiring them by hope is how a character ends up looking like
wet plastic or a negative. The trade is stated plainly: the body loses the
character shader's own lighting response and gets standard lighting instead.
MATTE, as requested - three levers that fail differently: _Glossiness to 0
(plain shader maths, always works - the load-bearing half); _SpecularHighlights
and _GlossyReflections to 0 TOGETHER WITH the _SPECULARHIGHLIGHTS_OFF /
_GLOSSYREFLECTIONS_OFF keywords, because they are a [ToggleOff] pair and
setting the float alone does nothing; and the gloss maps cleared if anything is
in them, since a map beats the float. Both keywords are shader_feature, which
Unity strips at build time if no material in the game sets them - which is why
roughness is the main lever and not the fallback. Applied to every ghost
material: the hair uses the same shader and kept its own shine, and a matte
body under a glossy beard would have read worse than either.
CONSOLE COMMAND necroghost (alias necrotrader): transparency in percent,
blend/dither, reset. A rendering balance can only be judged by looking at it,
and without the command each step cost a rebuild, a restart and a four-minute
walk to a trader. Percent rather than alpha: percent is the unit the request
was made in, and the two run in opposite directions. No registration is needed
- SdtdConsole.RegisterCommands goes through
ReflectionHelpers.FindTypesImplementingBase, which walks
ModManager.GetLoadedAssemblies(), and LoadMods() runs three lines before
RegisterCommands(). IsExecuteOnClient = true: the command changes pixels.
Two real traps. Re-applying must not compound: materials are cached and
sharedMaterials hands back our own clones after the first pass, so a naive
second sweep would give 0.9, then 0.8 = 0.72; the base value is remembered and
the live one is always base * GhostAlpha. The comma: StringParsers is
culture-independent but reads "," as a THOUSANDS separator, so "necroghost
12,5" would silently have become 125 - the comma is turned into a point first.
DefaultGhostAlpha 0.9 -> 0.3: the 70% transparency settled on in game. The road
was 1% -> 10% -> 70%, and the last jump was not a change of taste - at 10% the
body was still dithering, and a dither is bearable only while it is faint. For
the same reason the old "below ~0.7 the silhouette falls apart" note was
rewritten: that limit belonged to the technique, not to the eye.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XN8J75vnum2qAVrtRUMKf7
|