{"id":421,"date":"2026-08-13T17:34:45","date_gmt":"2026-08-14T00:34:45","guid":{"rendered":"https:\/\/secondhandcarrot.com\/blog\/?p=421"},"modified":"2026-08-13T17:35:27","modified_gmt":"2026-08-14T00:35:27","slug":"after-you","status":"publish","type":"post","link":"https:\/\/secondhandcarrot.com\/blog\/dev\/after-you\/","title":{"rendered":"After You"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Last time I wrote about staging conversations, and the whole post turned on one sentence: the dialogue flow is the master script, and the sequences are just shots. Pick a boss, let everything else be a servant. There was a line near the end of that post I want to cash in now \u2014 that we&#8217;d learned a long time ago not to let two systems argue about a character&#8217;s position.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Reader, we had three systems arguing about a doorway.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s been longer than usual between these. Heads-down stretch; the notes piled up faster than the writing did. This one&#8217;s the oldest thing on the pile and the one I still think about most.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fair warning, same as the last two: background post. Dense, in the weeds, no screenshots, no trailer beats. If you came here to see the game, this isn&#8217;t that one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And a stronger version of the standing caveat than usual, because this post needs it: <strong>this is a redesign in flight, not a victory lap.<\/strong> The first half \u2014 the diagnosis and the deletion \u2014 is landed code. The second half is a design we&#8217;ve committed to and are partway through, with a chunk of it still living in specs rather than in the build. I&#8217;ll mark the seam when we get to it. I&#8217;m writing it up now because the <em>reasoning<\/em> is the useful part, and reasoning doesn&#8217;t get more true by waiting for the last task to close.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The error we&#8217;d been ignoring<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every time an NPC crossed a threshold, the log said this:<\/p>\n\n\n\n<pre class=\"wp-block-code has-ast-global-color-3-color has-ast-global-color-5-background-color has-text-color has-background has-link-color wp-elements-1\" style=\"border-width:5px\"><code>LogSmartObject: Error: Expected slot state is 'Claimed' or 'Occupied' but current\nstate is 'ESmartObjectSlotState::Free'. Slot will not be released<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">One test run with a <em>single<\/em> NPC \u2014 Yarbo, walking from home to the grocery where he works \u2014 produced four of them. Two on his way into the house, two around entering the shop. Checking older logs afterward, the same error was sitting there a month earlier. It had been announcing itself politely for weeks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s the kind of thing that&#8217;s easy to file under &#8220;engine noise.&#8221; Nothing visibly broke. Yarbo got where he was going. But read what it says: <strong>something tried to free a door slot that had already been freed.<\/strong> Nearby in the same logs was its blunter sibling \u2014 a complaint about one user trying to release a slot claimed by a <em>different<\/em> user. Two keys to the same lock, no agreement about who locks up.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Three claimers, and nobody in charge<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A door in our game is a SmartObject \u2014 the engine&#8217;s way of saying &#8220;a thing in the world that advertises what can be done with it, with a limited number of slots for people doing it.&#8221; Claim a slot, use it, release it. Fine with one claimant.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We had three, none aware of the others:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The task that decides <em>&#8220;I want to go through this door&#8221;<\/em> claimed a slot and kept the handle in its own instance data.<\/li>\n\n\n\n<li>The processor that performs the crossing claimed a slot and kept the handle on the NPC&#8217;s location fragment.<\/li>\n\n\n\n<li>A third, general-purpose claim task \u2014 which also handled work stations \u2014 claimed a slot, kept the handle in a <em>third<\/em> place, and wrote a mirror copy into the second.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Three claimers, three handle storages, overlapping lifetimes, coordinated entirely by hand-written comments. One of those comments ran something like twenty-five lines, patiently explaining why <em>this<\/em> system clears <em>that<\/em> system&#8217;s handle so the <em>third<\/em> one will skip its release.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That comment was the architecture documenting its own race condition. When code needs a paragraph of apology to be correct, the paragraph isn&#8217;t the problem. (It&#8217;s gone now \u2014 deleting it was part of the fix, and I&#8217;d be lying if I said that wasn&#8217;t satisfying.)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It was worse than three, because one &#8220;walk inside&#8221; touches <strong>two different doors<\/strong>: the entrance on the street, and the interior-side landing door the NPC arrives on. So the real shape was three claimers spread across two SmartObjects with handles flowing through three storages. Working out which handle was live at any instant wasn&#8217;t <em>hard<\/em>, exactly. It was untraceable by reading \u2014 which, for two people, is the same thing as impossible.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The unnecessary claim<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The unlock was asking a dumb question out loud: <strong>why does walking toward a door require owning it?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It doesn&#8217;t. Several members of a household can head for the same front door at once. They aren&#8217;t <em>using<\/em> it yet. They&#8217;re walking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Exclusivity matters at one instant \u2014 the crossing itself, so two NPCs don&#8217;t land on the same interior spot in the same frame. And that instant was already handled by the crossing processor: it walks the door&#8217;s slots until one hands back a valid handle, and if none do, it doesn&#8217;t force it \u2014 the NPC&#8217;s state is left alone and the processor tries again next tick.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So two of the three claims weren&#8217;t protecting anything. They were redundant with a guarantee that already lived somewhere else. Removing them couldn&#8217;t lose serialization we depended on, because the serialization was never in them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix was mostly subtraction: delete the claim block from the intent task, delete the door branch from the general-purpose claim task, and let the crossing processor own the door claim outright.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Worth saying what that cost, since this is a from-the-workbench post. Making the general claim task activity-only <strong>broke the work loop<\/strong>. The tree still had a state asking it to claim a door, and it now refused, loudly and repeatedly. That was known and accepted \u2014 the tree needed rewiring anyway \u2014 but for a stretch there the deletion left the build worse than it found it. Subtraction isn&#8217;t free just because it&#8217;s subtraction.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The bigger thing underneath<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s the seam I promised. Everything above is landed. What follows is where the reasoning went next, and it&#8217;s mostly still ahead of us.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Having pulled that thread, we looked at what was left and had a less comfortable realization: we&#8217;d been hand-rolling a <strong>find \u2192 claim \u2192 use \u2192 release<\/strong> lifecycle as a side-car, running alongside the one Unreal&#8217;s Mass StateTree already ships.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You want to know how we could tell? There&#8217;s a comment in our crossing processor that says, more or less: <em>replicate the engine&#8217;s own cleanup routine here.<\/em> We had named the engine function we were reimplementing, in a comment, directly above the reimplementation. It&#8217;s still there today. That&#8217;s not a war story, it&#8217;s a to-do.<\/p>\n\n\n\n<pre class=\"wp-block-code has-ast-global-color-3-color has-ast-global-color-5-background-color has-text-color has-background has-link-color has-small-font-size wp-elements-2\" style=\"border-width:4px\"><code>WHAT WE HAVE                         WHAT THE ENGINE SHIPS\n\nour task claims a slot               Find  \u2014 candidate search\nour processor claims a slot          Claim \u2014 owns the handle AND the release\nour other task claims a slot         Use   \u2014 fires the behavior, never frees\n   \u2193                                    \u2193\nthree handle storages,               one storage, one owner,\ncoordinated by comments              release on success \/ fail \/ abort<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The engine&#8217;s version has a property ours structurally could not: <strong>the claim owns the release.<\/strong> Leaving the claim state frees the slot \u2014 on success, on failure, and on abort. There&#8217;s no path where a claim outlives its owner, because the owner <em>is<\/em> a state, and states always exit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Delegating also hands you a thing we&#8217;d been paying for in workarounds. Claiming through the engine&#8217;s handler registers a slot-invalidation callback: if the object goes away underneath you \u2014 an interior streaming out with an NPC mid-interaction \u2014 the engine aborts for you. Ours doesn&#8217;t register that, which is part of why we grew a set of polling checks that notice the same condition late and recover. Not all of that polling is waste; some of it covers cases the engine genuinely doesn&#8217;t model. But some of it is us re-deriving a callback that was one function call away.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To be precise about scope, since &#8220;delegate to the engine&#8221; sounds bigger than it is: we&#8217;re keeping our own <strong>Find<\/strong> \u2014 we know which door or station we want and don&#8217;t need a search \u2014 and handing over <strong>Claim<\/strong> and <strong>Use<\/strong>. It&#8217;s a narrower change than the diagram makes it look.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Where the engine&#8217;s version doesn&#8217;t fit<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The engine cycle assumes the object exists. Our hard case violates that: <strong>the destination might not be loaded.<\/strong> If nobody&#8217;s inside the grocery, the grocery&#8217;s interior isn&#8217;t streamed, and there is no landing-door SmartObject to claim or use. The NPC still goes in \u2014 abstractly \u2014 and gets placed for real later, if and when the interior loads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s the irreducible part, and it stays custom. The useful thing is that the boundary was already sitting in our code as a condition the tasks check: <em>is this NPC virtual right now?<\/em> We&#8217;d drawn the line in roughly the right place while patching bugs, without noticing it was also the answer to <em>what should we delegate.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I&#8217;ll flag a correction to my own thinking here, because I had this wrong for a few days. I&#8217;d assumed the other irreducible case was NPCs with no visible body \u2014 nothing to walk to a slot. Turns out using a door doesn&#8217;t require a body at all; the &#8220;use&#8221; can be abstract, and the same cycle covers represented and unrepresented NPCs alike. One boundary, not two. Finding out that a special case you&#8217;d been carefully protecting doesn&#8217;t exist is one of the better feelings in this job.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The claim came back, and that&#8217;s fine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One wrinkle, because leaving it out would make this cleaner than the truth.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The door claim came back. Not the one we killed \u2014 a smaller one. To make the door&#8217;s behavior data-driven, the crossing gets triggered by <em>using<\/em> the door SmartObject, and using it requires claiming it. The rule that stops this from recreating the original mess is about <strong>lifetime<\/strong>, not existence:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">The claim lives only inside the use. Never across the walk.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">The approach stays read-only. The claim is taken at the door, the use completes immediately, the claim releases when the cycle exits. And it&#8217;s on the <em>entrance<\/em> door, while the crossing processor still owns the <em>landing<\/em> door \u2014 two objects, two lifetimes that don&#8217;t overlap.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The cost, stated plainly: that entrance claim is functionally redundant. Setting the flag alone would cross the door. It exists purely to give the behavior a trigger, and it means housemates arriving together will briefly queue at the entrance. We took that trade on purpose, to buy one data-driven place where &#8220;going through a door&#8221; happens.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And the open ends, since I&#8217;d rather you hear them from me:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>The multi-NPC door case is not validated.<\/strong> Our own design notes say, in as many words, that it must be tested and not assumed. It hasn&#8217;t been yet. I&#8217;ve argued in this post that the race can&#8217;t recur; that&#8217;s an argument, not a test result.<\/li>\n\n\n\n<li><strong>A door that&#8217;s momentarily full has no back-off.<\/strong> Find and claim will hand off to each other in a tight loop rather than waiting. Fine for one NPC, not fine for a town.<\/li>\n\n\n\n<li><strong>The wait-to-be-placed step has no timeout.<\/strong> If the thing it&#8217;s waiting on never happens, it waits forever.<\/li>\n\n\n\n<li><strong>The headline pass condition \u2014 that error count going to zero \u2014 hasn&#8217;t been measured yet.<\/strong> That&#8217;s the number that decides whether any of this worked.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Why bother<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing here makes the game more fun. No player will notice it. So, the argument for spending the time anyway.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The three-claimer design <em>worked<\/em>. That&#8217;s the trap. Yarbo went to work, came home, went again. The only outward sign was four lines of error text nobody had to read \u2014 and against building something players can see, that loses the argument every week until the week it doesn&#8217;t.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Same fear as the last two posts, different hat. <strong>We&#8217;d rather pay in places that are cheap and visible than in places that are expensive and silent.<\/strong> An NPC stalled because a slot didn&#8217;t free is expensive and silent. A design where one system owns one thing, and the engine owns the rest, is legible at three in the morning eight months from now \u2014 which is when it&#8217;ll actually matter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One owner per thing. Don&#8217;t reimplement what&#8217;s already under your feet. Keep custom code only where the shipped version genuinely can&#8217;t reach \u2014 and know exactly where that boundary is, because it&#8217;s the most valuable line in the system.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We&#8217;re not all the way there. The door currently has an engine claim on one side and our claim on the other, and that replication comment is still sitting in the processor waiting for me. But the direction is set, and I&#8217;d rather write that down mid-move than pretend it arrived finished.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Boring and deterministic. Still the whole strategy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>More from the workbench soon \u2014 and sooner than last time. If you want the louder updates, that&#8217;s what the socials are for; this corner of the site is where we leave the long notes.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The post discusses the complexities encountered while redesigning how NPCs interact with doors in a game. It highlights issues with multiple systems claiming access to doors, leading to errors. The author emphasizes simplifying the process by eliminating redundant claims, ensuring a more robust design that relies on the engine&#8217;s built-in mechanisms while maintaining custom functionalities for specific cases.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"give_campaign_id":0,"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"advanced_seo_description":"Explore the challenges of NPC interactions with doorways in game design. Discover how simplifying claims enhances efficiency in your code.","jetpack_seo_html_title":"Managing Multiple Claims in Game Design: A Doorway Solution","jetpack_seo_noindex":false,"jetpack_seo_schema_type":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"{title}\n\n{excerpt}\n\n{url}","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"edge","default_image_id":27,"font":"","enabled":true,"token":"eyJpbWciOiJodHRwczpcL1wvc2Vjb25kaGFuZGNhcnJvdC5jb21cL2Jsb2dcL3dwLWNvbnRlbnRcL3VwbG9hZHNcLzIwMjZcLzA0XC9GZXlsaWdodEJheUljb24ucG5nIiwidHh0IjoiQWZ0ZXIgWW91IiwidGVtcGxhdGUiOiJlZGdlIiwiZm9udCI6IiIsImJsb2dfaWQiOjI1NDEzODg1N30.4X9a_fdDbsXqeSPnqzV4lmxyDHCQoG8SmkyBdHtcDlUMQ"},"version":2},"_wpas_customize_per_network":false,"jetpack_post_was_ever_published":false},"categories":[9],"tags":[31,8,30,14,13],"class_list":["post-421","post","type-post","status-publish","format-standard","hentry","category-dev","tag-code","tag-game","tag-indie","tag-tech","tag-unreal"],"jetpack_publicize_connections":[],"jetpack_likes_enabled":true,"jetpack_sharing_enabled":true,"jetpack-related-posts":[],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts\/421","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/comments?post=421"}],"version-history":[{"count":2,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts\/421\/revisions"}],"predecessor-version":[{"id":423,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/posts\/421\/revisions\/423"}],"wp:attachment":[{"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/media?parent=421"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/categories?post=421"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secondhandcarrot.com\/blog\/wp-json\/wp\/v2\/tags?post=421"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}