<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Orcsune</title>
	<atom:link href="https://orcsune.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://orcsune.com/</link>
	<description>An Orcly Wonderland</description>
	<lastBuildDate>Thu, 18 Dec 2025 18:27:01 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png</url>
	<title>Orcsune</title>
	<link>https://orcsune.com/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Stopwatch Groups in C#</title>
		<link>https://orcsune.com/blog/stopwatch-groups-in-c/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Sun, 27 Jul 2025 21:02:47 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4877</guid>

					<description><![CDATA[<p>Stopwatch Groups in C# Posted: July 27, 2025 Last Updated: July 29, 2025 Stopwatch Groups in C# Stopwatches. I use Stopwatches in C# as a basic way to time various systems in my games. Using those stopwatches, I can measure and display charts with my timing data to give me immediate, in-game feedback about what [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/stopwatch-groups-in-c/">Stopwatch Groups in C#</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4877" class="elementor elementor-4877">
				<div class="elementor-element elementor-element-64b2960f e-flex e-con-boxed e-con e-parent" data-id="64b2960f" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-5c26d146 elementor-widget elementor-widget-text-editor" data-id="5c26d146" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Stopwatch Groups in C#<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p><strong>Posted:</strong> July 27, 2025</p><p><strong>Last Updated:</strong> July 29, 2025</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-7d05d260 e-flex e-con-boxed e-con e-parent" data-id="7d05d260" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-2473585 elementor-widget elementor-widget-text-editor" data-id="2473585" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><span style="text-decoration: underline;">Stopwatch Groups in C#</span></p><p>Stopwatches. I use <a href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=net-9.0">Stopwatches in C#</a> as a basic way to time various systems in my games. Using those stopwatches, I can measure and display charts with my timing data to give me immediate, in-game feedback about what systems are causing slowdowns. But, I had a problem. Sometimes I had multiple parts to multiple systems. After 10 or so stopwatches, it became annoying to have a completely individual timing for each measured section of code. I needed a way to group timings together into categories. For example, I might want a tree of timings that I can combine or separate at-will to get a better look at some systems. Maybe a tree that looks like this:</p><ul><li>Gameplay<ul><li>Movement</li><li>Collision<ul><li>Gather</li><li>Update</li></ul></li></ul></li><li>Rendering</li></ul><p>If I want, I can check the processing time for whole groups (e.g. Gameplay, Gameplay/Collision, or Rendering), or for individual stopwatch timings (e.g. Gameplay/Collision/Gather). From these requirements, I came up with <code>StopwatchGroups</code>. Below is all the code for this, split into categories:</p>								</div>
				</div>
				<div class="elementor-element elementor-element-5eacac7 elementor-widget elementor-widget-text-editor" data-id="5eacac7" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>Code Implementation:</h2>								</div>
				</div>
				<div class="elementor-element elementor-element-b9e16e6 elementor-widget elementor-widget-n-accordion" data-id="b9e16e6" data-element_type="widget" data-e-type="widget" data-settings="{&quot;default_state&quot;:&quot;all_collapsed&quot;,&quot;max_items_expended&quot;:&quot;multiple&quot;,&quot;n_accordion_animation_duration&quot;:{&quot;unit&quot;:&quot;ms&quot;,&quot;size&quot;:100,&quot;sizes&quot;:[]}}" data-widget_type="nested-accordion.default">
				<div class="elementor-widget-container">
							<div class="e-n-accordion" aria-label="Accordion. Open links with Enter or Space, close with Escape, and navigate with Arrow Keys">
						<details id="e-n-accordion-item-1940" class="e-n-accordion-item" >
				<summary class="e-n-accordion-item-title" data-accordion-index="1" tabindex="0" aria-expanded="false" aria-controls="e-n-accordion-item-1940" >
					<span class='e-n-accordion-item-title-header'><div class="e-n-accordion-item-title-text"> Interfaces </div></span>
							<span class='e-n-accordion-item-title-icon'>
			<span class='e-opened' ><svg aria-hidden="true" class="e-font-icon-svg e-fas-minus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
			<span class='e-closed'><svg aria-hidden="true" class="e-font-icon-svg e-fas-plus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
		</span>

						</summary>
				<div role="region" aria-labelledby="e-n-accordion-item-1940" class="elementor-element elementor-element-becbcb7 e-con-full e-flex e-con e-child" data-id="becbcb7" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-e604b70 elementor-widget elementor-widget-text-editor" data-id="e604b70" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>IStopwatchInfo:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-a8e68b4 elementor-widget elementor-widget-code-block-for-elementor" data-id="a8e68b4" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

namespace Orcsune.Core.ODebug.Profiling {
    public interface IStopwatchInfo {
        double[] windowTimes { get; set; }
        double lastTime { get; set; }
        int windowSize { get; set; }
        double avgTime { get; set; }
        double maxTime { get; set; }
        double minTime { get; set; }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-f1b5757 elementor-widget elementor-widget-text-editor" data-id="f1b5757" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>The <code>IStopwatchInfo</code> interface essentially defines the output structure of any output point in the tree of stopwatches. Whether you retrieve the information about an individual <code>StopwatchRecorder</code> or a whole <code>StopwatchGroup</code>, the output should be an <code>IStopwatchInfo</code> type. This type provides an array of times recorded by a <code>Stopwatch</code> as well as some basic statistics about the set of times.</p><h3>IStopwatchInfoProvider:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-8518938 elementor-widget elementor-widget-code-block-for-elementor" data-id="8518938" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

namespace Orcsune.Core.ODebug.Profiling {
    public interface IStopwatchInfoProvider {
        public IStopwatchInfo GetInfo();
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-e02ec65 elementor-widget elementor-widget-text-editor" data-id="e02ec65" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>The <code>IStopwatchInfoProvider</code> interface is exactly what it sounds like. It defines a type that can produce <code>IStopwatchInfo</code> type instances. The <code>StopwatchRecorder</code> and <code>StopwatchGroup</code> both implement this interface.</p>								</div>
				</div>
				</div>
					</details>
						<details id="e-n-accordion-item-1941" class="e-n-accordion-item" >
				<summary class="e-n-accordion-item-title" data-accordion-index="2" tabindex="-1" aria-expanded="false" aria-controls="e-n-accordion-item-1941" >
					<span class='e-n-accordion-item-title-header'><div class="e-n-accordion-item-title-text"> StopwatchInfos </div></span>
							<span class='e-n-accordion-item-title-icon'>
			<span class='e-opened' ><svg aria-hidden="true" class="e-font-icon-svg e-fas-minus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
			<span class='e-closed'><svg aria-hidden="true" class="e-font-icon-svg e-fas-plus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
		</span>

						</summary>
				<div role="region" aria-labelledby="e-n-accordion-item-1941" class="elementor-element elementor-element-52c8dc8 e-con-full e-flex e-con e-child" data-id="52c8dc8" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-4a6b97a elementor-widget elementor-widget-text-editor" data-id="4a6b97a" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>StopwatchInfo:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-3b9777d elementor-widget elementor-widget-code-block-for-elementor" data-id="3b9777d" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

namespace Orcsune.Core.ODebug.Profiling {
    public struct StopwatchInfo : IStopwatchInfo {
        public double[] windowTimes { get; set; }
        public double lastTime { get; set; }
        public int windowSize { get; set; }
        public double avgTime { get; set; }
        public double maxTime { get; set; }
        public double minTime { get; set; }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-f6edba8 elementor-widget elementor-widget-text-editor" data-id="f6edba8" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>The <code>StopwatchInfo</code> is just a one-to-one implementation of the <code>IStopwatchInfo</code> interface as a struct. It just provides properties for each of those defined in the interface. The properties are filled when the provider produces the info output.</p><h3>StopwatchGroupInfo:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-4c08ced elementor-widget elementor-widget-code-block-for-elementor" data-id="4c08ced" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

using System.Collections.Generic;
using System.Linq;

namespace Orcsune.Core.ODebug.Profiling {
    public struct StopwatchGroupInfo : IStopwatchInfo {
        public double[] windowTimes { get; set; }
        public double lastTime { get; set; }
        public int windowSize { get; set; }
        public double avgTime { get; set; }
        public double maxTime { get; set; }
        public double minTime { get; set; }

        public StopwatchGroupInfo(IEnumerable&lt;StopwatchRecorder&gt; recorders) : this(recorders.Select(r=&gt;r.GetInfo()).Cast&lt;IStopwatchInfo&gt;()) {}
        public StopwatchGroupInfo(IEnumerable&lt;IStopwatchInfo&gt; infos) {
            int maxWindowSize = 0;
            foreach (IStopwatchInfo info in infos) { if (info.windowSize &gt; maxWindowSize) { maxWindowSize = info.windowSize; } }
            windowTimes = new double[maxWindowSize];
            lastTime = 0;
            windowSize = maxWindowSize;
            avgTime = 0;
            maxTime = 0;
            minTime = 0;
            // Sum relevant properties from my constituent recorder infos
            foreach (IStopwatchInfo info in infos) {
                ApplyInfo(info);
            }
        }

        private void ApplyInfo(IStopwatchInfo info) {
            for (int i = 0; i &lt; info.windowTimes.Length; i++) { windowTimes[i] += info.windowTimes[i]; }
            lastTime += info.lastTime;
            // windowSize does not change
            avgTime += info.avgTime;
            maxTime += info.maxTime;
            minTime += info.minTime;
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-28f4168 elementor-widget elementor-widget-text-editor" data-id="28f4168" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>The <code>StopwatchGroupInfo</code> implementation gets a little more complicated. This structure represents the output from a <code>StopwatchGroup</code>. It is created from multiple other <code>IStopwatchInfo</code> types. For each <code>IStopwatchInfo</code> provided, the <code>StopwatchGroupInfo</code> accumulates all the relevant statistics from each into itself, thus representing the sum of times of each constituent info.</p>								</div>
				</div>
				</div>
					</details>
						<details id="e-n-accordion-item-1942" class="e-n-accordion-item" >
				<summary class="e-n-accordion-item-title" data-accordion-index="3" tabindex="-1" aria-expanded="false" aria-controls="e-n-accordion-item-1942" >
					<span class='e-n-accordion-item-title-header'><div class="e-n-accordion-item-title-text"> StopwatchRecorder and StopwatchGroup </div></span>
							<span class='e-n-accordion-item-title-icon'>
			<span class='e-opened' ><svg aria-hidden="true" class="e-font-icon-svg e-fas-minus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
			<span class='e-closed'><svg aria-hidden="true" class="e-font-icon-svg e-fas-plus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
		</span>

						</summary>
				<div role="region" aria-labelledby="e-n-accordion-item-1942" class="elementor-element elementor-element-50c8276 e-con-full e-flex e-con e-child" data-id="50c8276" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-5f9db05 elementor-widget elementor-widget-text-editor" data-id="5f9db05" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>StopwatchRecorder:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-702fcd6 elementor-widget elementor-widget-code-block-for-elementor" data-id="702fcd6" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

using System;
using System.Diagnostics;

namespace Orcsune.Core.ODebug.Profiling {
    public class StopwatchRecorder : IStopwatchInfoProvider
    {
        public int counter { get; private set; }
        public double lastTime { get; private set; }
        double[] times;
        public int windowSize {
            get =&gt; times.Length;
        }
        double avgWindowTime;
        double maxTime;
        int maxTimeIndex;
        double minTime;
        int minTimeIndex;

        Stopwatch timer;

        public TimeSpan Elapsed { get =&gt; timer.Elapsed; }
        public long ElapsedMilliseconds { get =&gt; timer.ElapsedMilliseconds; }
        public long ElapsedTicks { get =&gt; timer.ElapsedTicks; }
        public bool IsRunning { get =&gt; timer.IsRunning; }

        public StopwatchRecorder() {
            timer = new Stopwatch();
            timer.Reset();
            ChangeWindowSize(30);
        }
        public StopwatchRecorder(int windowSize) {
            timer = new Stopwatch();
            timer.Reset();
            ChangeWindowSize(windowSize);
        }

        public IStopwatchInfo GetInfo() {
            avgWindowTime = 0;
            foreach (double t in times) { avgWindowTime += t; }
            avgWindowTime /= windowSize;
            return new StopwatchInfo {
                windowTimes = times,
                lastTime = lastTime,
                windowSize = windowSize,
                avgTime = avgWindowTime,
                maxTime = maxTime,
                minTime = minTime
            };
        }

        public void Reset() =&gt; timer.Reset();
        public void Restart() =&gt; timer.Restart();
        public void Start() =&gt; timer.Start();
        public void Stop() =&gt; timer.Stop();
        public override string ToString() {
            return $&quot;StopwatchRecorder(Stopwatch={timer.ToString()})&quot;;
        }
        

        public void LogTime() {
            LogTimeAtCurrentIndex();
            counter += 1;
        }
        /// &lt;summary&gt;
        /// Adds the timer&#039;s currently elapsed time to the previously
        /// recorded time.
        /// This is useful when you have run LogTime, but continued the stopwatch
        /// and want to replace the old value with the new one.
        /// &lt;/summary&gt;
        public void ReplaceTimeAtPriorIndex() {
            // Get the prior index
            int idx = (int)(counter+times.Length+1)%times.Length;
            lastTime = Elapsed.TotalMilliseconds;
            times[idx] = Elapsed.TotalMilliseconds;
            ReanalyzeWindowMetrics(idx);
            counter += 1;
        }

        public void LogTimeAtCurrentIndex() {
            int idx = (int)counter%times.Length;
            lastTime = Elapsed.TotalMilliseconds;
            times[idx] = Elapsed.TotalMilliseconds;
            ReanalyzeWindowMetrics(idx);
        }

        private void ReanalyzeWindowMetrics(int idx) {
            // Find the max time in the window
            if (maxTimeIndex == idx) {
                // If old max index is current index, then we could have replaced
                // the old value at index, so we should look through all times for a max.
                maxTime = 0;
                for (int i = 0; i &lt; times.Length; i++) { if (times[i] &gt; maxTime) { maxTime = times[i]; maxTimeIndex = i; }}
            } else {
                // Just look at the current index. If it is larger than the old max, replace the old max with this time
                if (times[idx] &gt; maxTime) {
                    maxTime = times[idx];
                    maxTimeIndex = idx;
                }
            }
            // Find the min time in the window
            if (minTimeIndex == idx) {
                // If old min index is current index, then we could have replaced
                // the old value at index, so we should look through all times for a min.
                minTime = double.MaxValue;
                for (int i = 0; i &lt; times.Length; i++) { if (times[i] &lt; minTime) { minTime = times[i]; minTimeIndex = i; }}
            } else {
                // Just look at the current index. If it is smaller than the old min, replace the old min with this time
                if (times[idx] &lt; minTime) {
                    minTime = times[idx];
                    minTimeIndex = idx;
                }
            }
        }

        public void ChangeWindowSize(int newSize) {
            if (newSize &lt; 1) { return; }
            counter = 0;
            maxTime = 0;
            maxTimeIndex = 0;
            minTime = 0;
            minTimeIndex = 0;
            times = new double[newSize];
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-3f666db elementor-widget elementor-widget-text-editor" data-id="3f666db" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>The <code>StopwatchRecorder</code> is like an amalgamation between a <a href="https://orcsune.com/blog/sliding-window-in-c/"><code>ComparableSlidingWindow</code></a> and a stopwatch. The simplest way to use it is by running <code>Start</code>, <code>Stop</code>, and then <code>LogTime</code>. Make sure to run <code>Reset</code> afterwards as well. Note that this class implements <code>IStopwatchInfoProvider</code>. In it&#8217;s <code>GetInfo</code> method, it simply fills out a <code>StopwatchInfo</code> instance with the values recorded by the stopwatch and returns it. The sliding window functionality is helpful if you want to measure a moving average of times on the stopwatch. Alternatively, you can just ignore this feature by setting the window size to 1 or simply using the <code>lastTime</code> property in the <code>StopwatchInfo</code>.</p><h3>StopwatchGroup:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-3cdac89 elementor-widget elementor-widget-code-block-for-elementor" data-id="3cdac89" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Orcsune.Core.ODebug.Profiling {
    public class StopwatchGroup : IStopwatchInfoProvider {
        public static char sep = &#039;/&#039;;
        public ConcurrentDictionary&lt;string, StopwatchRecorder&gt; stopwatchMap;
        public ConcurrentDictionary&lt;string, StopwatchGroup&gt; groupMap;
        private int defaultWindowSize;

        public StopwatchGroup() {
            defaultWindowSize = 30;
            stopwatchMap = new ConcurrentDictionary&lt;string, StopwatchRecorder&gt;();
            groupMap = new ConcurrentDictionary&lt;string, StopwatchGroup&gt;();
        }
        public StopwatchGroup(int windowSize) {
            defaultWindowSize = windowSize;
            stopwatchMap = new ConcurrentDictionary&lt;string, StopwatchRecorder&gt;();
            groupMap = new ConcurrentDictionary&lt;string, StopwatchGroup&gt;();
        }

        public Dictionary&lt;string, IStopwatchInfo&gt; GetLeafInfos(Dictionary&lt;string, IStopwatchInfo&gt; currentInfos = null, string currentPath=&quot;&quot;) {
            Dictionary&lt;string, IStopwatchInfo&gt; infos = currentInfos == null ?
                                                        new Dictionary&lt;string, IStopwatchInfo&gt;() :
                                                        currentInfos;
            foreach ((string name, StopwatchRecorder recorder) in stopwatchMap) {
                infos.Add(currentPath + sep + name, recorder.GetInfo());
            }
            foreach ((string name, StopwatchGroup subgroup) in groupMap) {
                subgroup.GetLeafInfos(infos, name);
            }
            return infos;
        }

        private StopwatchGroupInfo GetMyStopwatchInfo() {
            return new StopwatchGroupInfo(
                stopwatchMap.Values
                    .Select(sw =&gt; sw.GetInfo())
            );
        }
        public IStopwatchInfo GetInfo() {
            StopwatchGroupInfo consolidatedInfo = new StopwatchGroupInfo(
                stopwatchMap.Values
                    .Select(sw =&gt; sw.GetInfo())
                    .Concat(groupMap.Values
                        .Select(g =&gt; g.GetInfo()))
            );
            return consolidatedInfo;
        }

        private string[] SplitBaseName(string path) {
            return path.Split(sep, 2);
        }

        public bool TryGetInfoProvider(string path, out IStopwatchInfoProvider provider) {
            provider = GetInfoProvider(path);
            return provider != null;
        }
        public IStopwatchInfoProvider GetInfoProvider(string path) {
            string[] parts = SplitBaseName(path);
            string baseName = parts[0];
            // If we are on the last part of the path, then we need a provider now
            if (parts.Length == 1) {
                if (stopwatchMap.ContainsKey(baseName)) {
                    return stopwatchMap[baseName];
                }
                else if (groupMap.ContainsKey(baseName)) {
                    return groupMap[baseName];
                }
                else {
                    return null;
                }
            }
            // We are not at the end of the path, now look through my subgroups
            else {
                string extraPath = parts[1];
                // If we don&#039;t have a group called baseName, then we can&#039;t
                // look any further
                if (!groupMap.ContainsKey(baseName)) {
                    return null;
                }
                // By here, we MUST have a group called baseName,
                // so call GetInfoProvider on it
                return groupMap[baseName].GetInfoProvider(extraPath);
            }
        }

        public StopwatchRecorder GetRecorder(string path) {
            string[] parts = SplitBaseName(path);
            string baseName = parts[0];
            // If we are on the last part of the path, then we need a recorder now
            if (parts.Length == 1) {
                if (!stopwatchMap.ContainsKey(baseName)) {
                    CreateRecorderAtPath(baseName);
                }
                return stopwatchMap[baseName];
            }
            // We are not at the end of the path, now look through my subgroups
            else {
                string extraPath = parts[1];
                // If we don&#039;t have a group called baseName, then we know we must add it
                if (!groupMap.ContainsKey(baseName)) {
                    groupMap.TryAdd(baseName, new StopwatchGroup(defaultWindowSize));
                }
                // By here, we MUST have a group called baseName,
                // so call GetRecorder on it
                return groupMap[baseName].GetRecorder(extraPath);
            }
        }

        public void CreateRecorderAtPath(string path) {
            string[] parts = SplitBaseName(path);
            // We will have only 1 string if path is not nested
            // in which case we make a recorder with name &#039;path&#039; in this StopwatchGroup
            if (parts.Length == 1) {
                if (!stopwatchMap.ContainsKey(path)) {
                    stopwatchMap.TryAdd(path, new StopwatchRecorder(defaultWindowSize));
                }
            }
            // We have more path to traverse.
            // Look in my subgroups to see if a group already exists
            else {
                string baseGroupName = parts[0];
                string extraPath = parts[1];
                if (!groupMap.ContainsKey(baseGroupName)) {
                    groupMap.TryAdd(baseGroupName, new StopwatchGroup(defaultWindowSize));
                }
                groupMap[baseGroupName].CreateRecorderAtPath(extraPath);
            }
            // Check problems with group names and recorder names overlapping
            if (stopwatchMap.ContainsKey(parts[0]) &amp;&amp; groupMap.ContainsKey(parts[0])) {
                throw new System.Exception();
            }
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-731d097 elementor-widget elementor-widget-text-editor" data-id="731d097" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>The main purpose of the <code>StopwatchGroup</code> is to finally provide a mapping/grouping between a set of keys/recorder names (strings), and other <code>IStopwatchInfoProviders</code>. To boil it down, the <code>StopwatchGroup</code> keeps track of <code>StopwatchRecorders</code> and other <code>StopwatchGroups</code> directly underneath itself in the hierarchy. This class deserves a bit more explanation, so let&#8217;s go through its implementation.</p><ul><li>It&#8217;s two most important fields, <code>stopwatchMap</code> and <code>groupMap</code>, are just dictionaries mapping a key/recorder name to some info provider. Because the <code>StopwatchGroup</code> can have &#8220;leaf&#8221; providers (providers that end the hierarchy like <code>StopwatchRecorder</code>) or deeper providers (other <code>StopwatchGroups</code>), we need to separate these to properly handle access in the future.</li><li><code>GetInfo()</code> gathers up all the immediate <code>IStopwatchInfoProviders</code> under this instance&#8217;s jurisdiction and feeds them to a <code>StopwatchGroupInfo</code> constructor, effectively combining the timings of all child providers into one output for this group.</li><li><code>GetInfoProvider()</code> is the first serious method, but all the other methods generally follow the same pattern: they all split the recorder name (<code>path</code> argument) into pieces, using each piece as another level of the hierarchy. When we are on the last piece of the path, then we know that we need to do something special because we have arrived at the place we want to be. For this method, we are looking for any <code>IStopwatchInfoProvider</code> at the given path, so we descend down the hierarchy by calling the child <code>StopwatchGroups</code>&#8216; <code>GetInfoProvider()</code> method while not at the end of the path. Once at the end of the path, we MUST have either a <code>StopwatchRecorder</code> or <code>StopwatchGroup</code> that matches the last path piece, otherwise, we have no such recorder and thus return null.</li><li><code>GetRecorder()</code> deals specifically with getting a <code>StopwatchRecorder</code> from a given path. Again, as long as there is a path hierarchy to descend, we do so. If there is no <code>StopwatchGroup</code> on an intermediate piece of the path, then we CAN just create one, because we know that we are not at the end of the hierarchy, so the <code>IStopwatchInfoProvider</code> at this point MUST be a <code>StopwatchGroup</code>. Once we get to the end of the path, we first try to retrieve an existing <code>StopwatchRecorder</code>, but if one doesn&#8217;t exist, then we can just create one first, for a similar reason as with the <code>StopwatchGroup</code>.</li><li><code>CreateRecorderAtPath()</code> can create a <code>StopwatchRecorder</code> at any depth relative to the calling <code>StopwatchGroup</code> by, again, making any necessary intermediate <code>StopwatchGroups</code>.</li></ul>								</div>
				</div>
				</div>
					</details>
						<details id="e-n-accordion-item-1943" class="e-n-accordion-item" >
				<summary class="e-n-accordion-item-title" data-accordion-index="4" tabindex="-1" aria-expanded="false" aria-controls="e-n-accordion-item-1943" >
					<span class='e-n-accordion-item-title-header'><div class="e-n-accordion-item-title-text"> Utilities </div></span>
							<span class='e-n-accordion-item-title-icon'>
			<span class='e-opened' ><svg aria-hidden="true" class="e-font-icon-svg e-fas-minus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
			<span class='e-closed'><svg aria-hidden="true" class="e-font-icon-svg e-fas-plus" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg></span>
		</span>

						</summary>
				<div role="region" aria-labelledby="e-n-accordion-item-1943" class="elementor-element elementor-element-63f3a46 e-flex e-con-boxed e-con e-child" data-id="63f3a46" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-56a5e88 elementor-widget elementor-widget-text-editor" data-id="56a5e88" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>StopwatchTracker:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-d85a700 elementor-widget elementor-widget-code-block-for-elementor" data-id="d85a700" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

using System.Collections.Generic;

namespace Orcsune.Core.ODebug.Profiling {
    public static class StopwatchTracker {
        public static StopwatchGroup unifiedGroup = new StopwatchGroup(30);

        public static IStopwatchInfoProvider GetInfoProvider(string key) {
            return unifiedGroup.GetInfoProvider(key);
        }
        public static StopwatchRecorder GetRecorder(string key) {
            return unifiedGroup.GetRecorder(key);
        }
        public static StopwatchRecorderContext GetRecorderContext(string key) {
            return new StopwatchRecorderContext(GetRecorder(key));
        }

        public static IStopwatchInfo GetInfo(string key) {
            return GetInfoProvider(key).GetInfo();
        }
        
        public static Dictionary&lt;string, IStopwatchInfo&gt; GetAllInfos() {
            return unifiedGroup.GetLeafInfos();
        }

        public static void BeginTiming(string key) {
            StopwatchRecorder recorder = GetRecorder(key);
            recorder.Start();
        }
        public static void EndTiming(string key) {
            StopwatchRecorder recorder = GetRecorder(key);
            recorder.Stop();
        }
        public static void LogTime(string key) {
            StopwatchRecorder recorder = GetRecorder(key);
            recorder.LogTime();
        }
        public static void LogTimeAndEnd(string key) {
            StopwatchRecorder recorder = GetRecorder(key);
            recorder.LogTime();
            recorder.Stop();
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-8a5f74f elementor-widget elementor-widget-text-editor" data-id="8a5f74f" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><code>StopwatchTracker</code> is, more or less, a glorified static wrapper around a <code>StopwatchGroup</code> instance. The goal of the <code>StopwatchTracker</code> is to provide a simple, app-wide utility to easily log and retrieve values for timed portions of code. You could easily turn it into a non-static class and create instances of it, but I had no need to, and you could probably just directly use a <code>StopwatchGroup</code> instance instead.</p><h3>StopwatchRecorderContext:</h3>								</div>
				</div>
				<div class="elementor-element elementor-element-7c4cc2e elementor-widget elementor-widget-code-block-for-elementor" data-id="7c4cc2e" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

using System;

namespace Orcsune.Core.ODebug.Profiling {
    /// &lt;summary&gt;
    /// Create a &#039;using&#039; context for a StopwatchRecorder.
    /// Automatically log the time and reset the stopwatch at the end of scope.
    /// &lt;/summary&gt;
    public class StopwatchRecorderContext : IDisposable {
        private StopwatchRecorder recorder;
        public StopwatchRecorderContext(StopwatchRecorder recorder) {
            this.recorder = recorder;
            recorder.Start();
        }

        public static implicit operator StopwatchRecorder(StopwatchRecorderContext context) =&gt; context.recorder;

        /// &lt;summary&gt;
        /// Intended to be used in &#039;using&#039; context.
        /// Times the using block and logs the final time
        /// when disposed.
        /// &lt;/summary&gt;
        public void Dispose()
        {
            recorder.LogTime();
            recorder.Reset();
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-16eb450 elementor-widget elementor-widget-text-editor" data-id="16eb450" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>The <code>StopwatchRecorderContext</code> class is just a context wrapper around a <code>StopwatchRecorder</code>. This just allows you to wrap measured code with a <code>using</code> context block.</p>								</div>
				</div>
					</div>
				</div>
					</details>
					</div>
						</div>
				</div>
				<div class="elementor-element elementor-element-9dd1148 elementor-widget elementor-widget-text-editor" data-id="9dd1148" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>Example Usage:</h2>								</div>
				</div>
				<div class="elementor-element elementor-element-c41e75d elementor-widget elementor-widget-code-block-for-elementor" data-id="c41e75d" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>using Orcsune.Core.ODebug.Profiling;

class Program {
    static void Main(String[] args) {
        Console.WriteLine(&quot;Starting&quot;);
        int count = 0;
        string key1 = &quot;Gameplay/Movement&quot;;
        string key2 = &quot;Gameplay/Collision/Gather&quot;;
        string key3 = &quot;Gameplay/Collision/Update&quot;;
        // Do some work and record timing with StopwatchRecorder
        // Movement
        StopwatchRecorder movementRecorder = StopwatchTracker.GetRecorder(key1);
        movementRecorder.Restart();
        for (int i = 0; i &lt; 100000000; i++) {
            // Processing here...
            count += 1;
        }
        movementRecorder.LogTime();
        movementRecorder.Reset();
        
        // Use the StopwatchRecorderContext to record times
        using (var _ = StopwatchTracker.GetRecorderContext(key2)) {
            for (int i = 0; i &lt; 100000000; i++) {
                // Processing here...
                count += 1;
            }
        }
        using (var _ = StopwatchTracker.GetRecorderContext(key3)) {
            for (int i = 0; i &lt; 100000000; i++) {
                // Processing here...
                count += 1;
            }
        }
        
        // Use the timings from groups or individual recorders
        string keyGroupGameplay = &quot;Gameplay&quot;;
        string keyGroupCollision = &quot;Gameplay/Collision&quot;;
        Console.WriteLine($&quot;Total: {StopwatchTracker.unifiedGroup.GetInfo().lastTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay: {StopwatchTracker.GetInfoProvider(keyGroupGameplay).GetInfo().lastTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Movement: {StopwatchTracker.GetInfoProvider(key1).GetInfo().lastTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Collision: {StopwatchTracker.GetInfoProvider(keyGroupCollision).GetInfo().lastTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Collision/Gather: {StopwatchTracker.GetInfoProvider(key2).GetInfo().lastTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Collision/Update: {StopwatchTracker.GetInfoProvider(key3).GetInfo().lastTime}ms&quot;);
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-15a01d6f elementor-widget elementor-widget-text-editor" data-id="15a01d6f" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Alternatively, you can utilize the window averaging functionality of StopwatchRecorders to run multiple trials (or run once each frame, for example) to get an average time for parts of your code:</p>								</div>
				</div>
				<div class="elementor-element elementor-element-63ca7bd elementor-widget elementor-widget-code-block-for-elementor" data-id="63ca7bd" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>using Orcsune.Core.ODebug.Profiling;

class Program {
    static void Main(String[] args) {
        Console.WriteLine(&quot;Starting&quot;);
        int count = 0;
        int windowSize = 30;
        string key1 = &quot;Gameplay/Movement&quot;;
        string key2 = &quot;Gameplay/Collision/Gather&quot;;
        string key3 = &quot;Gameplay/Collision/Update&quot;;
        // Do some work and record timing with StopwatchRecorder
        // Movement
        StopwatchRecorder movementRecorder = StopwatchTracker.GetRecorder(key1);
        for (int j = 0; j &lt; windowSize; j++)
        {
            movementRecorder.Restart();
            for (int i = 0; i &lt; 10000000; i++)
            {
                // Processing here...
                count += 1;
            }
            movementRecorder.LogTime();
            movementRecorder.Reset();
        }

        // Use the StopwatchRecorderContext to record times
        for (int j = 0; j &lt; windowSize; j++)
        {
            using (var _ = StopwatchTracker.GetRecorderContext(key2))
            {
                for (int i = 0; i &lt; 10000000; i++)
                {
                    // Processing here...
                    count += 1;
                }
            }
        }
        for (int j = 0; j &lt; windowSize; j++)
        {
            using (var _ = StopwatchTracker.GetRecorderContext(key3))
            {
                for (int i = 0; i &lt; 10000000; i++)
                {
                    // Processing here...
                    count += 1;
                }
            }
        }
        
        // Use the timings from groups or individual recorders
        string keyGroupGameplay = &quot;Gameplay&quot;;
        string keyGroupCollision = &quot;Gameplay/Collision&quot;;
        Console.WriteLine($&quot;Total: {StopwatchTracker.unifiedGroup.GetInfo().avgTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay: {StopwatchTracker.GetInfoProvider(keyGroupGameplay).GetInfo().avgTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Movement: {StopwatchTracker.GetInfoProvider(key1).GetInfo().avgTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Collision: {StopwatchTracker.GetInfoProvider(keyGroupCollision).GetInfo().avgTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Collision/Gather: {StopwatchTracker.GetInfoProvider(key2).GetInfo().avgTime}ms&quot;);
        Console.WriteLine($&quot;Gameplay/Collision/Update: {StopwatchTracker.GetInfoProvider(key3).GetInfo().avgTime}ms&quot;);
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-014ca1b elementor-widget elementor-widget-text-editor" data-id="014ca1b" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Conclusion</h2><p>Easily measure and group timings using a key-based hierarchy of Stopwatches using this approach.</p>								</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/stopwatch-groups-in-c/">Stopwatch Groups in C#</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>C#ish Index</title>
		<link>https://orcsune.com/blog/cish-index/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Sun, 27 Jul 2025 17:36:59 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4826</guid>

					<description><![CDATA[<p>C#ish Index Index for random C# implementations and topics. Posted: July 27, 2025 Last Updated: July 27, 2025 UPDATES (Chronological): Sliding Windows in C# Stopwatch Groups in C# BY TOPIC: Structures/Data: Sliding Window Graph(s) Stopwatch Groups Texture Atlas Mass-Processing: How to (quickly) roll (1081) dice (in Unity).</p>
<p>The post <a href="https://orcsune.com/blog/cish-index/">C#ish Index</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4826" class="elementor elementor-4826">
				<div class="elementor-element elementor-element-17fe234f e-flex e-con-boxed e-con e-parent" data-id="17fe234f" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-78282025 elementor-widget elementor-widget-text-editor" data-id="78282025" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />C#ish Index<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p>Index for random C# implementations and topics.</p><p><strong>Posted:</strong> July 27, 2025</p><p><strong>Last Updated:</strong> July 27, 2025</p><h3>UPDATES (Chronological):</h3><ul><li><a href="https://orcsune.com/blog/sliding-window-in-c/">Sliding Windows in C#</a></li><li><a href="https://orcsune.com/blog/stopwatch-groups-in-c/">Stopwatch Groups in C#</a></li></ul><h3>BY TOPIC:</h3><h4>Structures/Data:</h4><ul><li><a href="https://orcsune.com/blog/sliding-window-in-c/">Sliding Window</a></li><li>Graph(s)</li><li><a href="https://orcsune.com/blog/stopwatch-groups-in-c/">Stopwatch Groups</a></li><li>Texture Atlas</li></ul><h4>Mass-Processing:</h4><ul><li>How to (quickly) roll (10<sup>81</sup>) dice (in Unity).</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-256e22fe elementor-widget-divider--view-line elementor-widget elementor-widget-divider" data-id="256e22fe" data-element_type="widget" data-e-type="widget" data-widget_type="divider.default">
				<div class="elementor-widget-container">
							<div class="elementor-divider">
			<span class="elementor-divider-separator">
						</span>
		</div>
						</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/cish-index/">C#ish Index</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Sliding Window in C#</title>
		<link>https://orcsune.com/blog/sliding-window-in-c/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Sun, 27 Jul 2025 17:34:40 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4831</guid>

					<description><![CDATA[<p>Sliding Window in C# Posted: July 27, 2025 Last Updated: July 27, 2025 Sliding Window in C# First, what do I mean by a sliding window? I just mean a structure that contains some fixed-length array of data that can easily be updated by cycling new values in and pushing the oldest values out. Now, [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/sliding-window-in-c/">Sliding Window in C#</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4831" class="elementor elementor-4831">
				<div class="elementor-element elementor-element-608d24cc e-flex e-con-boxed e-con e-parent" data-id="608d24cc" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-338d2256 elementor-widget elementor-widget-text-editor" data-id="338d2256" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Sliding Window in C#<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p><strong>Posted:</strong> July 27, 2025</p><p><strong>Last Updated:</strong> July 27, 2025</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-84e83e9 e-flex e-con-boxed e-con e-parent" data-id="84e83e9" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-210a16a0 elementor-widget elementor-widget-text-editor" data-id="210a16a0" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><span style="text-decoration: underline;">Sliding Window in C#</span></p><p>First, what do I mean by a sliding window? I just mean a structure that contains some fixed-length array of data that can easily be updated by cycling new values in and pushing the oldest values out.</p><p>Now, I like sliding windows. There&#8217;s just something about them that makes me happy. Are you trying to plot the N most recent datapoints in a moving profiling/timing chart? Sliding window could be good. Are you trying to maintain a real-time running average over a certain number of datapoints? Sliding window could be good again. There are lots of ways you can use a little array.</p><p>If you&#8217;re like me, and just want code, here it is for a basic sliding window and another variant window that tracks additional statistics about the window values:</p><h2>SlidingWindow&lt;T&gt;:</h2>								</div>
				</div>
				<div class="elementor-element elementor-element-f083783 elementor-widget elementor-widget-code-block-for-elementor" data-id="f083783" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

namespace Orcsune.Core.Data {
    /// &lt;summary&gt;
    /// SlidingWindow class that allows new values to
    /// cycle into a small array, pushing out the
    /// oldest values as space runs out. 
    /// &lt;/summary&gt;
    /// &lt;typeparam name=&quot;T&quot;&gt;The type of data contained in the window.&lt;/typeparam&gt;
    public class SlidingWindow&lt;T&gt;
    {
        public int windowSize =&gt; data.Length;
        public T[] data;
        public int counter;

        public SlidingWindow(int windowSize)
        {
            ChangeWindowSize(windowSize);
        }
        
        /// &lt;summary&gt;
        /// Cycle a new value into the window, replacing
        /// whatever oldest value previously occupied the
        /// index.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;value&quot;&gt;Incoming data&lt;/param&gt;
        /// &lt;returns&gt;Replaced value&lt;/returns&gt;
        public virtual T LogValue(T value)
        {
            T old = data[counter];
            data[counter] = value;
            counter = (counter + 1) % windowSize;
            return old;
        }

        /// &lt;summary&gt;
        /// Return an array of the window values in order
        /// from oldest to newest.
        /// &lt;/summary&gt;
        /// &lt;returns&gt;Array of window data.&lt;/returns&gt;
        public T[] OrderedValues()
        {
            T[] values = new T[windowSize];
            int tc = 0;
            for (int i = counter; i &lt; windowSize; i++)
            {
                values[tc] = data[i];
                tc += 1;
            }
            for (int i = 0; i &lt; counter; i++)
            {
                values[tc] = data[i];
                tc += 1;
            }
            return values;
        }

        /// &lt;summary&gt;
        /// Change the size of the window. This also
        /// resets/erases the current data in the window.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;newSize&quot;&gt;New window size&lt;/param&gt;
        public virtual void ChangeWindowSize(int newSize)
        {
            if (newSize &lt; 2) { return; }
            counter = 0;
            data = new T[newSize];
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-7f1c0c6 elementor-widget elementor-widget-text-editor" data-id="7f1c0c6" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>ComparableSlidingWindow:</h2>								</div>
				</div>
				<div class="elementor-element elementor-element-81801b3 elementor-widget elementor-widget-code-block-for-elementor" data-id="81801b3" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>// * * * * *
// Original Author: Orcsune
// Date: July 27, 2025
// License: MIT License
// * * * * *

using System;

namespace Orcsune.Core.Data {
    /// &lt;summary&gt;
    /// Specialized SlidingWindow that tracks min and max
    /// values within the current window data using the
    /// IComparable interface enforced on the window type.
    /// &lt;/summary&gt;
    /// &lt;typeparam name=&quot;T&quot;&gt;IComparable&lt;/typeparam&gt;
    public class ComparableSlidingWindow&lt;T&gt; : SlidingWindow&lt;T&gt; where T : IComparable
    {
        public T maxValue { get; private set; }
        int maxValueIndex;
        public T minValue { get; private set; }
        int minValueIndex;

        T defaultMinValue;
        T defaultMaxValue;

        /// &lt;summary&gt;
        /// Constructor. The default max and min values are those
        /// that should be used to compare against other values
        /// when the old max/min value is cycled out of the window.
        /// Example: if typeof(T) == float, then default min and max
        /// could be float.MaxValue and float.MinValue respectively to
        /// make sure that the new value coming into the window always
        /// &quot;wins&quot; the comparison.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;windowSize&quot;&gt;Window size&lt;/param&gt;
        /// &lt;param name=&quot;defaultMinValue&quot;&gt;Default Min&lt;/param&gt;
        /// &lt;param name=&quot;defaultMaxValue&quot;&gt;Default Max&lt;/param&gt;
        public ComparableSlidingWindow(int windowSize, T defaultMinValue, T defaultMaxValue) : base(windowSize)
        {
            this.defaultMinValue = defaultMinValue;
            this.defaultMaxValue = defaultMaxValue;
        }

        /// &lt;summary&gt;
        /// Cycle a new value into the window, replacing the
        /// oldest one if full. Also performs incremental
        /// update to recalculate the current max and min
        /// values within the current window.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;value&quot;&gt;Incoming data&lt;/param&gt;
        /// &lt;returns&gt;Replaced value&lt;/returns&gt;
        public override T LogValue(T value)
        {
            T old = data[counter];
            data[counter] = value;
            // If we are replacing the old maximum value,
            // then we must re-iterate the window to find
            // the new max value.
            if (maxValueIndex == counter)
            {
                maxValue = defaultMaxValue;
                for (int i = 0; i &lt; data.Length; i++)
                {
                    if (data[i].CompareTo(maxValue) &gt; 0)
                    {
                        maxValue = data[i]; maxValueIndex = i;
                    }
                }
            }
            else
            {
                // Otherwise, we just have to check whether the
                // new value is larger than the current max.
                if (data[counter].CompareTo(maxValue) &gt; 0)
                {
                    maxValue = data[counter];
                    maxValueIndex = counter;
                }
            }
            // If we are replacing the old minimum value,
            // then we must re-iterate the window to find
            // the new min value.
            if (minValueIndex == counter)
            {
                minValue = defaultMinValue;
                for (int i = 0; i &lt; data.Length; i++)
                {
                    if (data[i].CompareTo(minValue) &lt; 0)
                    {
                        minValue = data[i]; minValueIndex = i;
                    }
                }
            }
            else
            {
                // Otherwise, we just have to check whether the
                // new value is less than the current min.
                if (data[counter].CompareTo(minValue) &lt; 0)
                {
                    minValue = data[counter];
                    minValueIndex = counter;
                }
            }
            counter = (counter + 1) % windowSize;
            return old;
        }

        /// &lt;summary&gt;
        /// Change the size of the window. This also
        /// resets/erases the current data in the window.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;newSize&quot;&gt;New window size&lt;/param&gt;
        public override void ChangeWindowSize(int newSize)
        {
            if (newSize &lt; 2) { return; }
            base.ChangeWindowSize(newSize);
            minValue = defaultMinValue;
            maxValue = defaultMaxValue;
            minValueIndex = 0;
            maxValueIndex = 0;
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-3dadc1b elementor-widget elementor-widget-text-editor" data-id="3dadc1b" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>Example Usage:</h2>								</div>
				</div>
				<div class="elementor-element elementor-element-4e43748 elementor-widget elementor-widget-code-block-for-elementor" data-id="4e43748" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>
using System;
using Orcsune.Core.Data;

class Program {
    static void Main(String[] args) {
        // New window of size 10
        ComparableSlidingWindow&lt;double&gt; fpsWindow = new ComparableSlidingWindow&lt;double&gt;(10, double.MaxValue, double.MinValue);
        // Example data points (15 values)
        double[] fpsData = new double[] {
            60, 55, 54, 57, 58,  58, 59, 59, 59, 59,  42, 57, 58, 59, 59
        };
        // Fill our sliding window
        foreach (double fps in fpsData) {
            fpsWindow.LogValue(fps);
        }
        // Calculate and print statistics
        double avgFPS = 0;
        foreach (double fps in fpsWindow.data) {
            avgFPS += fps;
        }
        avgFPS /= fpsWindow.windowSize;
        Console.WriteLine($&quot;FPS Statistics for the past {fpsWindow.windowSize} records:&quot;);
        Console.WriteLine($&quot;\tMin FPS: {fpsWindow.minValue}&quot;);
        Console.WriteLine($&quot;\tMax FPS: {fpsWindow.maxValue}&quot;);
        Console.WriteLine($&quot;\tAvg FPS: {avgFPS}&quot;);
        // FPS Statistics for the past 10 records:
        //      Min FPS: 42
        //      Max FPS: 59
        //      Avg FPS: 56.9
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-dcc81f8 elementor-widget elementor-widget-text-editor" data-id="dcc81f8" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>Other Windows:</h2><p>There are plenty of things you could add to make these sliding windows better. For example, newer C# versions seem to allow you to <a href="https://learn.microsoft.com/en-us/dotnet/standard/generics/math">specify numeric interfaces</a>, which could allow for additional metrics to be tracked incrementally within a window (such as the sum or average value, for example). You could even try making your own specialized sliding windows to accommodate whatever funky data types you make.</p>								</div>
				</div>
				<div class="elementor-element elementor-element-2aaffda elementor-widget elementor-widget-code-block-for-elementor" data-id="2aaffda" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-okaidia' data-show-toolbar='yes'><code class='language-csharp'>
namespace Orcsune.Core.Data {
    /// &lt;summary&gt;
    /// Specialized SlidingWindow that tracks numerical
    /// statistics from the window values.
    /// &lt;/summary&gt;
    public class NumericSlidingWindow : ComparableSlidingWindow&lt;double&gt;
    {
        public double sum { get; private set; }
        public double average { get; private set; }

        public NumericSlidingWindow(int windowSize, double defaultMinValue, double defaultMaxValue)
            : base(windowSize, defaultMinValue, defaultMaxValue)
        {
            sum = 0;
            average = 0;
        }

        public override double LogValue(double value)
        {
            // Track the min/max value of window
            double old = base.LogValue(value);
            // Alter other statistics
            sum -= old;
            sum += value;
            average = sum / windowSize;
            return old;
        }
        
        public override void ChangeWindowSize(int newSize)
        {
            if (newSize &lt; 2) { return; }
            base.ChangeWindowSize(newSize);
            sum = 0;
            average = 0;
        }
    }
}</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-1d48c71 elementor-widget elementor-widget-text-editor" data-id="1d48c71" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>Implementation:</h2><p>The implementation is pretty self-explanatory (to me) given the code. Essentially, the <code>SlidingWindow</code> consists of a set-length array to hold logged values, and a counter to keep track of the next index to be replaced. When logging a new value, the value at <code>counter</code> is replaced with the new value, and then <code>counter</code> is incremented. When providing the ordered values, it simply copies values into a new array starting at <code>counter</code> and then looping through all the original data back until it reaches <code>counter</code>.</p>								</div>
				</div>
				<div class="elementor-element elementor-element-23908fb6 elementor-widget elementor-widget-text-editor" data-id="23908fb6" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Video Demos:</h2><p>Here is a quick example of me using SlidingWindow to track and display various timing values in a debug menu over time. Each line is built up from data points contained in SlidingWindows. Since I don&#8217;t really need to keep track of all the collected data for the entire session and I want window calculations to be as minimal as possible, this is the perfect use case:</p>								</div>
				</div>
				<div class="elementor-element elementor-element-a5abbb7 elementor-widget elementor-widget-image" data-id="a5abbb7" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img fetchpriority="high" decoding="async" width="800" height="254" src="https://orcsune.com/wp-content/uploads/2025/01/profile_bullets_compare.webp" class="attachment-large size-large wp-image-3305" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/01/profile_bullets_compare.webp 800w, https://orcsune.com/wp-content/uploads/2025/01/profile_bullets_compare-300x95.webp 300w, https://orcsune.com/wp-content/uploads/2025/01/profile_bullets_compare-768x244.webp 768w" sizes="(max-width: 800px) 100vw, 800px" />															</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/sliding-window-in-c/">Sliding Window in C#</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Texture Atlas, Settings, and More &#8211; BB &#8211; Update 17</title>
		<link>https://orcsune.com/blog/texture-atlas-settings-and-more-bb-update-17/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Sun, 29 Jun 2025 15:17:40 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4776</guid>

					<description><![CDATA[<p>Texture Atlas, Settings, and More &#8211; BB &#8211; Update 17 Posted: June 29, 2025 Last Updated: June 29, 2025 Texture Atlas, Settings, and More &#8211; BB &#8211; Update 17 Timeframe: June 2, 2025 &#8211; June 28, 2025 !!! Jump to Visual Demos !!! Welcome. It feels like these updates just get more wordy and tedious [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/texture-atlas-settings-and-more-bb-update-17/">Texture Atlas, Settings, and More &#8211; BB &#8211; Update 17</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4776" class="elementor elementor-4776">
				<div class="elementor-element elementor-element-7ef87021 e-flex e-con-boxed e-con e-parent" data-id="7ef87021" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-714dff1e elementor-widget elementor-widget-text-editor" data-id="714dff1e" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Texture Atlas, Settings, and More &#8211; BB &#8211; Update 17<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p><strong>Posted:</strong> June 29, 2025</p><p><strong>Last Updated:</strong> June 29, 2025</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-4192ed08 e-flex e-con-boxed e-con e-parent" data-id="4192ed08" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-5b8e03a7 elementor-widget elementor-widget-text-editor" data-id="5b8e03a7" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><span style="text-decoration: underline;">Texture Atlas, Settings, and More &#8211; BB &#8211; Update 17</span></p><p><span style="text-decoration: underline;">Timeframe: June 2, 2025 &#8211; June 28, 2025</span></p><p><a href="#videodemos">!!! Jump to Visual Demos !!!</a></p><p>Welcome. It feels like these updates just get more wordy and tedious each time, but I promise it&#8217;s because there&#8217;s a lot that went into the actual development. Sometimes those developments are only interesting to me, though. This time around, focus is mostly on a generalized settings menu and revising my texture atlas approach/fixing some texture memory leaks and other memory problems. Oh boy, fun!</p>								</div>
				</div>
				<div class="elementor-element elementor-element-5c73bc2 elementor-widget elementor-widget-text-editor" data-id="5c73bc2" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>TextureAtlas (And Memory):</h3><ul><li><strong>MultiTextureAtlas:</strong> contains multiple separate atlases to hopefully reduce precision-related problems when calculating UVs.</li><li><strong>TextureExtensions</strong> added for Pad, Binarize, Scale, ScaleToSize, Grayscale, and Chromakey.</li><li><strong>Memory:</strong> Looking at the profiler, there textures seem to take up an excessive amount of space. Some is unavoidable (textures for individual sprites and texture atlas), but it still seems large. Found a bunch of spots where textures were being made but not destroyed later.</li><li><strong>Artifacts:</strong> Testing atlas with various images causes the <a href="https://orcsune.com/blog/texture-atlas-overhaul-bb-update-7/">weird box artifacts</a> again. After tons of attempts, I finally found the problem: mipmaps. I thought I had set the mipmap count to 1 (basically none), but it was being set higher when adding textures from file to the texture atlas because the loaded textures had mipmaps. Doing this also improved the memory situation because the texture atlas no longer had to store mipmaps.</li><li><strong>SpriteAssetManager</strong> improved to reuse certain textures and destroy unecessary textures when opening a new project. Texture count should now break even when opening a project (instead of leaking).</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-68892cf elementor-widget elementor-widget-text-editor" data-id="68892cf" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>SettingsManager:</h3><ul><li><strong>SettingsManager:</strong> Ported and heavily changed from a different in-development game.</li><li><strong>Subclass</strong> SettingsManager and override various methods to create a custom settings menu easily:<ul><li><strong>AddSettingListeners:</strong> Adds listeners to given stats in the given GamePrefs. These listeners will be invoked on any changes to the settings (temporary and final).</li><li><strong>AddTempSettingListeners:</strong> Adds listeners to temporary GamePrefs that change as the user alters settings.</li></ul></li><li><strong>GamePrefs</strong> class represents the current settings for the game. Subclass GamePrefs to make a set of custom settings:<ul><li><strong>AddPresets:</strong> Add presets to your settings (volume, video, control, or custom presets) to add new setting names/types (float, int, bool, string) to the GamePrefs.</li><li><strong>AddSettings:</strong> Like AddPresets, except for adding individual/custom setting names/types to the GamePrefs.</li><li><strong>Reset:</strong> Define what happens when settings reset.</li></ul></li><li><strong>IGamePrefs</strong> interface defines GetSetting&lt;T&gt; for retrieving a setting value of type T.</li><li><strong>GamePrefTypeMap</strong> is a dictionary mapping for a single type (int, float, bool, string). GamePrefs has one map for each of those types.</li><li><strong>RegisterSettingCallback</strong> or <strong>RemoveSettingChangeListener</strong> from outside the SettingsManager to add or remove (respectively) listeners that are invoked during changes to the corresponding setting.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-cc22705 elementor-widget elementor-widget-text-editor" data-id="cc22705" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>SettingMenuInspector:</h3><ul><li><strong>SettingMenuInspector</strong> is like BuilderInspector except for SettingsManagers. Pass a SettingMenuInspector into a SettingManager&#8217;s SetupUI method to have it build a settings menu according to the SettingsManager&#8217;s setting keys using sliders, toggles, and buttons.</li><li><strong>SettingInput</strong> abstract base class for each input type allowed in settings.<ul><li><strong>SettingSlider:</strong> Hook setting to int or float.</li><li><strong>SettingDropdown:</strong> Hook setting to int or string.</li><li><strong>SettingToggle:</strong> Hook setting to bool.</li><li><strong>SettingButton:</strong> Hook up an action to a button.</li></ul></li></ul>								</div>
				</div>
		<div class="elementor-element elementor-element-59c27e7f e-grid e-con-full e-con e-child" data-id="59c27e7f" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-c5315ba e-con-full e-flex e-con e-child" data-id="c5315ba" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-277a4ff elementor-widget elementor-widget-image" data-id="277a4ff" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img decoding="async" width="300" height="300" src="https://orcsune.com/wp-content/uploads/2025/06/setting_menu-300x300.png" class="attachment-medium size-medium wp-image-4791" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/06/setting_menu-300x300.png 300w, https://orcsune.com/wp-content/uploads/2025/06/setting_menu-150x150.png 150w, https://orcsune.com/wp-content/uploads/2025/06/setting_menu.png 583w" sizes="(max-width: 300px) 100vw, 300px" />															</div>
				</div>
				<div class="elementor-element elementor-element-7f8b0616 elementor-widget elementor-widget-text-editor" data-id="7f8b0616" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Settings Menu which was assembled programatically.</p>								</div>
				</div>
				</div>
				</div>
				<div class="elementor-element elementor-element-6288caf elementor-widget elementor-widget-text-editor" data-id="6288caf" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>AudioManager:</h3><ul><li><strong>AudioManager</strong> class is responsible for being a centralized place to register and dispatch changes to different audio sources.</li><li><strong>AudioManager</strong> can register 3 different types of audio sources:<ul><li><strong>Music:</strong> For sources that should play music.</li><li><strong>Effect:</strong> General purpose &#8220;sound effect&#8221; sources.</li><li><strong>Voice:</strong> For visual novel voice-acting lines.</li></ul></li><li><strong>MusicManager</strong> with 2 audio sources for fading between tracks.</li><li><strong>AudioSystem</strong> (bad) attempts to use AudioManager to update its audio source volume in accordance to the effectsVolume.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-82bcf3f elementor-widget elementor-widget-text-editor" data-id="82bcf3f" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>Play Mode:</h3><p>Started messing around with making a menu scene and doing some scene transition stuff.</p><ul><li><strong>Core Builder:</strong> Needed a stripped-down version of the builder object so that when loading a project to play, it could still create all the prefab Entities necessary to have the level.</li><li><strong>LevelHelper</strong> utilities for starting and destroying levels.</li><li><strong>PlayProjectLoader</strong> creates a reduced builder object, runs project setup, and gathers up builder registries.</li><li><strong>LoadedProject</strong> represents a loaded project (mostly to organize its registries).</li><li><strong>LevelSelectManager</strong> manages inter-level loading and processes.</li><li><strong>New Scenes:</strong> Created some linked scenes (main menu, level builder scene, level select scene).</li><li><strong>SceneControl</strong> wraps SceneManager to control some visual transition effects.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-52494c2 elementor-widget elementor-widget-text-editor" data-id="52494c2" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>Health System Changes:</h3><ul><li><strong>HealthChangeElement</strong> buffer tracks instances of changed health.</li><li><strong>HealthSystem</strong> now provides some custom jobs that do specific things when player and enemy change health.</li><li><strong>HealthChangeGatherJob</strong> consolidates all health changes (from buffer) for each entity and each health change type (active/passive damage/heal).</li><li><strong>HealthChangeManageJob</strong> manages changing final health, setting invincibility, and marking dead entities to kill.</li><li><strong>Bullet collisions</strong> now just add HealthChangeElement to buffer on hit. Collisions keep track of invincibility internally, but do not write it back to the health component.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-68ca683 elementor-widget elementor-widget-text-editor" data-id="68ca683" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>New Trigger Processors:</h3><ul><li><strong>PlayerTriggerProcessor:</strong> apply preset, respawn, invincible, heal, damage.</li><li><strong>AudioTriggerProcessor:</strong> set volume, set track, stop track, start track.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-b2ef00f elementor-widget elementor-widget-text-editor" data-id="b2ef00f" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>Miscellaneous:</h3><ul><li><strong>PauseManager</strong>, a central place to track multiple IPauseSources.</li><li><strong>IPauseSource</strong>, a thing that can cause a game pause. Usually register in OnEnable and deregister in OnDisable.</li><li><strong>Fixed TranslationWidget</strong> dragging action. Enemy path multi-edit panel now only affects selected nodes instead of all nodes.</li><li>Added <strong>exception button</strong> to notice preview (can click this button to toggle exception log).</li><li><strong>Notice preview</strong> is now movable.</li><li><strong>Top menu bar</strong> now toggleable with Escape key.</li><li><strong>VisualsSystem</strong> processes a bunch of visual-related jobs (for the future).</li><li>Sprite <strong>flashes during invincibility</strong>.</li><li><strong>GameUI</strong> for providing a single API for changing UI values and activating UI events: GameUIBarSlider, GameUIText, GameUIImageStatus (think original DOOM portrait), GameUIPopupImage for fun little images popping up.</li><li><strong>ScoreManager</strong> static class keeps track of score. Change, set, or zero.</li><li><strong>ScoreComponent</strong> on anything that can add score.</li><li><strong>ScoreOnHit and ScoreOnKill</strong> used in ScoreSystem to add score based on health changes and whether something is scheduled to die.</li><li><strong>Bugfix:</strong> Fixed problems in UnpackMounts where it does not properly fail during project loading when the dependency prefabs (waves) for the level have not been built yet. Passing a new OnFieldRetrievalFail argument fixes the problem.</li><li><strong>Bugfix:</strong> Improvements to reflection to NOT skip Entity fields nested in SimpleList fields.</li><li><strong>Bugfix:</strong> InteractiveDropdown_GameTrigger fixed to run even if awake is not called.</li><li><strong>Bugfix:</strong> Player sprite on respawn was not right. Problem was that the default sprite info was never initialized, so the default data used in NewSubject is basically zeroed/default. Now check the default sprite info for the builder category.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-5115261c elementor-widget elementor-widget-text-editor" data-id="5115261c" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Video Demos</h2>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-4e0bcbe e-flex e-con-boxed e-con e-parent" data-id="4e0bcbe" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-fae9def e-grid e-con-full e-con e-child" data-id="fae9def" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-c79b6a3 e-con-full e-flex e-con e-child" data-id="c79b6a3" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-5232459 elementor-widget elementor-widget-video" data-id="5232459" data-element_type="widget" data-e-type="widget" data-settings="{&quot;video_type&quot;:&quot;hosted&quot;,&quot;controls&quot;:&quot;yes&quot;}" data-widget_type="video.default">
				<div class="elementor-widget-container">
							<div class="e-hosted-video elementor-wrapper elementor-open-inline">
					<video class="elementor-video" src="https://orcsune.com/wp-content/uploads/2025/06/level_select.mp4" controls="" preload="metadata" controlsList="nodownload"></video>
				</div>
						</div>
				</div>
				<div class="elementor-element elementor-element-b39f17e elementor-widget elementor-widget-text-editor" data-id="b39f17e" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>A full view of the (very incomplete) scene transition, level selection, GameUI and scoring. When a scene transition occurs, you may choose whether or not the loading bars are visible. Selecting &#8220;Play&#8221; brings up the level select scene, where you first choose a project to play, and after loading, pick a level from that project. On the right is the GameUI. The buttons are just debug testing, but the health bars and score text are placed and updated programatically.</p>								</div>
				</div>
				</div>
				</div>
				<div class="elementor-element elementor-element-102ea8f1 elementor-widget elementor-widget-wp-widget-nav_menu" data-id="102ea8f1" data-element_type="widget" data-e-type="widget" data-widget_type="wp-widget-nav_menu.default">
				<div class="elementor-widget-container">
					<div class="menu-bulletblognavmenu-container"><ul id="menu-bulletblognavmenu" class="menu"><li id="menu-item-2056" class="menu-item menu-item-type-post_type menu-item-object-post menu-item-2056"><a href="https://orcsune.com/blog/bulletly-bulletin-index/">Bulletly Bulletin Index</a></li>
<li id="menu-item-1890" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-1890"><a href="https://orcsune.com/category/bb/">Bulletly Bulletin</a></li>
</ul></div>				</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/texture-atlas-settings-and-more-bb-update-17/">Texture Atlas, Settings, and More &#8211; BB &#8211; Update 17</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		<enclosure url="https://orcsune.com/wp-content/uploads/2025/06/level_select.mp4" length="15993603" type="video/mp4" />

			</item>
		<item>
		<title>Bullet Hell Utilities &#8211; BB &#8211; Update 16</title>
		<link>https://orcsune.com/blog/bullet-hell-utilities-bb-update-16/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Thu, 05 Jun 2025 17:54:49 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4598</guid>

					<description><![CDATA[<p>Bullet Hell Utilities &#8211; BB &#8211; Update 16 Posted: June 5, 2025 Last Updated: June 5, 2025 Bullet Hell Utilities &#8211; BB &#8211; Update 16 Timeframe: May 24, 2025 &#8211; June 1, 2025 Here&#8217;s my first real update since the hiatus. I was working on another project making a Python backend and a frontend page [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/bullet-hell-utilities-bb-update-16/">Bullet Hell Utilities &#8211; BB &#8211; Update 16</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4598" class="elementor elementor-4598">
				<div class="elementor-element elementor-element-3f66c98 e-flex e-con-boxed e-con e-parent" data-id="3f66c98" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-1a532808 elementor-widget elementor-widget-text-editor" data-id="1a532808" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Bullet Hell Utilities &#8211; BB &#8211; Update 16<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p><strong>Posted:</strong> June 5, 2025</p><p><strong>Last Updated:</strong> June 5, 2025</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-7ec2e12e e-flex e-con-boxed e-con e-parent" data-id="7ec2e12e" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-3ceda5e6 elementor-widget elementor-widget-text-editor" data-id="3ceda5e6" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><span style="text-decoration: underline;">Bullet Hell Utilities &#8211; BB &#8211; Update 16</span></p><p><span style="text-decoration: underline;">Timeframe: May 24, 2025 &#8211; June 1, 2025</span></p><p>Here&#8217;s my first real update since the hiatus. I was working on another project making a Python backend and a frontend page with HTML, CSS, and JavaScript. I already knew Python, but all the frontend stuff was new to me. Now, I still don&#8217;t like JavaScript, but learning and making a project with it for the past 2 months has given me a new appreciation for treating functions as objects (it has also reinforced my love of statically typed languages).</p><p>It was fun, but now I&#8217;m back doing some work on the bullet hell. This update mostly deals with cleanup, but also has some cool additions, like moddable triggers and easy-bake utility panels for builders.</p><h2>MOD TRIGGERS:</h2><p>Triggers have been a part of the level builder for a while now, but it has been some time since I expanded upon them. The only available ones were just a few post-processing effects and a few gameplay utilities. While it was a powerful system with lots of potential entrypoints, it was also just limited to whatever I could think of. Thus, I moved towards a way to make the whole thing more modular.</p><ul><li><strong>IGameTriggerProcessor:</strong> Interface for a &#8220;trigger processor&#8221;. At its simplest, a trigger processor only needs a GUID and a method (ProcessTrigger) that takes an IGameTrigger and performs some action, typically using the trigger&#8217;s &#8220;process&#8221; field to choose the action.</li><li><strong>Changed IGameTrigger:</strong> Instead of having a hard-coded enum field for each type of processor, instead just has a GUID field (representing the IGameTriggerProcessor), and an int field (representing the action that processor should perform).</li><li><strong>ModTriggerEnumAttribute:</strong> Add this attribute to an enum to mark it as an &#8220;action set&#8221; for a specific IGameTriggerProcessor. In other words, it should represent what the IGameTrigger &#8220;process&#8221; field represents when paired with a specific processor.</li><li><strong>GameTriggerManager Processor Tracker:</strong> GameTriggerManager now keeps a static mapping between GUIDs and IGameTriggerProcessors.</li><li><strong>Added PostProcessingProcessor.</strong></li><li><strong>Added BulletHellBasicProcessor.</strong></li><li><strong>Dynamic Dropdowns:</strong> Dropdown inputs for triggers now update. Dropdown for process automatically reads the available IGameTriggerProcessors in the static map. Selecting a new processor also changes the action dropdown with the ModTriggerEnumAttribute enum corresponding to the selected processor.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-1b0f122 elementor-widget elementor-widget-image" data-id="1b0f122" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="1280" height="720" src="https://orcsune.com/wp-content/uploads/2025/06/dynamic_dropdown.webp" class="attachment-full size-full wp-image-4682" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/06/dynamic_dropdown.webp 1280w, https://orcsune.com/wp-content/uploads/2025/06/dynamic_dropdown-300x169.webp 300w, https://orcsune.com/wp-content/uploads/2025/06/dynamic_dropdown-1024x576.webp 1024w, https://orcsune.com/wp-content/uploads/2025/06/dynamic_dropdown-768x432.webp 768w, https://orcsune.com/wp-content/uploads/2025/06/dynamic_dropdown-800x450.webp 800w" sizes="(max-width: 1280px) 100vw, 1280px" />															</div>
				</div>
				<div class="elementor-element elementor-element-a1a955a elementor-widget elementor-widget-text-editor" data-id="a1a955a" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>UTILITY PANEL:</h2><ul><li>Utility Panel: <a href="https://orcsune.com/blog/bb-deep-dive-builders/">InGameBuilders</a> now each have a utility panel, which is simply a panel with buttons populated to activate helpful methods on the builder (e.g. reset preview, resave creations, preview cutscene script, etc.).</li><li>AddUtilities: InGameBuilders can now override the AddUtilities class to add helpful buttons along with limited inputs to the builder&#8217;s utility panel.</li><li>AddUtility: A series of methods in InGameBuilder that take a display string and a callback which helps easily populate the utility panel with buttons that perform custom actions.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-a99ec76 elementor-widget elementor-widget-image" data-id="a99ec76" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="1280" height="1280" src="https://orcsune.com/wp-content/uploads/2025/06/utility_panel.webp" class="attachment-full size-full wp-image-4681" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/06/utility_panel.webp 1280w, https://orcsune.com/wp-content/uploads/2025/06/utility_panel-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/06/utility_panel-1024x1024.webp 1024w, https://orcsune.com/wp-content/uploads/2025/06/utility_panel-150x150.webp 150w, https://orcsune.com/wp-content/uploads/2025/06/utility_panel-768x768.webp 768w, https://orcsune.com/wp-content/uploads/2025/06/utility_panel-600x600.webp 600w" sizes="(max-width: 1280px) 100vw, 1280px" />															</div>
				</div>
				<div class="elementor-element elementor-element-9706050 elementor-widget elementor-widget-text-editor" data-id="9706050" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>CLEANUP:</h2><h4>InGameBuilder Improvements:</h4><ul><li><strong>AddBufferElement:</strong> Generic method to add an element to a buffer. Calls necessary updates so you don&#8217;t need an individual method for updating specific buffers.</li><li><strong>RemoveBufferElementAt:</strong> Generic method to remove an element from a buffer at specified index. Like AddBufferElement.</li><li><strong>ClearBuffer:</strong> Generic method to clear a buffer. Like AddBufferElement.</li><li>Converted multiple subclass methods to use AddBufferElement, RemoveBufferElementAt, and ClearBuffer.</li><li><strong>RemoveBindingListElementAt:</strong> While RemoveBufferElementAt works on a passed in <a href="https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/components-buffer-introducing.html">DynamicBuffer</a>, this method works on a given list field of the binding object. This allows easier removal from areas that just have direct access to binding names rather than access to builders&#8217; buffers.</li><li><strong>UnpackMounts fixed</strong> to utilize an OnFieldRetrievalFail argument and properly succeed/fail at correct times.</li></ul><h4>WidgetManager Improvements:</h4><ul><li>Changed DestroyWidgetAndRemoveFromBinder to DestroyWidget. It still removes the widget bindings from the Binder.</li><li>Removed old DestroyWidget method. There is no case I can think of where you need to stop tracking a widget but still need its bindings in the Binder.</li><li>Changed DestroyWidgets to DestroyWidgetsWithInfo.</li><li>Changed DestroyWidgetsWithInfo to a private method. It was only used internally.</li><li>Removed DestroyWidgetsForIndexAndShiftBack. There were no usages of this method anywhere else.</li><li>Removed DestroyWidgetsForFieldAndIndexAndShiftBack. There were no usages of this method anywhere else.</li><li>Removed DestroyWidgetsForFieldAndIndex. There were no usages of this method anywhere else.</li><li>Removed DestroyWidgetsForIndex. After removing other methods, this one was unused.</li><li>Add DestroyWidgetsWhere, which allows custom Predicate&lt;WidgetInfo&gt;</li><li>Changed DestroyWidgetsForField and DestroyWidgetsWithTag to use DestroyWidgetsWhere with correct predicate.</li><li>Add CorrectWidgetIndicesAfter which takes in a Predicate&lt;WidgetInfo&gt; to determine ADDITIONAL predicates needed other than just having an index greater than input index for correcting indices.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-c130509 elementor-widget elementor-widget-text-editor" data-id="c130509" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2>MISCELLANEOUS:</h2><ul><li><strong>BindingObjectAnalyzer:</strong> Can now pass a Type into the analyzer, and it will tell you whether that type matches a top-level generic list type in the binding object, as well as return the field name if it matches.</li><li><strong>Enemy FiringPoint Fix:</strong> Fixed problem where enemy firing points were not spawning when spawned from a wave. Ended up being a problem where prefabification was not failing when it should have been. Fixed with UnpackMounts fix.</li><li><strong>Level Cutscene Fix:</strong> Cutscenes were not showing sprites when started from a level if the cutscene had not been played in the cutscene builder yet. Fixed with a LevelSetupSystem that iterates waves, looking for cutscenes and loading that data in the visual novel framework as the level starts.</li><li>Added some more options to the top toolbar.</li></ul><p>Also, here&#8217;s a neat pattern:</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-4121239 e-flex e-con-boxed e-con e-parent" data-id="4121239" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-93ed195 elementor-widget elementor-widget-image" data-id="93ed195" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="720" height="720" src="https://orcsune.com/wp-content/uploads/2025/06/spiral_maze.webp" class="attachment-large size-large wp-image-4689" alt="" />															</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/bullet-hell-utilities-bb-update-16/">Bullet Hell Utilities &#8211; BB &#8211; Update 16</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Mod/Runtime Builders &#8211; BB &#8211; Update 15</title>
		<link>https://orcsune.com/blog/mod-runtime-builders-bb-update-15/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Tue, 27 May 2025 02:19:56 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[Bullet Hell]]></category>
		<category><![CDATA[Game Design]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4532</guid>

					<description><![CDATA[<p>Mod &#38; Runtime Builders &#8211; BB &#8211; Update 15 Posted: May 26, 2025 Last Updated: May 26, 2025 Mod &#38; Runtime Builders &#8211; BB &#8211; Update 15 Timeframe: March 7, 2025 &#8211; March 16, 2025 !!! Jump to Visual Demos !!! Hello. Been a while. I know I said the last update and this update [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/mod-runtime-builders-bb-update-15/">Mod/Runtime Builders &#8211; BB &#8211; Update 15</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4532" class="elementor elementor-4532">
				<div class="elementor-element elementor-element-3b0c6a8b e-flex e-con-boxed e-con e-parent" data-id="3b0c6a8b" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-5e8d758 elementor-widget elementor-widget-text-editor" data-id="5e8d758" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Mod &amp; Runtime Builders &#8211; BB &#8211; Update 15<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p><strong>Posted:</strong> May 26, 2025</p><p><strong>Last Updated:</strong> May 26, 2025</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-548d0739 e-flex e-con-boxed e-con e-parent" data-id="548d0739" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-6094826b elementor-widget elementor-widget-text-editor" data-id="6094826b" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><span style="text-decoration: underline;">Mod &amp; Runtime Builders &#8211; BB &#8211; Update 15</span></p><p><span style="text-decoration: underline;">Timeframe: March 7, 2025 &#8211; March 16, 2025</span></p><p><a href="#videodemos">!!! Jump to Visual Demos !!!</a></p><p>Hello. Been a while. I know I said the last update and this update would come out in quick succession, but I got sidetracked by a completely different project that&#8217;s basically taken the past 2 months. This update brings InGameBuilders that can be added through mods and a ton of other fixes.</p><p>To be completely honest, I had to look back through all my notes to remember what I was even doing before. They helped a lot though, and I would basically be restating what was in them, so this week&#8217;s update will be a little different. I&#8217;m going to be really lazy and basically copy-past my notes from this time period in chronological order. A lot of it is shorthand or lingo or other arbitrary stuff that won&#8217;t really make sense if you&#8217;re not me, but I thought it would be an interesting look, nonetheless. I&#8217;ll even include some of my current thoughts/input on the notes here (in red). You probably won&#8217;t, but please enjoy my unfiltered (except for some formatting) notes:</p>								</div>
				</div>
				<div class="elementor-element elementor-element-b328292 elementor-widget elementor-widget-text-editor" data-id="b328292" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>March 7, 2025:</h4><p>&#8220;</p><p>Tested relative and original sprite settings and animations for size and position.<br />  -Size Setting:<br />    -20%v,100%r<br />    -50%r,50%v<br />    -10%o,10%o<br />  -Position Setting:<br />    -50%h, 50%r<br />    -50%r, 50%v<br />Implemented IAssetFileMap in AudioAssetManager.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-9f01ffc elementor-widget elementor-widget-text-editor" data-id="9f01ffc" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">This was the last of my work from the previous update adding animations to OVNS.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-912b555 elementor-widget elementor-widget-text-editor" data-id="912b555" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>&#8220;</p><p>Then I did a dumb thing and started to make a way to create InGameBuilders at runtime using config files.<br />This will be useful for modders, but also useful for me so I have to do even less scene setup.<br />I created the concept of a RuntimeBuilder, an abstract class which inherits from InGameBuilder and is mostly just responsible for instantiating a UI and figuring out where the config file is and using the config to set up the BuilderInspector.<br />So, in summary, I developed:<br />-RuntimeBuilder: above.<br />-RuntimeBuilderDataBehaviour: A connector that holds data to assign in the instantiated RuntimeBuilder upon building.<br />-RuntimeBuilderHelper: (NEEDS TESTING) Contains helper methods for setting up a RuntimeBuilder&#8217;s subject.<br />-RuntimeBuilderSubjectAuthoring: Authoring for a data object to help RuntimeBuilderHelper.<br />-RuntimeBuilderAttribute: Attribute to be placed on your RuntimeBuilder classes to automatically mark them for building.<br />-InGameBuilderManager_Runtime: an InGameBuilderManager subclass that first instantiates the RuntimeBuilders of the necessary types before moving on the normal functionality.<br />-ModBuilder: For later&#8230;<br />I still need to do a bunch of stuff to connect things up that I can&#8217;t really set in the inspector anymore:<br />Connect (widgetManager) to a child WidgetManager (you may AddComponent&lt;WidgetManager&gt; if none exist)<br />Connect (binder) to a Binder (you may add one if none exist). To add one, make sure to set the settings:<br />-findChildBindingGroups = true<br />-includeInactiveChildren = true<br />-noSetup = false<br />Connect (display) to a Display<br />Set your registry file name (registryFile)<br />Set your registry ID (registryID)<br />Connect (panelControl) to a PanelSwitchButtons (though this should be found during builder initialization anyways).<br />Set registry compression level (compressionLevel)</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-8c4e8b4 elementor-widget elementor-widget-text-editor" data-id="8c4e8b4" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">Here&#8217;s my initial brainstorm/outline for RuntimeBuilders which are just InGameBuilders that are put together at runtime instead of made beforehand.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-ecf5161 elementor-widget elementor-widget-text-editor" data-id="ecf5161" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>March 8, 2025:</h4><p>&#8220;</p><p>I&#8217;ve done a bunch of stuff already that I haven&#8217;t written down yet because I&#8217;m trying to fix something stupid.<br />Modified some code to fix some of the connectivity issues in the runtime builder.<br />One thing that was happening was that I was getting null reference exceptions when adding SingleWidgetInfo&#8217;s to the detailsWidgetInfo with AddWidgetInfo BuilderInspectorField&#8217;s DoSliderDetailsWidget call.<br />Basically, creator.detailsWidgetInfo was null when trying this, which in retrospect makes sense because it should have been set to a null value (SettingAttribute) in BuilderInspectorDataField (constructor), but it was only happening in the runtime builder version of events.<br />I thought the problem was that, in the editor, we end up serializing the creator&#8217;s detailsWidgetInfo (which is null), so maybe it doesn&#8217;t write it out or defaults it to some non-null default value, so that when we read it in in the scene, detailsWidgetInfo is not null, but some default version of DetailsWidgetInfo. This would not happen if we were building the creators at runtime and immediately using them.<br />Changing BuilderInspectorDataField (constructor) to return a default DetailsWidgetInfo, however, created an infinite loop that would use up all the editor resources, and I don&#8217;t know why. I then added a bunch of default values to the BuilderInspectorElementCreator fields (like Lists/SettingAttributes and stuff), and this seemed to work.<br />However, I&#8217;m getting a really strange argument out of bounds error when trying to DisableBuilder AFTER the prefabification process. It was already disabled&#8230; at the BEGINNING of the prefabify process. Idk what the fucking shit it&#8217;s doing to fuck it up that bad.<br />Undoing all the shit I changed, and I still get this error, but just resuming gameplay afterwards seems to still work.<br />I recall turning off Error Pause at some point yesterday. Maybe it was showing up and I didn&#8217;t notice.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-f391515 elementor-widget elementor-widget-text-editor" data-id="f391515" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">My first attempts at the RuntimeBuilder were&#8230; not good. There were a lot of blemishes that I hadn&#8217;t had to deal with in InGameBuilder before because they were kinda covered up by having them statically built in the Scene. Trying to create builders on-demand turned out to be significantly more annoying.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-4107c65 elementor-widget elementor-widget-text-editor" data-id="4107c65" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>&#8220;</p><p>Before getting back to the prior problem, I also ran into the thing where clicking a slider widget button in a dynamic buffer list pulls up about 100 sliders&#8230; I don&#8217;t remember if I ever fixed/tested it, but it obviously doesn&#8217;t work.<br />The problem, I think, is that the BuilderInspectorDynamicBufferField instantiates a BuilderInspectorDynamicBufferDisplay and passes a copy of the BuilderInspectorElementCreator prefabs to it. The Display then creates 100 BuilderInspectors, each of which are passed these prefabs as their elements field. When each BuilderInspector calls BuildInspector, it adds another SingleWidgetInfo to the creator&#8217;s detailsWidgetInfo, which refers back to the same ones.<br />I&#8217;m trying to fix this by providing some explicit Creator-copying logic to hopefully separate them into new Creators.<br />After some tweaking, this did fix the 100-slider problem, but the slider widgets for fields in dynamic buffer elements still were not working.<br />They all had the wrong indices (-1), well actually they had 2 indices (-1, correct_index).<br />This is because we call BuilderInspectorDynamicBufferDisplay.Initialize, we call SetIndex on the individual inspectors AFTER we call BuildInspector, and we need the creator indices to be correct before BuildInspector to accurately set the widget indices during BuildInspector &gt; CreateWidgets.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-984d9c8 elementor-widget elementor-widget-text-editor" data-id="984d9c8" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">This is me being all over the place. I&#8217;ve had this problem where clicking a widget button in a dynamic buffer display created from the BuilderInspector process had 100 widgets show up at once. The gist of this problem was that I was passing a reference to a List when I should have made a new List.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-b924fb5 elementor-widget elementor-widget-text-editor" data-id="b924fb5" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>&#8220;</p><p>Looking back at argument out of range exception, the problem seems to be that there are bindings from the BuilderInspectorDynamicBufferDisplay leftover after the prefabification process, and this is causing a problem when calling remake widgets.<br />I&#8217;m not super sure, but a patchwork solution was to edit EnemyBuilder.ClearPathNodes to call UpdateBufferDisplays after pathNodeBuffer.Clear so that the BuilderInspectorDynamicBufferDisplay responsible for path movement can update to reflect the empty buffer.</p><p>While I was at it, I modified Creator&#8217;s AddTranslationWidget and AddRotationWidget calls to use creator.indices, which also required me to amemd the relevant widget InGameBuilder methods.<br />Also changed the tags that the widgets in spawn with to include indices, that way the same widgets are not overwritten when referencing a different index but the same field.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-14d5df7 elementor-widget elementor-widget-text-editor" data-id="14d5df7" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">Some Widget fixes.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-a148489 elementor-widget elementor-widget-text-editor" data-id="a148489" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>March 9, 2025:</h4><p>&#8220;</p><p>Now I went back to the BuilderScene to try and get it working at runtime (for real, though).<br />Running into some problems when setting up the EnemyBuilder subject.<br />When calling my RuntimeBuilderHelpers to apply various components, I am not getting past the TryGet for the RuntimeBuilderTag singleton.<br />I think it&#8217;s because my MonoBehaviour (InGameBuilderManager_Runtime), runs this stuff on Awake, so the subscenes haven&#8217;t really initialized yet? Maybe like that&#8230;<br />Did some stuff ot InGameBuilderManager_Runtime to just add the runtime builders to &#8216;builders&#8217; on awake.<br />I have to wait until the wait for builders coroutine to do the other setup because that happens after the RuntimeBuilderTag entity has been setup.<br />Now, before the normal WaitForBuilderSetupCoroutine coroutine, I iterate all the created runtimebuilders and setup the builders UI and subject.<br />This caused a problem because the InGameBuilderManager was incorrectly saying that all the builders were done waiting for subject and mapper before all of them actually were (the runtime builders take an extra frame to find the subject because they make it directly after the call to OpenBuilderFromProject because WaitForBuilderSetupCoroutine is actually invoked after OpenBuilderFromProject, OpenBuilderFromProject being the method that starts WaitForSubject and WaitForMapper.<br />In other words: For normal builders, WaitForSubject and WaitForMapper basically finish on the same frame they were invoked because all that stuff already exists, but for the runtime builders, the mapper exists, but the subject is created at the beginning of WaitForBuilderSetupCoroutine in the InGameBuilderManager_Runtime, which is started after all the calls to OpenBuilderFromProject.<br />The ACTUAL problem was in my waiting logic in WaitForBuilderSetupCoroutine, and I was ending the wait instantly every time. This didn&#8217;t cause problems before because everything was always available that first frame.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-c6d52ad elementor-widget elementor-widget-text-editor" data-id="c6d52ad" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">Here were my first real bugfixes for RuntimeBuilder. There was an issue in my WaitForX&#8230; logic that I had never caught before because it wasn&#8217;t causing issues. It caused issues for the RuntimeBuilder, though.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-70dee66 elementor-widget elementor-widget-text-editor" data-id="70dee66" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>&#8220;</p><p>Right now, it seems to be working. Except for the &#8216;Add Button&#8217; button in the buffer inspectors.<br />I need to figure out if it&#8217;s just the button that&#8217;s not hooking up, or a binding problem.<br />Also the registry buttons don&#8217;t work anymore. Probably need to connect them back up to the new InGameBuilderManager_Runtime.<br />Update: Nope. The problem is again in OpenBuilderFromProject. Here, we call TryFirstTimeSetup, which initializes &#8216;inspectorBufferDisplays&#8217; with all the current/existing children. Upon UpdateBufferDisplays, our inspectorBufferDisplays is empty, so it does not update.<br />Added a coroutine setup for SetupBuilders and an override for it which does this setup first and waits for it to be done before calling OpenBuilderFromProject.<br />This gets the buffer display to update, but now there is a problem with the Enemy not moving when changing the nodes.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-6c08dff elementor-widget elementor-widget-text-editor" data-id="6c08dff" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">Things seemed to work until I checked the buffer displays created by the RuntimeBuilders. These were mostly unresponsive, and the problem was that certain initialization logic was happening before everything to be initialized was in place. I basically had to wait for RuntimeBuilders to do some setup before the typical InGameBuilder setup.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-b9ea9b5 elementor-widget elementor-widget-text-editor" data-id="b9ea9b5" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>&#8220;</p><p>It seems like the enemy does not move because the PathMovement component on it is not &#8216;Enabled&#8217;.<br />I&#8217;m going to fix this by adding more prototypes to RuntimeBuilderHelper to aid in getting these things to be more like they are post-baking.<br />Added more prototypes. These all relate to a corresponding Authoring:<br />EnemyPrototype<br />HealthPrototype<br />PathMoverPrototype<br />IncrementalMoverPrototype<br />Made the incremental movement components serializable.<br />Added a IncrementalMoverAuthoring to place speed, accelerate, and jerk components to an entity.</p><p>Next I ran into an error copying component &#8216;DoTrigger&#8217;.<br />I&#8217;m thinking this is because it&#8217;s a buffer element, so probably should be copied differently.<br />This was the case. I decided to pick my battles here. Just knowing that the buffer is on the entity is good enough, so I just add the buffer and don&#8217;t copy any elements over.</p><p>Next started turning LevelBuilder into a RuntimeBuilder.<br />Problem where there are 2 LevelComponents, but this is because the prefabs in the RuntimeBuilderSubject are not disabled.<br />Disabled the entities in the RuntimeBuilderSubject.<br />InGameBuilderManager_Runtime now also checks if the RuntimeBuilder type is already present in the scene and does not add it if so.<br />RuntimeBuilderSubjectAuthoring now disables all prototype entities.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-308638c elementor-widget elementor-widget-text-editor" data-id="308638c" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">Finally, I fixed some stuff with my RuntimeBuilderHelper methods. These methods only really exist to be a terrible version of Unity&#8217;s baking process, where I call these helper methods to add presets of components to created Entities. For normal InGameBuilders, I manually create a SubScene, create a &#8220;Subject&#8221; entity, and place MonoBehaviours on the subject to act as the target for InGameBuilder modifications. However, these subjects don&#8217;t exist beforehand for RuntimeBuilder. Since my previous subjects were heavily reliant on the Baking process (and I can&#8217;t Bake at runtime), I had to make RuntimeBuilderHelper, which I populate with a bunch of pre-Baked entities with specific components. I call RuntimeBuilderHelper to copy components from the pre-baked entities to my new subjects. Each pre-baked entity typically represents some base functionality, like having an entity with incremental movement components, another entity with physics/hitbox components, another entity with sprite rendering components, etc.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-44bec80 elementor-widget elementor-widget-text-editor" data-id="44bec80" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>March 10, 2025:</h4><p>&#8220;</p><p>Starting on ModBuilder.<br />ModLoader- Big modifications which fit IModLoaderModules into the mod loading pipeline. After loading the sorted mod assemblies, it then checks each mod&#8217;s assemblies for any types that implement IModLoaderModule. Then, it creates an instance of each of those types (in same order as sorted mods) and caches them. Then, for each (ordered) IModLoaderModule, it does the following:<br />  -Calls module.LoadInternal(&#8230;) to allow the mod loader module to act only on the assemblies on the mod where the loader was defined.<br />  -Calls module.Load(&#8230;) on ALL the mods in the order that they were sorted. This gives it a chance to operate on all modded assemblies.<br />  -Note: We add the game&#8217;s initial assemblies to the front of the lists so that the game&#8217;s assemblies are included for any IModLoaderModule loading stuff.<br />ModBuilder- A new type of InGameBuilder that extends RuntimeBuilder. ModBuilder is responsible for setting up it&#8217;s RuntimeBuilder methods by querying statically-loaded ModBuilderLoader for the corresponding ModBuilderManifest for that type and transferring that information to the RuntimeBuilder methods.<br />IModLoaderModule- Defines LoadInternal(IModLoaderData data), which acts on only the mod that defines the IModLoaderModule. Defines Load(IModLoaderData data), which acts on all loaded mod assemblies (not on base assmblies).<br />IModLoaderData- Defines an argument to be passed into IModLoaderModule methods. Includes information like ModManifest, project manifest, mod folder and subfolder, and assemblies included by the mod.<br />ModBuilderLoader- A new IModLoaderModule that is responsible for setting up ModBuilders during mod loading. It is responsible for taking and syncing data defined in the loader config and mapping that information to each type in each mod that implements ModBuilder.<br />ModBuilderManifest-A poorly named data class that just holds stuff similar to RuntimeBuilderDataBehaviour<br />ModBuilderAttribute-A backup method for defining the availability/configuration of a modded-in builder.</p><p>After brief test, it seems to work, except for the builder display name not carrying through&#8230;<br />Should probably experiment by making a ForceField mod and corresponding ForceFieldBuilder:ModBuilder.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-9132a34 elementor-widget elementor-widget-text-editor" data-id="9132a34" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">This one&#8217;s pretty self-contained. I subclassed RuntimeBuilder into ModBuilder, and created a new process for loading mods. IModLoaderModule is basically a custom <em>process</em> for loading mods. In your mods, you can implement IModLoaderModule, and it will run on all the assemblies in the game (including other mods). I use this to create a special ModBuilderLoader which helps me create custom InGameBuilders at runtime, even those that are not built-in to the game.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-380d741 elementor-widget elementor-widget-text-editor" data-id="380d741" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>March 11, 2025:</h4><p>&#8220;</p><p>Fixed problem with display name not carrying through. Just added to RuntimeBuilderDataBehaviour SyncMetadata.<br />For some reason, the BulletLimitControl was acting up. Finally adjusted it so the EntityQuery is built on-demand if it hasn&#8217;t been setup yet. Avoids the nullref I get sometimes.<br />Also, the WidgetSnapControl was acting up, and always defaulted to show origin. Added a delayed coroutine to Start to just reset it after 1 frame. IDK why it was needed or why it worked, but that seems to be a common solution to things.<br />Fixed a bug where the inspector buffer lists were not being placed on the correct inspector page. Fixed this by editing BuilderInspectorHelpers.GetBufferFromData to add the display name, current page, and field name to the new buffer Creator.</p><p>Finished up some of the other builder inspector configs and got them working in the RiskyTestScene, finally.</p><p>Starting process of modding in ForceField.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-10c2bec elementor-widget elementor-widget-text-editor" data-id="10c2bec" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">Easygoing day. Fixed up a couple of miscellaneous things before diving into making an example InGameBuilder through a mod: ForceFieldBuilder.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-9e4a9b5 elementor-widget elementor-widget-text-editor" data-id="9e4a9b5" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>March 14, 2025:</h4><p>&#8220;</p><p>More work on modded ForceField today.<br />The main problem continues to be that the ITriggerEventJob does not play nicely in the host.</p><p>Let&#8217;s look at this output:<br />&#8220;&#8221;&#8221;<br />0x00007FF87F65DB50 (UnityPlayer) (function-name not available)<br />ERROR: SymGetSymFromAddr64, GetLastError: &#8216;Attempt to access invalid address.&#8217; (Address: 00007FF87ED32C15)<br />0x00007FF87ED32C15 (UnityPlayer) (function-name not available)<br />0x0000019CEF07BEFC (Mono JIT Code) (wrapper managed-to-native) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule_Injected (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&amp;,Unity.Jobs.JobHandle&amp;)<br />0x0000019CEF07BD8B (Mono JIT Code) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&amp;)<br />&#8220;&#8221;&#8221;<br />I notice that we go from Mono JIT Code to UnityPlayer. We also know that Schedule_Injected references some native/pre-built code that I *really* don&#8217;t have access to. I&#8217;m thinking that maybe the modded version goes through its calls and eventually makes a reference to this native code in its OWN UnityPlayer.dll file (the one generated from building the SDK), but that does not match up with the reference/address of the Schedule_Injected used in the host project&#8217;s .dll file. Maybe, we need a hook in the host project, that the mod can call which eventually resolves in the TRUE Schedule_Injected native call being made.<br />1. In host/Core package, create a new method/hook/utility/static helper whose ONLY JOB is to do exactly the same thing that we normally do to get the call stack down to Schedule_Injected. Call stack starting from Schedule here:<br />&#8220;&#8221;&#8221;<br />0x00007FF87F65DB50 (UnityPlayer) (function-name not available)<br />ERROR: SymGetSymFromAddr64, GetLastError: &#8216;Attempt to access invalid address.&#8217; (Address: 00007FF87ED32C15)<br />0x00007FF87ED32C15 (UnityPlayer) (function-name not available)<br />0x0000019CEF07BEFC (Mono JIT Code) (wrapper managed-to-native) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule_Injected (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&amp;,Unity.Jobs.JobHandle&amp;)<br />0x0000019CEF07BD8B (Mono JIT Code) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&amp;)<br />0x0000019D38DA1E1B (Mono JIT Code) [.\Library\PackageCache\com.unity.physics@1.2.3\Unity.Physics\Dynamics\Simulation\ITriggerEventsJob.cs:108] Unity.Physics.ITriggerEventJobExtensions:ScheduleUnityPhysicsTriggerEventsJob&lt;BH.Modding.ForceField.ForceFieldJob&gt; (BH.Modding.ForceField.ForceFieldJob,Unity.Physics.Simulation,Unity.Jobs.JobHandle) <br />0x0000019D38DA06BB (Mono JIT Code) [.\Library\PackageCache\com.unity.physics@1.2.3\Unity.Physics\Dynamics\Simulation\ITriggerEventsJob.cs:56] Unity.Physics.ITriggerEventJobExtensions:Schedule&lt;BH.Modding.ForceField.ForceFieldJob&gt; (BH.Modding.ForceField.ForceFieldJob,Unity.Physics.SimulationSingleton,Unity.Jobs.JobHandle) <br />&#8220;&#8221;&#8221;<br />This means we have the following chain:<br />1. ScheduleHelper.Schedule&lt;T&gt;(<br /><br />Trying first in Core package.<br />So that didn&#8217;t really work.<br />The problems still is that it redirects to some native code that I don&#8217;t understand.<br />I took a different approach, which was to ask the question: why is ITriggerEventJob so weird compared to other jobs?<br />Basically the only constraint with it is passing in this SimulationSingleton. What&#8217;s so hard about the rest?<br />I dug into SimulationSingleton &gt; Simulation &gt; TriggerEvents &gt; TriggerEvents.Enumerator and found that I could basically pass a TriggerEvents.Enumerator into a normal IJob and just perform the iteration directly there.<br />This&#8230; might be working?<br />I&#8217;m not sure yet. Need to modify the host a bit more to see.</p><p>Next up, probably go for adding in PlayerBuilder to the Builder scene so I can test it with bullets. Also add back debug logging that just prints all the stuff with force fields.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-49d9774 elementor-widget elementor-widget-text-editor" data-id="49d9774" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">The most significant thing about today was learning how to handle the Unity Physics stuff in modded content. ITriggerEventJob seemed not to be very usable in mods (which ForceField relies upon). I think that eventually, scheduling an ITriggerEventJob eventually calls some special secret UnityPlayer.dll code. Unfortunately, this reference would be to the UnityPlayer.dll of the <em>mod</em> project, not the main game, causing it to break. After a lot of bad attempts, I finally bypassed the whole thing by changing the ITriggerEventJob responsible for ForceField functionality into a normal IJob where I just pass in the physics simulation trigger events and manually iterate them. It seems like this will have to be the approach for other physics-based mods for the foreseeable future.</span></p>								</div>
				</div>
				<div class="elementor-element elementor-element-98cc4e7 elementor-widget elementor-widget-text-editor" data-id="98cc4e7" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>March 15, 2025:</h4><p>&#8220;</p><p>Maybe it was NaNing itself? I was placing an incremental movement thing on the field, so maybe it was interacting with itself? But how would it trigger itself? I don&#8217;t think that&#8217;s it&#8230; Checking the location anyways.<br />Also trying to remove incremental movement components from the field in TrySetupSubject.<br />So, removing the incremental mover from the force field did (kinda) fix the problem.<br />New problem, though&#8230;<br />During testing, the actual effect on the bullets seems to be really erratic. Some bullets are affected for a moment and other bullets are not, and the ones that are affected seem the change around.<br />During testing in the SDK, I do not have these kinds of problems, so I&#8217;m thinking maybe it has something to do with the trigger events being &#8220;consumed&#8221; by the BulletCollision check or something?<br />NO DONT DO THIS-&gt; I can probably test this by making an identical force field component/system that does the same thing and add another force field to the scene with this alternative field. If events are consumed, then only 1 should activate &#8230; no.<br />I could make another system that does something with these collisions.</p><p>After some testing, i&#8217;m not sure it&#8217;s my logic yet. I think it might be a test object in the scene that is causing the weird stuff.<br />I had a test field that I don&#8217;t think does anything anymore, but when bullets pass through it, it messes with the bullets currently in the functional force field, like it makes the field not affect them.<br />I&#8217;m going to remove the test object from the scene and see what happens.<br />Okay, I removed the thing and it works.<br />And yes, I found out what it is and I&#8217;m stupid.<br />I&#8217;m still mostly using code copied from my old ITriggerEventJob implementation which takes in a single TriggerEvent into Execute.<br />With this old method, I call &#8216;return&#8217; when I know we shouldn&#8217;t process it and I kept that in the new code.<br />However, in the new code, I have a single job that iterates all the TriggerEvents, so the first one that should not be processed calls return and skips the rest&#8230;<br />I just need to call &#8216;continue&#8217; instead.<br />Works now. Even fixed the inconsistencies that were happening because of player collision triggers messing things up before.</p><p>&#8220;</p>								</div>
				</div>
				<div class="elementor-element elementor-element-d05774c elementor-widget elementor-widget-text-editor" data-id="d05774c" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: right;"><span style="color: #ff0000;">I kind of jump right in here, but the problem I was having was that while testing the ForceField mod, my bullets seemed to only be allowed to interact with one thing at a time. It took a while to notice this because there was another ForceField in the scene that I had forgotten about causing seemingly erratic bullet behavior. I finally figured out that the logic I used while iterating TriggerEvents in the ForceField processing job was calling &#8220;return&#8221; instead of &#8220;continue&#8221;. This was bad because &#8220;return&#8221; <em>was</em> the correct logic when it was implemented with an ITriggerEventJob that represents a <em>single</em> trigger, but now that it&#8217;s just an IJob that iterates all <em>triggers</em>, it quits too early and &#8220;consumes&#8221; the trigger the first time it touches the force field.</span></p>								</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/mod-runtime-builders-bb-update-15/">Mod/Runtime Builders &#8211; BB &#8211; Update 15</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Magic Bytes in Python</title>
		<link>https://orcsune.com/blog/magic-bytes-in-python/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Wed, 23 Apr 2025 15:46:44 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4427</guid>

					<description><![CDATA[<p>Magic Bytes in Python Posted: April 23, 2025 Last Updated: April 23, 2025 Magic Bytes in Python I have a question. What if you want to figure out the file type of some bytes in Python? My guess is you would google &#8220;python get file type from bytes&#8221; or something similar, you would check this [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/magic-bytes-in-python/">Magic Bytes in Python</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4427" class="elementor elementor-4427">
				<div class="elementor-element elementor-element-3b1e77c5 e-flex e-con-boxed e-con e-parent" data-id="3b1e77c5" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-1ab33fa9 elementor-widget elementor-widget-text-editor" data-id="1ab33fa9" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Magic Bytes in Python<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p><strong>Posted:</strong> April 23, 2025</p><p><strong>Last Updated:</strong> April 23, 2025</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-3413c9b9 e-flex e-con-boxed e-con e-parent" data-id="3413c9b9" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-7b66cf3b elementor-widget elementor-widget-text-editor" data-id="7b66cf3b" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><span style="text-decoration: underline;">Magic Bytes in Python</span></p><p>I have a question. What if you want to figure out the file type of some bytes in Python? My guess is you would google &#8220;python get file type from bytes&#8221; or something similar, you would check <a href="https://stackoverflow.com/questions/10937350/how-to-check-type-of-files-without-extensions">this StackOverflow post</a>, and land on using the <a href="https://github.com/ahupp/python-magic">python-magic</a> package. That&#8217;s great, but what if you are also somehow lazy enough to not want to install libmagic on Windows, but not lazy enough to just make your own extensible file type-guesser in Python? Then you would be me.</p><p>In my opinion, this approach is about as short and no-nonsense as you can get with file type guessing in Python. Essentially, we can just check the query byte-by-byte against any file signature and organize the signatures in a way that we can quickly filter out any that would possibly be applicable. If you care about nothing else, the full code is below, and the details are described after.</p><h2>Magic Bytes Full Code:</h2>								</div>
				</div>
				<div class="elementor-element elementor-element-18f09a3 elementor-widget elementor-widget-code-block-for-elementor" data-id="18f09a3" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-tomorrow-night' data-show-toolbar='yes'><code class='language-python'># * * * * *
# Original Author: Orcsune
# Date: April 23, 2025
# License: MIT License
# * * * * *

# Use the following methods to check your file type queries:
# format_from_file
# format_from_bytes
# extension_from_bytes
# mimetype_from_bytes

from typing import Dict, Iterable, Set, List, Union
from dataclasses import dataclass, field
from collections import defaultdict
from pathlib import Path

@dataclass
class FileFormat:
    &quot;&quot;&quot;Class describing a file format with extension and MIME type.&quot;&quot;&quot;
    extension:str = None
    mimetype:str = None

    @property
    def mime(self):
        return self.mimetype
    @property
    def ext(self):
        return self.extension
    @property
    def has_mimetype(self):
        return self.mimetype is not None
    @property
    def has_extension(self):
        return self.extension is not None

@dataclass
class SignatureBytes:
    data: bytes
    offset: int
    any_byte_idxs: Set[int] = field(default_factory=set)

    @property
    def length(self):
        &quot;&quot;&quot;Total length of the signature including &#039;any&#039;-bytes.&quot;&quot;&quot;
        return len(self.data) + len(self.any_byte_idxs)
        
    def compare(self, other:bytes):
        &quot;&quot;&quot;
        Compare an incoming byte sequence to see if it
        matches this signature. Compares byte-by-byte
        skipping any &quot;any&quot;-match indices, such as those
        in WAV: &quot;52 49 46 46 ?? ?? ?? ?? 57 41 56 45&quot;.

        Args:
            other: Query bytes for guessing.
        Returns:
            match: True if query matches signature.
                False otherwise.
        &quot;&quot;&quot;
        cut = other[self.offset:self.offset+self.length]
        comp_offset = 0
        for i in range(len(self.data)):
            while comp_offset + i in self.any_byte_idxs:
                comp_offset += 1
            if (self.data[i] != cut[comp_offset + i]):
                return False
        return True

    def __str__(self) -&gt; str:
        &quot;&quot;&quot;Hex string representation of signature.&quot;&quot;&quot;
        any_byte = &quot;?? &quot;
        final = any_byte*self.offset
        comp_offset = 0
        # Loop all bytes, replacing &quot;any&quot; bytes
        # with &quot;?? &quot; substring
        for i in range(self.length):
            if i in self.any_byte_idxs:
                final += any_byte
                comp_offset += 1
            else:
                final += self.data[i - comp_offset].to_bytes(1, &quot;big&quot;).hex() + &quot; &quot;
        
        return final.strip()

class Signature:
    &quot;&quot;&quot;
    Representation of a file type signature, including the
    signature bytes and the file type extension and MIME type.
    &quot;&quot;&quot;
    def __init__(self, sig_bytes: SignatureBytes, extension=&quot;&lt;no_extension&gt;&quot;, mime=&quot;&lt;no_mime&gt;&quot;) -&gt; None:
        self.signature = sig_bytes
        self.format = FileFormat(extension, mime)

    @property
    def data(self):
        &quot;&quot;&quot;Signature bytes.&quot;&quot;&quot;
        return self.signature.data
    @property
    def length(self):
        &quot;&quot;&quot;Signature length (including &#039;any&#039;-bytes).&quot;&quot;&quot;
        return self.signature.length
    @property
    def offset(self):
        &quot;&quot;&quot;Signature offset.&quot;&quot;&quot;
        return self.signature.offset
    @property
    def extension(self):
        &quot;&quot;&quot;Signature extension.&quot;&quot;&quot;
        return self.format.extension
    @property
    def mimetype(self):
        &quot;&quot;&quot;Signature MIME type&quot;&quot;&quot;
        return self.format.mimetype
    
    def compare(self, other:bytes):
        &quot;&quot;&quot;
        Compare an incoming byte sequence to see if it
        matches this signature. Compares byte-by-byte
        skipping any &quot;any&quot;-match indices, such as those
        in WAV: &quot;52 49 46 46 ?? ?? ?? ?? 57 41 56 45&quot;.

        Args:
            other: Query bytes for guessing.
        Returns:
            match: True if query matches signature.
                False otherwise.
        &quot;&quot;&quot;
        return self.signature.compare(other)
    
    def __str__(self) -&gt; str:
        &quot;&quot;&quot;Signature string, including extension and MIME type.&quot;&quot;&quot;
        return f&quot;Signature(ext={self.extension}, mime={self.mimetype}, signature={self.signature.__str__()})&quot;
    
class SignatureMap:
    &quot;&quot;&quot;
    Special signature map that does some easy preprocessing
    that separates each signature based on their first bytes.
    This helps increase speed by pruning a large subset of
    signatures that do not start with the query&#039;s first byte.
    Also separated by offset.
    &quot;&quot;&quot;
    def __init__(self, signatures: Iterable[Signature]) -&gt; None:
        # Separate the signatures based first on their offset values
        # Then separate them based on their first byte values.
        self.unique_offsets: List[int] = sorted(list(set((sig.offset for sig in signatures))))
        self.signatures: Dict[int, Dict[bytes, List[Signature]]] = {
            offset: defaultdict(list) for offset in self.unique_offsets
        }
        # Fill the signature map
        for sig in signatures:
            self.signatures[sig.offset][sig.data[0].to_bytes(1, &quot;big&quot;)].append(sig)

    def guess_format(self, other: bytes) -&gt; FileFormat:
        &quot;&quot;&quot;
        Guesses the file format of given bytes using the available
        file signatures.

        Args:
            other: Query bytes for guessing.
        Returns:
            format: FileFormat describing the file type.
        &quot;&quot;&quot;
        other_len = len(other)
        if other_len &lt; 1: return None
        
        for offset in self.unique_offsets:
            # Ignore checking signatures where the offset 
            # is out of bounds of the input comparison data
            if offset &gt;= other_len:
                continue
            first_byte = other[offset].to_bytes(1, &quot;big&quot;)
            if first_byte not in self.signatures[offset]:
                continue
            for signature in self.signatures[offset][first_byte]:
                if (signature.compare(other)):
                    return signature.format

        return None
    

# https://en.wikipedia.org/wiki/List_of_file_signatures
# gif (GIF87a):     (offset 0), 47 49 46 38 37 61
# gif (GIF89a):     (offset 0), 47 49 46 38 39 61
# png:              (offset 0), 89 50 4E 47 0D 0A 1A 0A
# wav:              (offset 0), 52 49 46 46 ?? ?? ?? ?? 57 41 56 45
# mp4 (ftypMSNV):   (offset 4), 66 74 79 70 4D 53 4E 56
# mp4 (ftypisom):   (offset 4), 66 74 79 70 69 73 6F 6D
SIG_GIF1    = Signature(SignatureBytes(b&#039;\x47\x49\x46\x38\x37\x61&#039;, 0), &quot;gif&quot;, &quot;image/gif&quot;)
SIG_GIF2    = Signature(SignatureBytes(b&#039;\x47\x49\x46\x38\x39\x61&#039;, 0), &quot;gif&quot;, &quot;image/gif&quot;)
SIG_PNG     = Signature(SignatureBytes(b&#039;\x89\x50\x4E\x47\x0D\x0A\x1A\x0A&#039;, 0), &quot;png&quot;, &quot;image/png&quot;)
SIG_WAV     = Signature(SignatureBytes(b&#039;\x52\x49\x46\x46\x57\x41\x56\x45&#039;, 0, set((4, 5, 6, 7))), &quot;wav&quot;, &quot;audio/wav&quot;)
SIG_MP41    = Signature(SignatureBytes(b&#039;\x66\x74\x79\x70\x4D\x53\x4E\x56&#039;, 4), &quot;mp4&quot;, &quot;video/mp4&quot;)
SIG_MP42    = Signature(SignatureBytes(b&#039;\x66\x74\x79\x70\x69\x73\x6F\x6D&#039;, 4), &quot;mp4&quot;, &quot;video/mp4&quot;)

signatures = [
    SIG_GIF1,
    SIG_GIF2,
    SIG_PNG,
    SIG_WAV,
    SIG_MP41,
    SIG_MP42
]
signature_map = SignatureMap(signatures)

def format_from_file(file_path: Union[str, Path]) -&gt; Union[FileFormat, None]:
    &quot;&quot;&quot;
    Guesses the file format of given file using the available
    file signatures from the SignatureMap.

    Args:
        file_path: Query file path for guessing.
    Returns:
        format: FileFormat describing the file type.
    &quot;&quot;&quot;
    chunk_size = 256
    try:
        with open(file_path, &quot;rb&quot;) as f:
            data = f.read(chunk_size)
            return format_from_bytes(data)
    except:
        return None
def format_from_bytes(file_bytes: bytes) -&gt; Union[FileFormat, None]:
    &quot;&quot;&quot;
    Guesses the file format of given bytes using the available
    file signatures from the SignatureMap.

    Args:
        file_bytes: Query bytes for guessing.
    Returns:
        format: FileFormat describing the file type.
    &quot;&quot;&quot;
    if not isinstance(file_bytes, bytes):
        return None
    return signature_map.guess_format(file_bytes)
def extension_from_bytes(file_bytes: bytes) -&gt; Union[str, None]:
    &quot;&quot;&quot;
    Guesses the file extension of given bytes using the available
    file signatures from the SignatureMap.

    Args:
        file_bytes: Query bytes for guessing.
    Returns:
        extension: File extension describing the file type.
    &quot;&quot;&quot;
    fformat = format_from_bytes(file_bytes)
    if fformat is None:
        return None
    return fformat.ext
def mimetype_from_bytes(file_bytes: bytes) -&gt; Union[str, None]:
    &quot;&quot;&quot;
    Guesses the MIME type of given bytes using the available
    file signatures from the SignatureMap.

    Args:
        file_bytes: Query bytes for guessing.
    Returns:
        extension: MIME type describing the file type.
    &quot;&quot;&quot;
    fformat = format_from_bytes(file_bytes)
    if fformat is None:
        return None
    return fformat.mime</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-35b9b38b elementor-widget elementor-widget-text-editor" data-id="35b9b38b" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">FileFormat:</h2><p><code>FileFormat</code> just describes the format of the file to be identified. My only use case for this is to retrieve the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types">MIME type</a> and the file extension, but this can easily be changed or augmented. I make <code>FileFormat</code> a basic dataclass and provide some simple properties for quick access to these desired values. If certain file types don&#8217;t have one or the other of extension or MIME type, then you can check this with the <code>has_extension</code> or <code>has_mimetype</code>, respectively.</p>								</div>
				</div>
				<div class="elementor-element elementor-element-920c91d elementor-widget elementor-widget-code-block-for-elementor" data-id="920c91d" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-tomorrow-night' data-show-toolbar='yes'><code class='language-python'>@dataclass
class FileFormat:
    &quot;&quot;&quot;Class describing a file format with extension and MIME type.&quot;&quot;&quot;
    extension:str = None
    mimetype:str = None

    @property
    def mime(self):
        return self.mimetype
    @property
    def ext(self):
        return self.extension
    @property
    def has_mimetype(self):
        return self.mimetype is not None
    @property
    def has_extension(self):
        return self.extension is not None</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-1b12849 elementor-widget elementor-widget-text-editor" data-id="1b12849" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">SignatureBytes:</h2><p>The <code>SignatureBytes</code> class is the main logic for where the actual &#8220;magic byte&#8221; checking occurs. As you can see, it&#8217;s actually pretty short. The major design decision was how to cover the majority of file types without adding a ton of complexity. Looking through the <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">list of file signatures</a> on Wikipedia, I saw that the two major obstacles were: 1) <strong>Signature offset</strong> and 2) <strong>Split Signatures</strong>.</p><ol><li><strong>Signature Offset:</strong> The offset of a signature is just a number which represents the byte where the signature would begin in a file of that type. For example, one of mp4&#8217;s signatures (<code>66 74 79 70 69 73 6F 6D</code>) has an offset of 4, so in reality, it&#8217;s signature looks like: (<code>?? ?? ?? ?? 66 74 79 70 69 73 6F 6D</code>), where the <code>??</code> bytes represent &#8220;any&#8221; bytes &#8211; bytes that can be anything. To represent this, I just store a single integer representing the offset. As we will see later, I can encode multiple different offsets for the same file type by just having multiple <code>Signatures</code> in the map with different offsets. Of course, this is only viable for signatures with a finite/small number of possible offset values.</li><li><strong>Split Signature:</strong> The next problem was with split signatures, like wav&#8217;s: (<div><div><code>52 49 46 46 ?? ?? ?? ?? 57 41 56 45</code>). Here, there are some &#8220;any&#8221; bytes stuck in the middle. To keep this extensible, I did not want to put any restrictions on the index or contiguity of these &#8220;any&#8221; bytes, so I choose to store each specific &#8220;any&#8221; byte index in a set.</div></div></li></ol><p>During comparison to some query bytes, it just iterates all the signatures bytes and compares each to the corresponding index in the query bytes. If it would be checking an index in the <code>any_byte_idxs</code> set, it increments a secondary index pointer until it checks the next byte not a part of the &#8220;any&#8221; bytes. If any byte does not match then it returns <code>False</code>, otherwise <code>True</code>.</p>								</div>
				</div>
				<div class="elementor-element elementor-element-18acb89 elementor-widget elementor-widget-code-block-for-elementor" data-id="18acb89" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-tomorrow-night' data-show-toolbar='yes'><code class='language-python'>@dataclass
class SignatureBytes:
    data: bytes
    offset: int
    any_byte_idxs: Set[int] = field(default_factory=set)

    @property
    def length(self):
        &quot;&quot;&quot;Total length of the signature including &#039;any&#039;-bytes.&quot;&quot;&quot;
        return len(self.data) + len(self.any_byte_idxs)
        
    def compare(self, other:bytes):
        &quot;&quot;&quot;
        Compare an incoming byte sequence to see if it
        matches this signature. Compares byte-by-byte
        skipping any &quot;any&quot;-match indices, such as those
        in WAV: &quot;52 49 46 46 ?? ?? ?? ?? 57 41 56 45&quot;.

        Args:
            other: Query bytes for guessing.
        Returns:
            match: True if query matches signature.
                False otherwise.
        &quot;&quot;&quot;
        cut = other[self.offset:self.offset+self.length]
        comp_offset = 0
        for i in range(len(self.data)):
            while comp_offset + i in self.any_byte_idxs:
                comp_offset += 1
            if (self.data[i] != cut[comp_offset + i]):
                return False
        return True

    def __str__(self) -&gt; str:
        &quot;&quot;&quot;Hex string representation of signature.&quot;&quot;&quot;
        any_byte = &quot;?? &quot;
        final = any_byte*self.offset
        comp_offset = 0
        # Loop all bytes, replacing &quot;any&quot; bytes
        # with &quot;?? &quot; substring
        for i in range(self.length):
            if i in self.any_byte_idxs:
                final += any_byte
                comp_offset += 1
            else:
                final += self.data[i - comp_offset].to_bytes(1, &quot;big&quot;).hex() + &quot; &quot;
        
        return final.strip()</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-1e1fdd0 elementor-widget elementor-widget-text-editor" data-id="1e1fdd0" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Signature:</h2><p>The <code>Signature</code> is just a wrapper that ties <code>FileFormat</code> and <code>SignatureBytes</code> instances together. It provides an easy interface to check for matching and provide resulting format information in one place. The implementation is basically just wrapper functions for the important encapsulated instances.</p>								</div>
				</div>
				<div class="elementor-element elementor-element-60dbbc4 elementor-widget elementor-widget-code-block-for-elementor" data-id="60dbbc4" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-tomorrow-night' data-show-toolbar='yes'><code class='language-python'>class Signature:
    &quot;&quot;&quot;
    Representation of a file type signature, including the
    signature bytes and the file type extension and MIME type.
    &quot;&quot;&quot;
    def __init__(self, sig_bytes: SignatureBytes, extension=&quot;&lt;no_extension&gt;&quot;, mime=&quot;&lt;no_mime&gt;&quot;) -&gt; None:
        self.signature = sig_bytes
        self.format = FileFormat(extension, mime)

    @property
    def data(self):
        &quot;&quot;&quot;Signature bytes.&quot;&quot;&quot;
        return self.signature.data
    @property
    def length(self):
        &quot;&quot;&quot;Signature length (including &#039;any&#039;-bytes).&quot;&quot;&quot;
        return self.signature.length
    @property
    def offset(self):
        &quot;&quot;&quot;Signature offset.&quot;&quot;&quot;
        return self.signature.offset
    @property
    def extension(self):
        &quot;&quot;&quot;Signature extension.&quot;&quot;&quot;
        return self.format.extension
    @property
    def mimetype(self):
        &quot;&quot;&quot;Signature MIME type&quot;&quot;&quot;
        return self.format.mimetype
    
    def compare(self, other:bytes):
        &quot;&quot;&quot;
        Compare an incoming byte sequence to see if it
        matches this signature. Compares byte-by-byte
        skipping any &quot;any&quot;-match indices, such as those
        in WAV: &quot;52 49 46 46 ?? ?? ?? ?? 57 41 56 45&quot;.

        Args:
            other: Query bytes for guessing.
        Returns:
            match: True if query matches signature.
                False otherwise.
        &quot;&quot;&quot;
        return self.signature.compare(other)
    
    def __str__(self) -&gt; str:
        &quot;&quot;&quot;Signature string, including extension and MIME type.&quot;&quot;&quot;
        return f&quot;Signature(ext={self.extension}, mime={self.mimetype}, signature={self.signature.__str__()})&quot;</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-f299b7b elementor-widget elementor-widget-text-editor" data-id="f299b7b" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">SignatureMap:</h2><p>I could have basically stopped here and just created and iterated a list of Signatures, but I did want to provide a little bit of indexing support to (hopefully) speed queries up slightly. The <code>SignatureMap</code> is my attempt at this indexing. It creates 2 nested mappings. The first mapping separates <code>Signatures</code> by their offset fields. Each offset mapping value is another mapping from the first byte of the signature to a list of signatures with matching first bytes.</p><p>Why do this? That&#8217;s because there seemed to be enough variety in the first bytes of signatures that they would not all land under the same key in the dictionary. We can filter out a bunch of potential <code>Signatures</code> just by checking the first byte of the query, basically. Why not more than 1 byte for more specificity so we have to iterate fewer Signatures? We don&#8217;t know if there are more than 1 bytes before the first &#8220;any&#8221; byte (take the <a href="https://en.wikipedia.org/wiki/List_of_file_signatures#cite_note-76">PGP format</a>, for example), so sticking with the first byte is a safe bet.</p><p>Other than the indexing, when we actually perform a query, we iterate the unique offset keys, and for each sub-dictionary, we find the list of <code>Signatures</code> matching the query&#8217;s first byte <em>at that offset</em> (if any). Once we find this list, we just iterate those signatures until one matches, returning its format (or <code>None</code> if no matches).</p>								</div>
				</div>
				<div class="elementor-element elementor-element-9ba1475 elementor-widget elementor-widget-code-block-for-elementor" data-id="9ba1475" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-tomorrow-night' data-show-toolbar='yes'><code class='language-python'>class SignatureMap:
    &quot;&quot;&quot;
    Special signature map that does some easy preprocessing
    that separates each signature based on their first bytes.
    This helps increase speed by pruning a large subset of
    signatures that do not start with the query&#039;s first byte.
    Also separated by offset.
    &quot;&quot;&quot;
    def __init__(self, signatures: Iterable[Signature]) -&gt; None:
        # Separate the signatures based first on their offset values
        # Then separate them based on their first byte values.
        self.unique_offsets: List[int] = sorted(list(set((sig.offset for sig in signatures))))
        self.signatures: Dict[int, Dict[bytes, List[Signature]]] = {
            offset: defaultdict(list) for offset in self.unique_offsets
        }
        # Fill the signature map
        for sig in signatures:
            self.signatures[sig.offset][sig.data[0].to_bytes(1, &quot;big&quot;)].append(sig)

    def guess_format(self, other: bytes) -&gt; FileFormat:
        &quot;&quot;&quot;
        Guesses the file format of given bytes using the available
        file signatures.

        Args:
            other: Query bytes for guessing.
        Returns:
            format: FileFormat describing the file type.
        &quot;&quot;&quot;
        other_len = len(other)
        if other_len &lt; 1: return None
        
        for offset in self.unique_offsets:
            # Ignore checking signatures where the offset 
            # is out of bounds of the input comparison data
            if offset &gt;= other_len:
                continue
            first_byte = other[offset].to_bytes(1, &quot;big&quot;)
            if first_byte not in self.signatures[offset]:
                continue
            for signature in self.signatures[offset][first_byte]:
                if (signature.compare(other)):
                    return signature.format

        return None</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-4f3edf8 elementor-widget elementor-widget-text-editor" data-id="4f3edf8" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Setting Up:</h2><p>Once all the implementations are out of the way, it becomes really simple to use. Just instantiate a bunch of <code>Signatures</code> that meet your specifications, pass them into the <code>SignatureMap</code>, and create a few public methods for your queries.</p><p>My current use case only really needs GIFs, PNGs, WAVs, and MP4s. Notice here there are 2 different <code>Signatures</code> for GIFs and MP4s.</p><p>I also add a chunk_size limit for checking directly using a file path just as a heuristic that most formats do not have large offsets, so they don&#8217;t need to check very far. You can remove this from <code>format_from_file</code> if you need.</p>								</div>
				</div>
				<div class="elementor-element elementor-element-290acd7 elementor-widget elementor-widget-code-block-for-elementor" data-id="290acd7" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-tomorrow-night' data-show-toolbar='yes'><code class='language-python'># https://en.wikipedia.org/wiki/List_of_file_signatures
# gif (GIF87a):     (offset 0), 47 49 46 38 37 61
# gif (GIF89a):     (offset 0), 47 49 46 38 39 61
# png:              (offset 0), 89 50 4E 47 0D 0A 1A 0A
# wav:              (offset 0), 52 49 46 46 ?? ?? ?? ?? 57 41 56 45
# mp4 (ftypMSNV):   (offset 4), 66 74 79 70 4D 53 4E 56
# mp4 (ftypisom):   (offset 4), 66 74 79 70 69 73 6F 6D
SIG_GIF1    = Signature(SignatureBytes(b&#039;\x47\x49\x46\x38\x37\x61&#039;, 0), &quot;gif&quot;, &quot;image/gif&quot;)
SIG_GIF2    = Signature(SignatureBytes(b&#039;\x47\x49\x46\x38\x39\x61&#039;, 0), &quot;gif&quot;, &quot;image/gif&quot;)
SIG_PNG     = Signature(SignatureBytes(b&#039;\x89\x50\x4E\x47\x0D\x0A\x1A\x0A&#039;, 0), &quot;png&quot;, &quot;image/png&quot;)
SIG_WAV     = Signature(SignatureBytes(b&#039;\x52\x49\x46\x46\x57\x41\x56\x45&#039;, 0, set((4, 5, 6, 7))), &quot;wav&quot;, &quot;audio/wav&quot;)
SIG_MP41    = Signature(SignatureBytes(b&#039;\x66\x74\x79\x70\x4D\x53\x4E\x56&#039;, 4), &quot;mp4&quot;, &quot;video/mp4&quot;)
SIG_MP42    = Signature(SignatureBytes(b&#039;\x66\x74\x79\x70\x69\x73\x6F\x6D&#039;, 4), &quot;mp4&quot;, &quot;video/mp4&quot;)

signatures = [
    SIG_GIF1,
    SIG_GIF2,
    SIG_PNG,
    SIG_WAV,
    SIG_MP41,
    SIG_MP42
]
signature_map = SignatureMap(signatures)

def format_from_file(file_path: Union[str, Path]) -&gt; Union[FileFormat, None]:
    &quot;&quot;&quot;
    Guesses the file format of given file using the available
    file signatures from the SignatureMap.

    Args:
        file_path: Query file path for guessing.
    Returns:
        format: FileFormat describing the file type.
    &quot;&quot;&quot;
    chunk_size = 256
    try:
        with open(file_path, &quot;rb&quot;) as f:
            data = f.read(chunk_size)
            return format_from_bytes(data)
    except:
        return None
def format_from_bytes(file_bytes: bytes) -&gt; Union[FileFormat, None]:
    &quot;&quot;&quot;
    Guesses the file format of given bytes using the available
    file signatures from the SignatureMap.

    Args:
        file_bytes: Query bytes for guessing.
    Returns:
        format: FileFormat describing the file type.
    &quot;&quot;&quot;
    if not isinstance(file_bytes, bytes):
        return None
    return signature_map.guess_format(file_bytes)
def extension_from_bytes(file_bytes: bytes) -&gt; Union[str, None]:
    &quot;&quot;&quot;
    Guesses the file extension of given bytes using the available
    file signatures from the SignatureMap.

    Args:
        file_bytes: Query bytes for guessing.
    Returns:
        extension: File extension describing the file type.
    &quot;&quot;&quot;
    fformat = format_from_bytes(file_bytes)
    if fformat is None:
        return None
    return fformat.ext
def mimetype_from_bytes(file_bytes: bytes) -&gt; Union[str, None]:
    &quot;&quot;&quot;
    Guesses the MIME type of given bytes using the available
    file signatures from the SignatureMap.

    Args:
        file_bytes: Query bytes for guessing.
    Returns:
        extension: MIME type describing the file type.
    &quot;&quot;&quot;
    fformat = format_from_bytes(file_bytes)
    if fformat is None:
        return None
    return fformat.mime</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-53ff345 elementor-widget elementor-widget-text-editor" data-id="53ff345" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Example:</h2><p>In my case, I mostly need to use the type guesser to distinguish between two different image file types that can be stored in <a href="https://www.tensorflow.org/tensorboard">TensorBoard</a>. In my reinforcement learning project, I can store either PNG images, or entire GIFs in the backend TensorBoard log. Both are considered images and retrieved as <code>ImageEvents</code><a href="https://github.com/tensorflow/tensorboard/blob/master/tensorboard/backend/event_processing/event_accumulator.py"> from the <code>EventAccumulator</code></a>. Unfortunately, I also need to know the exact MIME types of these records because I am attempting to send it over the network through an API. I just created a little helper function to allow me to do just that:</p>								</div>
				</div>
				<div class="elementor-element elementor-element-4aecdef elementor-widget elementor-widget-code-block-for-elementor" data-id="4aecdef" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-tomorrow-night' data-show-toolbar='yes'><code class='language-python'>def tb_event_to_media_format(event) -&gt; Union[FileFormat, None]:
    &quot;&quot;&quot;
    Return the FileFormat for the contents of a media file
    logged in a tensorboard file. Returns None if no
    such event or if conversion cannot find suitable
    indicators of a particular type.
    &quot;&quot;&quot;
    fformat = None
    if isinstance(event, ImageEvent):
        fformat = format_from_bytes(event.encoded_image_string)
    elif isinstance(event, AudioEvent):
        fformat = format_from_bytes(event.encoded_audio_string)
    return fformat</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-c576c13 elementor-widget elementor-widget-text-editor" data-id="c576c13" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Conclusion:</h2><p>Need a small, easy, and extensible way to check for file types in Python without libmagic? Try this.</p>								</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/magic-bytes-in-python/">Magic Bytes in Python</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Potpourri Python Index</title>
		<link>https://orcsune.com/blog/potpourri-python-index/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Wed, 23 Apr 2025 15:45:42 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4488</guid>

					<description><![CDATA[<p>Potpourri Python Index The blog for neat stuff I do in Python. Posted: April 23, 2025 Last Updated: April 23, 2025 UPDATES (Chronological): Magic Bytes (2025/04/23) BY TOPIC: Files/File Types: Magic Bytes</p>
<p>The post <a href="https://orcsune.com/blog/potpourri-python-index/">Potpourri Python Index</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4488" class="elementor elementor-4488">
				<div class="elementor-element elementor-element-5d6b953d e-flex e-con-boxed e-con e-parent" data-id="5d6b953d" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-799813a1 elementor-widget elementor-widget-text-editor" data-id="799813a1" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Potpourri Python Index<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p>The blog for neat stuff I do in Python.</p><p><strong>Posted:</strong> April 23, 2025</p><p><strong>Last Updated:</strong> April 23, 2025</p><h3>UPDATES (Chronological):</h3><ul><li><a href="https://orcsune.com/blog/magic-bytes-in-python/">Magic Bytes (2025/04/23)</a></li></ul><h3>BY TOPIC:</h3><h4>Files/File Types:</h4><ul><li><a href="https://orcsune.com/blog/magic-bytes-in-python/">Magic Bytes</a></li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-420ef720 elementor-widget-divider--view-line elementor-widget elementor-widget-divider" data-id="420ef720" data-element_type="widget" data-e-type="widget" data-widget_type="divider.default">
				<div class="elementor-widget-container">
							<div class="elementor-divider">
			<span class="elementor-divider-separator">
						</span>
		</div>
						</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/potpourri-python-index/">Potpourri Python Index</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Best Bullet Patterns</title>
		<link>https://orcsune.com/blog/best-bullet-patterns/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Mon, 17 Mar 2025 18:01:56 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4321</guid>

					<description><![CDATA[<p>Posted: March 17, 2025 Last Updated: June 1, 2025 Peruse some of my favorite and most excessive bullet hell bullet patterns that I&#8217;ve come up with in my level editor over time. I think this was the very first pattern I made that I was really impressed with. I still love it to this day. [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/best-bullet-patterns/">Best Bullet Patterns</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4321" class="elementor elementor-4321">
				<div class="elementor-element elementor-element-876a9e5 e-flex e-con-boxed e-con e-parent" data-id="876a9e5" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-1322226 elementor-widget elementor-widget-text-editor" data-id="1322226" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p style="text-align: center;"><strong>Posted:</strong> March 17, 2025</p><p style="text-align: center;"><strong>Last Updated:</strong> June 1, 2025</p>								</div>
				</div>
				<div class="elementor-element elementor-element-4fd4a88 elementor-widget elementor-widget-text-editor" data-id="4fd4a88" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Peruse some of my favorite and most excessive bullet hell bullet patterns that I&#8217;ve come up with in my level editor over time.</p>								</div>
				</div>
		<div class="elementor-element elementor-element-86ed4a3 e-con-full e-flex e-con e-child" data-id="86ed4a3" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-e06159f e-grid e-con-full e-con e-child" data-id="e06159f" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-4f7622f e-con-full e-flex e-con e-child" data-id="4f7622f" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-7acaeb4 elementor-widget elementor-widget-image" data-id="7acaeb4" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/contained_swirl_converted.webp" class="attachment-medium_large size-medium_large wp-image-4404" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/contained_swirl_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/contained_swirl_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/contained_swirl_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-51f1017 elementor-widget elementor-widget-text-editor" data-id="51f1017" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>I think this was the very first pattern I made that I was really impressed with. I still love it to this day. Something about it is just satisfying in a way I can&#8217;t describe.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-a1ea485 e-con-full e-flex e-con e-child" data-id="a1ea485" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-00f1ad3 elementor-widget elementor-widget-image" data-id="00f1ad3" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/chandelier_converted.webp" class="attachment-medium_large size-medium_large wp-image-4403" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/chandelier_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/chandelier_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/chandelier_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-3939b81 elementor-widget elementor-widget-text-editor" data-id="3939b81" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Another early work. I call it the &#8220;Chandelier&#8221;. It works using my bullet-change system where you can add timed changes to your bullets after firing.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-4fd7567 e-con-full e-flex e-con e-child" data-id="4fd7567" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-53e7798 elementor-widget elementor-widget-image" data-id="53e7798" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/boowomp_converted.webp" class="attachment-medium_large size-medium_large wp-image-4401" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/boowomp_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/boowomp_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/boowomp_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-bf43435 elementor-widget elementor-widget-text-editor" data-id="bf43435" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>I like this one. The red ones grow and blue ones shrink, giving a neat effect.</p>								</div>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-c16bee7 e-grid e-con-full e-con e-child" data-id="c16bee7" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-25f2905 e-con-full e-flex e-con e-child" data-id="25f2905" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-7b69f8c elementor-widget elementor-widget-image" data-id="7b69f8c" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/bruhuh_converted.webp" class="attachment-medium_large size-medium_large wp-image-4402" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/bruhuh_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/bruhuh_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/bruhuh_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-43b5514 elementor-widget elementor-widget-text-editor" data-id="43b5514" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Patterns don&#8217;t have to be complex to be pleasant to look at.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-b5379e4 e-con-full e-flex e-con e-child" data-id="b5379e4" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-d14068f elementor-widget elementor-widget-image" data-id="d14068f" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/gort_converted.webp" class="attachment-medium_large size-medium_large wp-image-4405" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/gort_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/gort_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/gort_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-fb86db0 elementor-widget elementor-widget-text-editor" data-id="fb86db0" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>One of my favorite dodgeable ones. It feels really nice to be in the flow on this one. There&#8217;s just an intuition that you develop for when to wait and when to rush.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-ce9efc5 e-con-full e-flex e-con e-child" data-id="ce9efc5" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-e7ff100 elementor-widget elementor-widget-image" data-id="e7ff100" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/rainbow_converted.webp" class="attachment-medium_large size-medium_large wp-image-4414" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/rainbow_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/rainbow_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/rainbow_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-dd9d758 elementor-widget elementor-widget-text-editor" data-id="dd9d758" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>A rainbow, thanks to a gravity mod I made for testing.</p>								</div>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-45ab0d9 e-grid e-con-full e-con e-child" data-id="45ab0d9" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-d125b95 e-con-full e-flex e-con e-child" data-id="d125b95" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-657f128 elementor-widget elementor-widget-image" data-id="657f128" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/pendulum_converted.webp" class="attachment-medium_large size-medium_large wp-image-4409" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/pendulum_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/pendulum_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/pendulum_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-227a009 elementor-widget elementor-widget-text-editor" data-id="227a009" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>It&#8217;s a little basic, but it does encapsulate the idea of combining different elements to create something playable but also a little aesthetically nice.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-748012f e-con-full e-flex e-con e-child" data-id="748012f" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-1fff5e1 elementor-widget elementor-widget-image" data-id="1fff5e1" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/stopgo_converted.webp" class="attachment-medium_large size-medium_large wp-image-4411" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/stopgo_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/stopgo_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/stopgo_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-2a63132 elementor-widget elementor-widget-text-editor" data-id="2a63132" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Another basic-ish one that&#8217;s more for visual appeal than gameplay. It&#8217;s a little easy to dodge, but that can be good sometimes.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-9d392c9 e-con-full e-flex e-con e-child" data-id="9d392c9" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-51298b4 elementor-widget elementor-widget-image" data-id="51298b4" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/riptide_converted.webp" class="attachment-medium_large size-medium_large wp-image-4410" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/riptide_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/riptide_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/riptide_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-68e2130 elementor-widget elementor-widget-text-editor" data-id="68e2130" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>This one is in my top 5 of ones that are definitely more interesting to look at. It&#8217;s actually pretty difficult to dodge because of the confined space.</p>								</div>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-5328987 e-grid e-con-full e-con e-child" data-id="5328987" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-7f8c95a e-con-full e-flex e-con e-child" data-id="7f8c95a" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-97688a1 elementor-widget elementor-widget-image" data-id="97688a1" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/moonflower_converted.webp" class="attachment-medium_large size-medium_large wp-image-4408" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/moonflower_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/moonflower_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/moonflower_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-26fba5c elementor-widget elementor-widget-text-editor" data-id="26fba5c" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Another top-5 pattern for me. Not only is it dodgeable, it&#8217;s also one of my prettiest ones to date.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-f7e14ff e-con-full e-flex e-con e-child" data-id="f7e14ff" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-f24e048 elementor-widget elementor-widget-image" data-id="f24e048" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/woods_converted.webp" class="attachment-medium_large size-medium_large wp-image-4413" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/woods_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/woods_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/woods_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-f79e649 elementor-widget elementor-widget-text-editor" data-id="f79e649" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>A walk in the woods. I was testing out bullet layers.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-5de1f92 e-con-full e-flex e-con e-child" data-id="5de1f92" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-64fb880 elementor-widget elementor-widget-image" data-id="64fb880" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/mandibles_converted.webp" class="attachment-medium_large size-medium_large wp-image-4407" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/mandibles_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/mandibles_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/mandibles_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-1be104a elementor-widget elementor-widget-text-editor" data-id="1be104a" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>I was mostly testing bloom post-processing with this one, but it turned into an interesting little containment thingy.</p>								</div>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-9adc0ad e-grid e-con-full e-con e-child" data-id="9adc0ad" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-0108ba0 e-con-full e-flex e-con e-child" data-id="0108ba0" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-fab382d elementor-widget elementor-widget-image" data-id="fab382d" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/loss_converted.webp" class="attachment-medium_large size-medium_large wp-image-4406" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/loss_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/loss_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/loss_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-fa6ff6e elementor-widget elementor-widget-text-editor" data-id="fa6ff6e" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>No comment.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-95f129a e-con-full e-flex e-con e-child" data-id="95f129a" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-026552b elementor-widget elementor-widget-image" data-id="026552b" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="360" height="360" src="https://orcsune.com/wp-content/uploads/2025/03/tripulator_converted.webp" class="attachment-medium_large size-medium_large wp-image-4412" alt="" srcset="https://orcsune.com/wp-content/uploads/2025/03/tripulator_converted.webp 360w, https://orcsune.com/wp-content/uploads/2025/03/tripulator_converted-300x300.webp 300w, https://orcsune.com/wp-content/uploads/2025/03/tripulator_converted-150x150.webp 150w" sizes="(max-width: 360px) 100vw, 360px" />															</div>
				</div>
				<div class="elementor-element elementor-element-983e03f elementor-widget elementor-widget-text-editor" data-id="983e03f" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>You can&#8217;t dodge this one. It just looks trippy.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-cf61174 e-con-full e-flex e-con e-child" data-id="cf61174" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-5399e53 elementor-widget elementor-widget-image" data-id="5399e53" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="720" height="720" src="https://orcsune.com/wp-content/uploads/2025/06/spiral_maze.webp" class="attachment-medium_large size-medium_large wp-image-4689" alt="" />															</div>
				</div>
				<div class="elementor-element elementor-element-a49d28e elementor-widget elementor-widget-text-editor" data-id="a49d28e" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Spiral maze thing. Pretty easy to dodge and a little funky to look at.</p>								</div>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-88a108f e-grid e-con-full e-con e-child" data-id="88a108f" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-df585d3 e-con-full e-flex e-con e-child" data-id="df585d3" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-5a5fcf2 elementor-widget elementor-widget-video" data-id="5a5fcf2" data-element_type="widget" data-e-type="widget" data-settings="{&quot;youtube_url&quot;:&quot;https:\/\/youtu.be\/dny_K-LCbQA&quot;,&quot;video_type&quot;:&quot;youtube&quot;,&quot;controls&quot;:&quot;yes&quot;}" data-widget_type="video.default">
				<div class="elementor-widget-container">
							<div class="elementor-wrapper elementor-open-inline">
			<div class="elementor-video"></div>		</div>
						</div>
				</div>
				<div class="elementor-element elementor-element-84ab207 elementor-widget elementor-widget-text-editor" data-id="84ab207" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>Here is an experiment I did pretty early on to test the limits of my project. I created a method to turn video frames into a firing point. It&#8217;s actually cool, but I&#8217;ll forego typing a big paragraph here because most of that information is in the video description.</p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-cd657ef e-con-full e-flex e-con e-child" data-id="cd657ef" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-86c2312 elementor-widget elementor-widget-image" data-id="86c2312" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="480" height="270" src="https://orcsune.com/wp-content/uploads/2024/12/never_bullet.gif" class="attachment-large size-large wp-image-3053" alt="" />															</div>
				</div>
				<div class="elementor-element elementor-element-428dbd9 elementor-widget elementor-widget-text-editor" data-id="428dbd9" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>On the topic of bullet patterns from videos, some time later I went back and modified things to basically allow for chromakeying out a background. Lo and behold, it turned out nicely, too. <a href="https://orcsune.com/blog/cutscene-integration-bb-update-6/">Here&#8217;s the update</a> where I did that.</p>								</div>
				</div>
				</div>
				</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/best-bullet-patterns/">Best Bullet Patterns</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Visual Novel Animations &#8211; BB &#8211; Update 14</title>
		<link>https://orcsune.com/blog/visual-novel-animations-bb-update-14/</link>
		
		<dc:creator><![CDATA[Orcsune Miku]]></dc:creator>
		<pubDate>Mon, 17 Mar 2025 12:21:43 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[Bulletly Bulletin]]></category>
		<category><![CDATA[Bullet Hell]]></category>
		<category><![CDATA[Game Design]]></category>
		<guid isPermaLink="false">https://orcsune.com/?p=4181</guid>

					<description><![CDATA[<p>Visual Novel Animations &#8211; BB &#8211; Update 14 Posted: March 17, 2025 Last Updated: March 17, 2025 Visual Novel Animations &#8211; BB &#8211; Update 14 Timeframe: February 25, 2025 &#8211; March 7, 2025 !!! Jump to Visual Demos !!! I bring you tidings of bullet hell updates! Like updates 10 &#38; 11, this and the [&#8230;]</p>
<p>The post <a href="https://orcsune.com/blog/visual-novel-animations-bb-update-14/">Visual Novel Animations &#8211; BB &#8211; Update 14</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="4181" class="elementor elementor-4181">
				<div class="elementor-element elementor-element-2078c4c2 e-flex e-con-boxed e-con e-parent" data-id="2078c4c2" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-4b86cab4 elementor-widget elementor-widget-text-editor" data-id="4b86cab4" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" />Visual Novel Animations &#8211; BB &#8211; Update 14<img decoding="async" class="wp-image-659" src="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png" alt="close up" width="50" height="50" srcset="https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-300x300.png 300w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-150x150.png 150w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-270x270.png 270w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-192x192.png 192w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-180x180.png 180w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2-32x32.png 32w, https://orcsune.com/wp-content/uploads/2024/03/cropped-cropped-april-fools-logo2-2.png 512w" sizes="(max-width: 50px) 100vw, 50px" /></h2><p><strong>Posted:</strong> March 17, 2025</p><p><strong>Last Updated:</strong> March 17, 2025</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-2c6b4922 e-flex e-con-boxed e-con e-parent" data-id="2c6b4922" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-5a63a02b elementor-widget elementor-widget-text-editor" data-id="5a63a02b" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><span style="text-decoration: underline;">Visual Novel Animations &#8211; BB &#8211; Update 14</span></p><p><span style="text-decoration: underline;">Timeframe: February 25, 2025 &#8211; March 7, 2025</span></p><p><a href="#videodemos">!!! Jump to Visual Demos !!!</a></p><p>I bring you tidings of bullet hell updates! Like updates 10 &amp; 11, this and the next update will come out in pretty quick succession to one another. Again, this is because I covered 2 pretty distinct things over the past (3-ish?) weeks: visual novel animations and modding (again&#8230;).</p><h2>OVNS ANIMATIONS:</h2>								</div>
				</div>
		<div class="elementor-element elementor-element-e72508e e-con-full e-flex e-con e-child" data-id="e72508e" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-1c9e943 e-con-full e-flex e-con e-child" data-id="1c9e943" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-3bd897a elementor-widget elementor-widget-image" data-id="3bd897a" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img decoding="async" src="https://orcsune.com/wp-content/uploads/2025/03/teaser.webp" title="teaser" alt="teaser" loading="lazy" />															</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-1d37885 e-con-full e-flex e-con e-child" data-id="1d37885" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-be9948d elementor-widget elementor-widget-image" data-id="be9948d" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="1024" height="574" src="https://orcsune.com/wp-content/uploads/2025/03/orc_appear.webp" class="attachment-large size-large wp-image-4201" alt="" />															</div>
				</div>
				</div>
				</div>
				<div class="elementor-element elementor-element-405f973 elementor-widget elementor-widget-text-editor" data-id="405f973" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>Animations:</h4><p>You can now define and invoke simplified, CSS-style animations on your VN&#8217;s sprites. Define an animation by sandwiching &#8220;keyframes&#8221; between <em>&lt;&lt;start_animation&gt;&gt;</em> and <em>&lt;&lt;end_animation&gt;&gt;</em> blocks. Invoke animations with the <em>&lt;&lt;animation&gt;&gt;</em> directive and apply various arguments to change how the animation is invoked.</p><h4>Keyframe Attributes:</h4><p>A keyframe represents the state of the sprite/image at a specific timepoint through the animation. Additionally, a keyframe consists of a percentage (the point during the animation when the sprite should achieve that keyframe&#8217;s state), and various sprite attributes reflecting the desired image state at that point.</p><ul><li><strong>width</strong> : The width of the sprite. Use dimensions.</li><li><strong>height</strong> : The height of the sprite. Use dimensions.</li><li><strong>x</strong> : The x position of the sprite. Use dimensions.</li><li><strong>y</strong> : The y position of the sprite. Use dimensions.</li><li><strong>color</strong> : The base color of the sprite. Define this as a vector of 4 values between 0 and 1.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-e84169f elementor-widget elementor-widget-code-block-for-elementor" data-id="e84169f" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-default' data-show-toolbar='yes'><code class='language-csharp'>// Example animation definitions:

// Stretches and squashes a sprite
// to first be wider and shorter
// and then taller and thinner before
// going back to its original size.
&lt;&lt;start_animation&gt;&gt; stretchnsquash
    0%:     width:100%o height:100%o
    33%:    width:130%o height:70%o
    66%:    width:70%o height:130%o
    100%:   width:100%o height:100%o
&lt;&lt;end_animation&gt;&gt;

// Cycles through different color
// hues on the sprite.
&lt;&lt;start_animation&gt;&gt; hue_cycle
    0%:         color:&quot;1,0,0,1&quot;
    16.67%:     color:&quot;1,1,0,1&quot;
    33.33%:     color:&quot;0,1,0,1&quot;
    50%:        color:&quot;0,1,1,1&quot;
    66.67%:     color:&quot;0,0,1,1&quot;
    83.33%:     color:&quot;1,0,1,1&quot;
    100%:       color:&quot;1,0,0,1&quot;
&lt;&lt;end_animation&gt;&gt;</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-1ec94bd elementor-widget elementor-widget-text-editor" data-id="1ec94bd" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>Animation Invocation:</h4><p>After defining an animation with keyframes, you need to invoke the animation on a sprite slot to actually use it.</p><ul><li><strong>key</strong> : Required. The name of the animation to play.</li><li><strong>slot</strong> : Required. The sprite slot key to which you want to apply the animation.</li><li><strong>time</strong> : Required. The amount of time the animation lasts per repetition.</li><li><strong>block</strong> : The trigger block type.<ul><li>‘none‘ : Default. Does not block execution of following directives.</li><li>‘auto‘ : Blocks execution of following directives while the animation is playing. Once the animation is over, execution proceeds automatically to the next directive.</li><li>‘wait‘ : Blocks execution of following directives while the animation is playing. Once the animation is over, it remains blocked until the user provides input.</li></ul></li><li><strong>interrupt</strong> : Boolean. If true, then running this animation will auto-finish any animations currently playing on the target sprite slot before beginning. If false, this animation applies on top of any current animations.</li><li><strong>reset</strong> : Boolean. Whether the sprite slot should revert to its pre-animation state at the end of the animation.</li><li><strong>repeat</strong> : An integer describing how many times the animation should repeat. Default is 1.</li><li><strong>repeatType</strong> : How the animation repeats.<ul><li>‘loop‘ : Default. The animation starts over from the beginning at the start of each repetition.</li><li>‘reverse‘ : The animation regresses in the opposite direction once it finishes. This causes the animation to take twice as long for one repetition.</li></ul></li><li><strong>ease</strong> : Which preset ease function to apply to the animation fraction.<ul><li>‘linear‘ : Default. The output value follows the form f(input) = input.</li><li>‘<a href="https://easings.net/#easeInOutCubic">easeInOutCubic</a>‘ : A function that makes the animation slower at both ends and faster in the middle.</li><li>‘<a href="https://easings.net/#easeInOutElastic">easeInOutElastic</a>‘ : Like easeInOutCubic except with some elastic properties.</li><li>‘<a href="https://easings.net/#easeInQuint">easeInQuint</a>‘ : A function that starts slow and speeds up at the end.</li><li>‘<a href="https://easings.net/#easeOutQuint">easeOutQuint</a>‘ : A function that starts fast and slows down at the end.</li></ul></li><li><strong>delay</strong> : The amount of time to wait before the animation initially starts.</li><li><strong>repeatDelay</strong> : The amount of time to wait between each repetition of the animation.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-704a469 elementor-widget elementor-widget-code-block-for-elementor" data-id="704a469" data-element_type="widget" data-e-type="widget" data-widget_type="code-block-for-elementor.default">
				<div class="elementor-widget-container">
					<pre class='line-numbers theme-default' data-show-toolbar='yes'><code class='language-js'>// Example setup and invocations:

// Set up the sprites
&lt;&lt;sprite&gt;&gt; slot:custom_slot1 key:gregg_casual flip:autox color:1,1,1,1 size:60%v,60%v pos:10%h,0
&lt;&lt;sprite&gt;&gt; slot:custom_slot2 key:gregg_blank flip:autox color:1,1,1,1 size:60%v,60%v pos:90%h,0
&lt;&lt;sprite&gt;&gt; slot:custom_slot3 key:gregg_casual flip:autox color:1,1,1,1 size:60%v,60%v pos:40%h,0
&lt;&lt;sprite&gt;&gt; slot:custom_slot4 key:gregg_blank flip:autox color:1,1,1,1 size:60%v,60%v pos:60%h,0
&lt;&lt;sprite&gt;&gt; slot:custom_slot5 key:gregg_casual flip:auto color:1,1,1,1 size:60%v,60%v pos:25%h,100%v
&lt;&lt;sprite&gt;&gt; slot:custom_slot6 key:gregg_blank flip:auto color:1,1,1,1 size:60%v,60%v pos:75%h,100%v

// Call a color-cycling animation on 2 of them.
&lt;&lt;animation&gt;&gt; key:hue_cycle slot:custom_slot1 time:2 repeat:1000 delay:3 interrupt:true repeatType:loop block:none
&lt;&lt;animation&gt;&gt; key:hue_cycle slot:custom_slot2 time:2 repeat:1000 delay:3 interrupt:true repeatType:loop block:none
// Call a stretch &amp; squash animation on another 2.
&lt;&lt;animation&gt;&gt; key:stretchnsquash slot:custom_slot3 time:0.8 repeat:1000 delay:3 interrupt:true repeatType:loop block:none ease:easeInOutCubic
&lt;&lt;animation&gt;&gt; key:stretchnsquash slot:custom_slot4 time:0.8 repeat:1000 delay:3.4 interrupt:true repeatType:loop block:none ease:easeInOutCubic
// Call both animations on the last 2.
&lt;&lt;animation&gt;&gt; key:hue_cycle slot:custom_slot5 time:2 repeat:1000 delay:3 interrupt:true repeatType:loop block:none
&lt;&lt;animation&gt;&gt; key:hue_cycle slot:custom_slot6 time:2 repeat:1000 delay:3 interrupt:true repeatType:loop block:none
&lt;&lt;animation&gt;&gt; key:stretchnsquash slot:custom_slot5 time:0.8 repeat:1000 delay:3.4 interrupt:false repeatType:loop block:none ease:easeInOutCubic
&lt;&lt;animation&gt;&gt; key:stretchnsquash slot:custom_slot6 time:0.8 repeat:1000 delay:3 interrupt:false repeatType:loop block:none ease:easeInOutCubic</code></pre>				</div>
				</div>
				<div class="elementor-element elementor-element-10c9d63 elementor-widget elementor-widget-image" data-id="10c9d63" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="1024" height="574" src="https://orcsune.com/wp-content/uploads/2025/03/gregg_example.webp" class="attachment-large size-large wp-image-4198" alt="" />															</div>
				</div>
				<div class="elementor-element elementor-element-9feb92c elementor-widget elementor-widget-text-editor" data-id="9feb92c" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h4>New Directives:</h4><ul><li><strong>&lt;&lt;reset_slots&gt;&gt;</strong>: You now pass in slot keys which you want to reset.</li><li><strong>&lt;&lt;reset_all_slots&gt;&gt;</strong>: Now resets all slots and deletes all custom sprite slots.</li><li><strong>&lt;&lt;stop_animations&gt;&gt;</strong>: Pass in slots for which you wish to stop all running animations.</li><li><strong>&lt;&lt;start_animation&gt;&gt;</strong>: Start parsing animation keyframes.</li><li><strong>&lt;&lt;end_animation&gt;&gt;</strong>: End parsing animation keyframes.</li><li><strong>&lt;&lt;animation&gt;&gt;</strong>: Invokes a named animation on a sprite slot along with other invocation arguments.</li></ul><h4>Dimensions:</h4><p>A minor but important change was the addition of <code>Dimensions</code>. These just provide a better way to define position and size on the screen with options that allow for scaling based on screen dimensions and other relative size options. Read more <a href="https://orcsune.com/orcsune-visual-novel-script-documentation/">in the documentation here</a>.</p><h4>OVNS Builder Animation Blocks:</h4><p>While I was at it, I added the relevant animation blocks to the <a href="https://orcsune.com/ovns-builder-documentation/">OVNS builder</a>. I made two new blocks:</p><ul><li><strong>Define Animation Block:</strong> Provides an editor that allows the user to add/remove keyframes and edit the sprite attributes at each keyframe.</li><li><strong>Invoke Animation Block:</strong> Provides an editor for invoking an animation on a sprite slot similar to the settings block.</li></ul>								</div>
				</div>
		<div class="elementor-element elementor-element-a04521b e-con-full e-flex e-con e-child" data-id="a04521b" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-415f945 elementor-widget elementor-widget-image" data-id="415f945" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img decoding="async" src="https://orcsune.com/wp-content/uploads/2025/03/keyframe_editor.webp" title="keyframe_editor" alt="keyframe_editor" loading="lazy" />															</div>
				</div>
				<div class="elementor-element elementor-element-c987e2a elementor-widget elementor-widget-image" data-id="c987e2a" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img decoding="async" src="https://orcsune.com/wp-content/uploads/2025/03/invocation.webp" title="invocation" alt="invocation" loading="lazy" />															</div>
				</div>
				</div>
				<div class="elementor-element elementor-element-4451264 elementor-widget elementor-widget-text-editor" data-id="4451264" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h3>Miscellaneous:</h3><ul><li>Changing a sprite in a builder now registers as an undo-able action.</li><li>Changed the kill system order so that things are still visible on the frame they are killed.</li><li>Upgraded my VN trigger system a little bit:<ul><li><strong>IRunningTrigger</strong>: A new interface that represents arbitrary blocking triggers. Check if a trigger is done with internal logic, force trigger completion, or reset trigger state.</li><li><strong>AnimationRunningTrigger</strong>: An <code>IRunningTrigger</code> to interface between <code>TriggerControl</code> and <code>SpriteSlots</code>. Animations call <code>AnimationRunningTrigger.Complete()</code> to indicate that the animations are no longer blocking.</li><li><strong>TriggerControl</strong>: Updated to check the blocking trigger for its &#8216;done&#8217; status and has options for continued blocking based on user input.</li></ul></li><li>OVNS Builder now allows a few more keyboard controls (ESC) to make things nicer to use. &#8220;New-on-output&#8221; hotkey pulls up block search and autoconnects new block to the last selected block.</li><li><code>RepositionInsideScreen</code> bugfix fixes tooltips and block search panel.</li><li>Asset selection button on some settings. When you need to select an asset (sprite, font, audio clip,&#8230;), you previously needed to know the key for that asset and manually type it in. Now, you can click the asset selector and browse for an asset, and it should return the valid key (if the file is already registered in an asset map) or just the path to the file.</li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-7869357d elementor-widget elementor-widget-text-editor" data-id="7869357d" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2 id="videodemos">Video Demos</h2>								</div>
				</div>
				<div class="elementor-element elementor-element-14ea091 elementor-widget elementor-widget-text-editor" data-id="14ea091" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p>I&#8217;ve been putting images throughout my recent posts rather than having a specific demo section. I think it&#8217;s nice for breaking up big text walls. Let me know what you think about it.</p>								</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-693093f5 e-flex e-con-boxed e-con e-parent" data-id="693093f5" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-723e6ccd elementor-widget elementor-widget-wp-widget-nav_menu" data-id="723e6ccd" data-element_type="widget" data-e-type="widget" data-widget_type="wp-widget-nav_menu.default">
				<div class="elementor-widget-container">
					<div class="menu-bulletblognavmenu-container"><ul id="menu-bulletblognavmenu-1" class="menu"><li class="menu-item menu-item-type-post_type menu-item-object-post menu-item-2056"><a href="https://orcsune.com/blog/bulletly-bulletin-index/">Bulletly Bulletin Index</a></li>
<li class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-1890"><a href="https://orcsune.com/category/bb/">Bulletly Bulletin</a></li>
</ul></div>				</div>
				</div>
					</div>
				</div>
				</div>
		<p>The post <a href="https://orcsune.com/blog/visual-novel-animations-bb-update-14/">Visual Novel Animations &#8211; BB &#8211; Update 14</a> appeared first on <a href="https://orcsune.com">Orcsune</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
