<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Editor &amp; Tooling on stillcooking.dev</title>
    <link>https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/</link>
    <description>Personal portfolio and technical notes</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    
    <copyright>© 2026 stillcooking.dev — CC BY 4.0, https://creativecommons.org/licenses/by/4.0/</copyright>
    <lastBuildDate>Tue, 04 Aug 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Movie files in Content/Movies stay outside the pak — the two fields that change it</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/movies-staged-outside-pak/</link>
      <pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/movies-staged-outside-pak/</guid>
      <description>By default, staging copies files from Content/Movies outside the pak as NonUFS, which is why .mp4 files sit loose in the build directory. Turning on bSkipMovies and listing the file under UFSMovies pulls it inside, and playback through UMediaPlayer with FileMediaSource keeps working: WmfMedia on Windows reads through IFileManager, which can access files inside the pak.</description>
      <content:encoded><![CDATA[<p>Package a project, and the <code>.mp4</code> files from <code>Content/Movies</code> end up loose in the build directory, sitting next to the <code>.pak</code> and playable in any video player. That is not a staging oversight. It is the default behavior, written into the staging script. Two fields in Project Settings reverse it, and playback through <code>UMediaPlayer</code> using <code>WmfMedia</code> on Windows keeps working without a single edit to game code.</p>
<h2 id="the-default-is-nonufs-which-means-outside-the-pak">The default is NonUFS, which means outside the pak</h2>
<p>The rule lives in the staging script, not in engine code. <code>Content/Movies</code> is walked recursively: anything that is not a <code>.uasset</code> or a <code>.umap</code> gets staged, and the engine&rsquo;s own movies in <code>Engine/Content/Movies</code> go along with the project&rsquo;s. The staged type is <code>NonUFS</code> in every scenario except when using a file server (<code>CopyBuildToStagingDirectory.Automation.cs:1982-2011</code>). A comment a few dozen lines above leaves no room for interpretation (<code>:1971</code>):</p>
<div class="highlight" data-lang="csharp">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="c1">// NonUFS files are never in pak files and should always be remapped</span></span></span></code></pre></div></div>
<p>A <code>NonUFS</code> file is one that staging copies into the output directory and that the pak knows nothing about. Hence the loose <code>.mp4</code> files in the build. Packing them was always possible; nobody ever told staging to put them inside the pak.</p>
<h2 id="the-two-fields-that-reverse-it">The two fields that reverse it</h2>
<p>With <code>bSkipMovies</code> on, the script takes a different branch (<code>:2013</code>), where two explicit lists of movie names decide the staged type instead. In the editor they sit under Project Settings → Packaging → Advanced.</p>
<figure class="prose-figure prose-figure--medium">
  <img src="/screens/packaging-movies-settings.png" alt="The Project Settings panel in the Packaging → Advanced section, showing the labels of three fields: Exclude movie files when staging, Specific movies to Package, and Specific movies to Copy" loading="lazy" /></figure>
<p>The field names hide what they do:</p>
<div class="table-wrap">
  <table>
    <thead>
      <tr>
        <th>Field in the editor</th>
        <th><code>.ini</code> key</th>
        <th>Effect</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Exclude movie files when staging</td>
        <td><code>bSkipMovies</code></td>
        <td>switches modes: from “everything as NonUFS” to “only what is listed”</td>
      </tr>
      <tr>
        <td>Specific movies to Package</td>
        <td><code>UFSMovies</code></td>
        <td>the file goes <strong>into the pak</strong></td>
      </tr>
      <tr>
        <td>Specific movies to Copy</td>
        <td><code>NonUFSMovies</code></td>
        <td>the file goes next to the pak, as before</td>
      </tr>
    </tbody>
  </table>
</div>
<p>The description for <code>UFSMovies</code> states this explicitly: <code>will still be added to the .pak file</code> (<code>ProjectPackagingSettings.h:513</code>). In <code>DefaultGame.ini</code> it looks like this:</p>
<div class="highlight" data-lang="ini">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[/Script/UnrealEd.ProjectPackagingSettings]</span>
</span></span><span class="line"><span class="cl"><span class="na">bSkipMovies</span><span class="o">=</span><span class="s">True</span>
</span></span><span class="line"><span class="cl"><span class="na">+UFSMovies</span><span class="o">=</span><span class="s">Intro</span></span></span></code></pre></div></div>
<h2 id="why-playback-keeps-working">Why playback keeps working</h2>
<p>A movie inside the pak stops being a file the operating system can see. Only code that reads through <code>IPlatformFile</code> can reach it, and the pak is mounted there as another layer of the virtual file system.</p>
<p>Media Framework reads exactly that way. <code>UFileMediaSource</code> hands the path over as a URL with the <code>file://</code> scheme (<code>FileMediaSource.cpp:169</code>). <code>WmfMedia</code>, the player enabled by default on Windows, always opens such a URL through <code>IFileManager</code> and then passes the resulting byte stream to Media Foundation (<code>WmfMediaUtils.cpp:1285-1305</code>, <code>:1341</code>). Nothing in that chain needs the file on disk under its own name.</p>
<p>I checked this in my own build: an <code>.mp4</code> packaged inside the pak through <code>UFSMovies</code> kept playing at runtime through <code>UMediaPlayer</code> with <code>FileMediaSource</code>.</p>
<h2 id="when-this-matters">When this matters</h2>
<p>With one movie, the benefit is a cleaner build directory: the file stops sitting in plain view, and the output directory holds one archive instead of scattered assets alongside it. With a dozen or more it becomes the difference between a build you can scan at a glance and a file list you have to keep track of every time you distribute the build.</p>
<p>Verify this in a packaged build, not in PIE. In the editor every movie sits on disk, so the staging settings have no effect there at all.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>A Blueprint node tooltip stops at the first @param</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/blueprint-node-tooltip-param-truncation/</link>
      <pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/blueprint-node-tooltip-param-truncation/</guid>
      <description>The tooltip generator cuts the function comment off at the first @param and moves everything below it onto the individual pin tooltips. It affects every BlueprintCallable node, and it changes where you have to put warnings in your own API exposed to Blueprints.</description>
      <content:encoded><![CDATA[<p>A Blueprint node&rsquo;s tooltip shows the function comment only up to the first <code>@param</code>. Parameter descriptions, return value documentation, warnings attached to one specific argument: everything below that point ends up on the individual pin tooltips. Nobody checks those tooltips until they already know what they&rsquo;re looking for.</p>
<p>It comes down to how the tooltip generator splits the comment. That behavior affects every BlueprintCallable node, in the engine and in every plugin.</p>
<h2 id="the-symptom-nine-pins-a-two-sentence-tooltip">The symptom: nine pins, a two-sentence tooltip</h2>
<p><code>Set Timer by Event</code> is the textbook case. The function comment warns that a time less than or equal to zero clears the timer instead of setting it (<code>Engine/Classes/Kismet/KismetSystemLibrary.h:723</code>):</p>
<div class="highlight" data-lang="cpp">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * Set a timer to execute delegate. Setting an existing timer will reset that timer with updated parameters.
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param Event   Event. Can be a K2 function or a Custom Event.
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param Time    How long to wait before executing the delegate, in seconds.
</span></span></span><span class="line"><span class="cl"><span class="cm"> *                Setting a timer to &lt;= 0 seconds will clear it if it is set.
</span></span></span><span class="line"><span class="cl"><span class="cm"> * ...
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span></span></span></code></pre></div></div>
<p>The node tooltip itself reads, in full:</p>
<blockquote>
<p>Set a timer to execute delegate. Setting an existing timer will reset that timer with updated parameters.</p>
<p>Target is Kismet System Library</p>
</blockquote>
<figure class="prose-figure">
  <img src="/screens/tooltip-node-truncated.png" alt="Tooltip of the Set Timer by Event node: two sentences of description and the line Target is Kismet System Library, with no mention of the behavior when Time &lt;= 0" loading="lazy" /></figure>
<p>The sentence about <code>&lt;= 0</code> shows up when you hover the <code>Time</code> pin, and nowhere else:</p>
<figure class="prose-figure">
  <img src="/screens/tooltip-pin-time.png" alt="Tooltip of the Time pin on the same node: the full description, including the sentence about clearing the timer when the value is less than or equal to zero" loading="lazy" /></figure>
<p>I wrote up the consequences of this particular case separately in <a href="/en/topics/unreal-engine/gameplay-framework/set-timer-by-event-time-zero">Set Timer by Event with Time = 0.0 never fires</a>. What interests me here is the mechanism, because it affects far more than just this one node.</p>
<h2 id="the-cause-split-with-nullptr-on-the-right">The cause: <code>Split</code> with <code>nullptr</code> on the right</h2>
<p>The node tooltip comes from <code>ObjectTools::GetDefaultTooltipForFunction</code> (<code>Editor/UnrealEd/Private/ObjectTools.cpp:5396</code>), and the whole decision fits in two calls:</p>
<div class="highlight" data-lang="cpp">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="c1">// Strip off the doxygen nastiness
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">static</span> <span class="k">const</span> <span class="n">FString</span> <span class="nf">DoxygenParam</span><span class="p">(</span><span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;@param&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">const</span> <span class="n">FString</span> <span class="nf">DoxygenReturn</span><span class="p">(</span><span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;@return&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Tooltip</span><span class="p">.</span><span class="n">Split</span><span class="p">(</span><span class="n">DoxygenParam</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Tooltip</span><span class="p">,</span> <span class="k">nullptr</span><span class="p">,</span> <span class="n">ESearchCase</span><span class="o">::</span><span class="n">IgnoreCase</span><span class="p">,</span> <span class="n">ESearchDir</span><span class="o">::</span><span class="n">FromStart</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">Tooltip</span><span class="p">.</span><span class="n">Split</span><span class="p">(</span><span class="n">DoxygenReturn</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Tooltip</span><span class="p">,</span> <span class="k">nullptr</span><span class="p">,</span> <span class="n">ESearchCase</span><span class="o">::</span><span class="n">IgnoreCase</span><span class="p">,</span> <span class="n">ESearchDir</span><span class="o">::</span><span class="n">FromStart</span><span class="p">);</span></span></span></code></pre></div></div>
<p><code>Split</code> with the result written back to <code>LeftString</code> and <code>nullptr</code> in place of <code>RightString</code> truncates the text at the first match. The right-hand side is computed and thrown away. The comment “strip off the doxygen nastiness” describes the intent precisely: the Doxygen tags were treated as noise and removed together with the content they describe.</p>
<p>Everything else follows from that one decision. Parameter descriptions take a separate route to the individual pin tooltips (<code>ObjectTools.cpp:5452</code>), and <code>@see</code> and <code>@note</code> survive the cut and render as <code>See:</code> and <code>Note:</code>, but only when they appear above the first <code>@param</code>.</p>
<h2 id="what-to-do-about-it">What to do about it</h2>
<p><strong>Reading someone else&rsquo;s nodes:</strong> if a node tooltip looks suspiciously thin for the number of pins, the documentation is most likely on the pins. Hovering a pin takes a second and routinely reveals information that was missing from the node tooltip.</p>
<p><strong>Exposing your own API to Blueprints</strong> (which covers every plugin with <code>UFUNCTION(BlueprintCallable)</code>):</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  Everything the user needs to know <strong>before</strong> wiring up the node has to sit in the description block above the first <code>@param</code>: edge cases, values that invalidate the call, initialization order requirements. A sentence attached to a parameter is technically correct documentation and effectively invisible: the user will not see it until they start hunting for the cause of a bug, which is one step too late.
</div>

<div class="callout callout--tip">
  <div class="callout__title">Tip</div>
  An <code>@note</code> placed above the parameter list renders as <code>Note:</code>. That is the simplest way to make such a warning stand out while keeping the comment compatible with Doxygen.
</div>

<h2 id="when-this-matters">When this matters</h2>
<p>On a node with two pins, not much. The whole description fits in the opening sentence anyway.</p>
<p>The stakes rise with the number of parameters, and with the number of values that are technically valid but mean something other than what the user assumes: zero for “cancel,” a negative value for “no limit,” an empty name for “use the default.” That kind of knowledge belongs next to the parameter by definition — and the text next to the parameter is exactly what the node tooltip will not show.</p>
<p>There&rsquo;s one more consequence for plugin authors: a user who falls into that trap despite a correctly written comment will file it as a documentation bug. The comment will be right where it belongs, and the report will still be justified.</p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
