<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Rafael Epplée</title>
    <subtitle>Software engineer, artist and musician</subtitle>
    <link href="https://www.rafa.ee/atom.xml" rel="self" type="application/atom+xml"/>
    <link href="https://www.rafa.ee"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2023-09-12T00:00:00+00:00</updated>
    <id>https://www.rafa.ee/atom.xml</id>
    <entry xml:lang="en">
        <title>A CSS-Only Overflow Indicator for Multiline Text</title>
        <published>2023-09-12T00:00:00+00:00</published>
        <updated>2023-09-12T00:00:00+00:00</updated>
        <author>
          <name>Unknown</name>
        </author>
        <link rel="alternate" href="https://www.rafa.ee/articles/css-only-overflow-indicator-for-multiline-text/" type="text/html"/>
        <id>https://www.rafa.ee/articles/css-only-overflow-indicator-for-multiline-text/</id>
        
        <content type="html">&lt;p&gt;When building &lt;a href=&quot;https:&#x2F;&#x2F;archive.observer&quot;&gt;archive.observer&lt;&#x2F;a&gt;, a page for browsing the AskHistorians subreddit archives, I wanted to preview the text for each post in a list:&lt;&#x2F;p&gt;
&lt;img src=&quot;no_overflow.png&quot; &#x2F;&gt;
&lt;p&gt;Posts in that subreddit can be quite long, so I wanted to truncate the preview to a fixed number of lines. If that happened, I needed an indicator showing that there was more text available to read. I settled for a “fadeout” effect:&lt;&#x2F;p&gt;
&lt;img src=&quot;overflow_goal.png&quot; &#x2F;&gt;
&lt;p&gt;In this post, I’ll show you how to implement this indicator, along with a little trick that makes sure it works in all of these cases:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The indicator is only shown if the text actually overflows its container.&lt;&#x2F;li&gt;
&lt;li&gt;It works for multiple lines of text (many techniques documented elsewhere don’t).&lt;&#x2F;li&gt;
&lt;li&gt;If the text needs less space than the maximum height, the container shrinks to fit the text.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-code&quot;&gt;The Code&lt;&#x2F;h2&gt;
&lt;p&gt;This is the code we’ll produce:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span style=&quot;color:#8fbcbb;&quot;&gt;.hide-overflow &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    position&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;relative&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    max-height&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;15&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;75&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    overflow&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;hidden&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fbcbb;&quot;&gt;.hide-overflow&lt;&#x2F;span&gt;&lt;span&gt;::after {
&lt;&#x2F;span&gt;&lt;span&gt;    position&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;absolute&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    height&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;5&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;25&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    left&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    right&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    background&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;linear-gradient&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;transparent&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#81a1c1;&quot;&gt;white&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    content&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    pointer-events&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;none&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;&#x2F;* If text is shorter than max-height, push the 
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;    overflow indicator down so it&amp;#39;s hidden by the overflow. *&#x2F;
&lt;&#x2F;span&gt;&lt;span&gt;    bottom&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;calc&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;100&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;% - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;15&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;75&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;20&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It assumes a HTML structure like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;lt;div &lt;&#x2F;span&gt;&lt;span style=&quot;color:#8fbcbb;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot;hide-overflow&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;lt;p&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;        Lorem Ipsum dolor sit amet...
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;lt;&#x2F;p&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;lt;&#x2F;div&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;walkthrough&quot;&gt;Walkthrough&lt;&#x2F;h2&gt;
&lt;p&gt;The CSS is mostly a standard affair. We give the container a class, set a max-height and hide the overflow:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span style=&quot;color:#8fbcbb;&quot;&gt;.hide-overflow &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    position&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;relative&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    max-height&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;15&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    overflow&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;hidden&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Using &lt;code&gt;position: relative&lt;&#x2F;code&gt; allows us to position the &lt;code&gt;::after&lt;&#x2F;code&gt; pseudo-element relative to the bottom edges of our container:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span style=&quot;color:#8fbcbb;&quot;&gt;.hide-overflow&lt;&#x2F;span&gt;&lt;span&gt;::after {
&lt;&#x2F;span&gt;&lt;span&gt;    position&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;absolute&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    height&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;5&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    left&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    right&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    bottom&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;&#x2F;* ... *&#x2F;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So far, so good. Let’s make the indicator visible using a simple gradient:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span style=&quot;color:#8fbcbb;&quot;&gt;.hide-overflow&lt;&#x2F;span&gt;&lt;span&gt;::after {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;&#x2F;* ... *&#x2F;
&lt;&#x2F;span&gt;&lt;span&gt;    background&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;linear-gradient&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;transparent&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#81a1c1;&quot;&gt;white&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    content&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    pointer-events&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;none&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;&#x2F;* ... *&#x2F;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;pointer-events: none&lt;&#x2F;code&gt; ensures that users can still select text below the indicator.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s the trick. We change the &lt;code&gt;bottom&lt;&#x2F;code&gt; style to actually hide this indicator when the text fits inside the containers &lt;code&gt;max-height&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span style=&quot;color:#8fbcbb;&quot;&gt;.hide-overflow&lt;&#x2F;span&gt;&lt;span&gt;::after {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;&#x2F;* ... *&#x2F;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    bottom&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;calc&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;100&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;% - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;15&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;20&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;100% - 15rem&lt;&#x2F;code&gt; is the key: This expression is lower than zero if the actual height of the container (&lt;code&gt;100%&lt;&#x2F;code&gt;) is smaller than its maximum height (&lt;code&gt;15rem&lt;&#x2F;code&gt;), meaning the text didn’t overflow the container. If it is lower than zero, we multiply it by an arbitrarily large number (in this example it’s 20) to &lt;em&gt;move the indicator below the bottom edge of the container&lt;&#x2F;em&gt;, effectively hiding it. The whole thing is basically a roundabout way of using &lt;code&gt;calc&lt;&#x2F;code&gt; to “detect” if there’s any text overflow.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s an example showing the position of the hidden indicator in green. In this case, the &lt;code&gt;bottom&lt;&#x2F;code&gt; property has a computed value of &lt;code&gt;-160px&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;img src=&quot;moved_indicator_visualized.png&quot; &#x2F;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;It took me a little while to work out this trick and I found no alternatives with similar features on the web; Even though it’s a hackish workaround, I’m quite fond of its simplicity, and I think other websites might profit from it as well. Reddit, for example, seems to use JS to determine whether the overflow indicator should be visible or not.&lt;&#x2F;p&gt;
&lt;p&gt;I tried to implement the indicator using &lt;a href=&quot;https:&#x2F;&#x2F;www.smashingmagazine.com&#x2F;2021&#x2F;05&#x2F;complete-guide-css-container-queries&#x2F;&quot;&gt;container queries&lt;&#x2F;a&gt;, but couldn’t get around some of their limitations. &lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Deploying Godot 4 HTML exports with cross-origin isolation</title>
        <published>2022-11-14T00:00:00+00:00</published>
        <updated>2022-11-14T00:00:00+00:00</updated>
        <author>
          <name>Unknown</name>
        </author>
        <link rel="alternate" href="https://www.rafa.ee/articles/deploying-godot-4-html-exports/" type="text/html"/>
        <id>https://www.rafa.ee/articles/deploying-godot-4-html-exports/</id>
        
        <content type="html">&lt;p&gt;Godot 4 games exported to HTML use a JavaScript feature called &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;JavaScript&#x2F;Reference&#x2F;Global_Objects&#x2F;SharedArrayBuffer&quot;&gt;SharedArrayBuffer&lt;&#x2F;a&gt;.
This allows sharing memory between the main thread and service workers efficiently.
However, it makes it possible for scripts loaded from other pages to access this shared memory as well, which opens a page up to security vulnerabilities.&lt;&#x2F;p&gt;
&lt;aside&gt;
    &lt;small&gt;Side note&lt;&#x2F;small&gt;&lt;br &#x2F;&gt;
    Check out my
    &lt;a href=&quot;https:&#x2F;&#x2F;raffomania.itch.io&#x2F;knakk&quot;
        &gt;free and open source game knakk&lt;&#x2F;a
    &gt;!
&lt;&#x2F;aside&gt;
&lt;p&gt;To prevent vulnerabilities, sites using the &lt;code&gt;SharedArrayBuffer&lt;&#x2F;code&gt; feature need to fulfill some security requirements:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;They must be in a &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;Security&#x2F;Secure_Contexts&quot;&gt;Secure Context&lt;&#x2F;a&gt;, which is mostly achieved by serving the site over HTTPS.&lt;&#x2F;li&gt;
&lt;li&gt;They need to set a suitable &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTTP&#x2F;Headers&#x2F;Cross-Origin-Opener-Policy&quot;&gt;Cross-Origin-Opener-Policy (COOP)&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTTP&#x2F;Headers&#x2F;Cross-Origin-Embedder-Policy&quot;&gt;Cross-Origin-Embedder-Policy (COEP)&lt;&#x2F;a&gt; header resulting in “Cross-Origin Isolation”.
The standard values for these are &lt;code&gt;same-origin&lt;&#x2F;code&gt; and &lt;code&gt;require-corp&lt;&#x2F;code&gt; respectively, but &lt;a href=&quot;https:&#x2F;&#x2F;developer.chrome.com&#x2F;blog&#x2F;coep-credentialless-origin-trial&#x2F;&quot;&gt;Chrome supports &lt;code&gt;credentialless&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; as an embedder policy as well.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Let’s look at how we can fulfill these requirements when deploying our games to itch.io and Netlify.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;itch-io&quot;&gt;itch.io&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;itch.io&#x2F;t&#x2F;2025776&#x2F;experimental-sharedarraybuffer-support&quot;&gt;itch.io has experimental support for SharedArrayBuffers&lt;&#x2F;a&gt;.
All we have to do is tick the “SharedArrayBuffer support” checkbox in the &lt;code&gt;Embed Options &amp;gt; Frame Options&lt;&#x2F;code&gt; section of our game’s settings page.&lt;&#x2F;p&gt;
&lt;p&gt;Setting the standard Cross-Origin Isolation headers breaks some features that interact with other domains. 
itch.io uses Chrome’s &lt;code&gt;credentialless&lt;&#x2F;code&gt; option for the COEP header which lifts some of the restrictions imposed by the &lt;code&gt;require-corp&lt;&#x2F;code&gt; option.
Firefox does not implement &lt;code&gt;credentialless&lt;&#x2F;code&gt; at the time of writing, meaning Godot 4 HTML exports deployed on itch.io will only run in Chrome-based browsers.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;netlify&quot;&gt;Netlify&lt;&#x2F;h2&gt;
&lt;p&gt;In contrast to itch.io and other simple hosting options like GitHub Pages, Netlify &lt;a href=&quot;https:&#x2F;&#x2F;docs.netlify.com&#x2F;routing&#x2F;headers&#x2F;&quot;&gt;allows setting custom headers&lt;&#x2F;a&gt; using a &lt;code&gt;_headers&lt;&#x2F;code&gt; file or the &lt;code&gt;netlify.toml&lt;&#x2F;code&gt; configuration file.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s the entry in &lt;code&gt;netlify.toml&lt;&#x2F;code&gt; for setting the required cross origin policy headers to run both in Chrome and Firefox:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;&lt;span&gt;[[headers]]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;for &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot;&#x2F;*&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;[headers&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;values]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;Cross-Origin-Opener-Policy &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot;same-origin&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;Cross-Origin-Embedder-Policy &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot;require-corp&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;&#x2F;h2&gt;
&lt;p&gt;It has only been about half a year since Chrome shipped &lt;code&gt;credentialless&lt;&#x2F;code&gt; support, and &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mozilla&#x2F;standards-positions&#x2F;issues&#x2F;539#issuecomment-1224320026&quot;&gt;Firefox is testing it using an Origin Trial&lt;&#x2F;a&gt;. 
Going forward, it’s likely we’ll see &lt;code&gt;credentialless&lt;&#x2F;code&gt; as the standard way to enable SharedArrayBuffers, resolving the itch.io Firefox incompatibility.&lt;&#x2F;p&gt;
&lt;p&gt;I’d love to deploy my games to GitHub Pages as well.
GitHub pages does not allow setting custom headers, but &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;community&#x2F;community&#x2F;discussions&#x2F;13309&quot;&gt;there is a feature request for COOP&#x2F;COEP with a potential workaround&lt;&#x2F;a&gt; that uses a service worker to set the required headers.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;see-also&quot;&gt;See also&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;web.dev&#x2F;coop-coep&#x2F;&quot;&gt;Cross-origin isolation Guide on web.dev&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.chrome.com&#x2F;blog&#x2F;coep-credentialless-origin-trial&#x2F;&quot;&gt;Chrome’s post on credentialless support has a good explanation of Cross-Origin Isolation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.rzegocki.pl&#x2F;blog&#x2F;custom-http-headers-with-github-pages&#x2F;&quot;&gt;Proxying GitHub Pages through Cloudflare and Heroku allows setting custom headers&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Making peer-to-peer multiplayer seamless with Godot</title>
        <published>2022-01-02T00:00:00+00:00</published>
        <updated>2022-01-02T00:00:00+00:00</updated>
        <author>
          <name>Unknown</name>
        </author>
        <link rel="alternate" href="https://www.rafa.ee/articles/godot-peer-to-peer-multiplayer/" type="text/html"/>
        <id>https://www.rafa.ee/articles/godot-peer-to-peer-multiplayer/</id>
        
        <content type="html">&lt;p&gt;When we started developing our recently-released mini-strategy game &lt;span class=&quot;nowrap&quot;&gt;&lt;a href=&quot;https:&#x2F;&#x2F;pomme-grenade.itch.io&#x2F;2planets&quot;&gt;2 Planets&lt;&#x2F;a&gt;&lt;&#x2F;span&gt;, we wanted to add networked multiplayer support.
Since the game is a side project and would be released for free, we didn’t want to spend much time and money on maintaining dedicated servers.
Instead, we chose a simple peer-to-peer networking setup where both players would connect to the other player’s machine directly.&lt;&#x2F;p&gt;
&lt;aside&gt;
    &lt;small&gt;Side note&lt;&#x2F;small&gt;&lt;br &#x2F;&gt;
    Check out my
    &lt;a href=&quot;https:&#x2F;&#x2F;raffomania.itch.io&#x2F;knakk&quot;
        &gt;free and open source game knakk&lt;&#x2F;a
    &gt;!
&lt;&#x2F;aside&gt;
&lt;p&gt;Our initial implementation was very simple - players entered the IP of their match partner, we passed it to Godot and let the engine handle the rest. This had some problems:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Entering IP addresses by hand is cumbersome and error-prone&lt;&#x2F;li&gt;
&lt;li&gt;No matchmaking support&lt;&#x2F;li&gt;
&lt;li&gt;Machines behind a &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Network_address_translation&quot;&gt;NAT&lt;&#x2F;a&gt;-enabled router are not accessible directly&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;To solve these problems, we set up a small “rendezvous” server capable of &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Hole_punching_(networking)&quot;&gt;NAT hole punching&lt;&#x2F;a&gt;.
When starting a game, players connect to the publicly available server, which sends them the IP address of their match partner and punches holes into router’s NATs if needed.
Both game clients then connect directly to each other, and the server’s job is done.
This process allowed us to connect players using small “Match codes” which were shorter and easier to type than IP addresses. We haven’t implemented matchmaking yet, but adding it to the current implementation would be a simple task.&lt;&#x2F;p&gt;
&lt;video controls&gt;
    &lt;source src=&quot;game_code_demo.mp4&quot; type=&quot;video&#x2F;mp4&quot;&gt;
&lt;&#x2F;video&gt;
&lt;small&gt;Two players starting a game over the network in 2 Planets&lt;&#x2F;small&gt;
&lt;p&gt;I consider this approach ideal for small indie games.
Dedicated servers allow better cheat protection, and NAT hole punching doesn’t work all the time, but with this approach, the maintenance burden and complexity is considerably lower, both for the game’s code and the server’s operation.&lt;&#x2F;p&gt;
&lt;p&gt;If you’re looking for a way to have peer-to-peer multiplayer without setting up your own server, have a look at &lt;a href=&quot;https:&#x2F;&#x2F;partner.steamgames.com&#x2F;doc&#x2F;features&#x2F;multiplayer&#x2F;networking&quot;&gt;Steam’s Networking API&lt;&#x2F;a&gt; which also has a &lt;a href=&quot;https:&#x2F;&#x2F;gramps.github.io&#x2F;GodotSteam&#x2F;functions-module.html#networking&quot;&gt;Godot integration via GodotSteam&lt;&#x2F;a&gt;, or have a look at the &lt;a href=&quot;http:&#x2F;&#x2F;dev.epicgames.com&#x2F;docs&#x2F;services&#x2F;en-US&#x2F;GameServices&#x2F;P2P&#x2F;index.html&quot;&gt;Epic’s NAT P2P API&lt;&#x2F;a&gt;. Both provide NAT hole punching support with relay services as a fallback.&lt;&#x2F;p&gt;
&lt;p&gt;If, on the other hand, you want to stay independent of Steam and Epic or just like to do things yourself, continue reading! In the following you’ll find a short guide on setting up peer-to-peer multiplayer with NAT hole-punching in Godot for your own game, using your own server.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setting-up-your-own&quot;&gt;Setting up your own&lt;&#x2F;h2&gt;
&lt;p&gt;You’ll need the following:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The excellent &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SLGamesCregg&#x2F;HolePuncher&quot;&gt;HolePuncher&lt;&#x2F;a&gt; Godot plugin and server by Cregg Hancock&lt;&#x2F;li&gt;
&lt;li&gt;A server or service allowing you to host a small python application (e.g. &lt;a href=&quot;https:&#x2F;&#x2F;www.heroku.com&#x2F;&quot;&gt;Heroku&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;vercel.com&#x2F;&quot;&gt;Vercel&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Basic networking and server administration knowledge. Using a service like Heroku, you can get a python server up and running pretty easily, but you’ll still need some experience to configure everything correctly.&lt;&#x2F;li&gt;
&lt;li&gt;An understanding of &lt;a href=&quot;https:&#x2F;&#x2F;docs.godotengine.org&#x2F;en&#x2F;stable&#x2F;tutorials&#x2F;networking&#x2F;high_level_multiplayer.html&quot;&gt;Godot’s multiplayer API&lt;&#x2F;a&gt; for connecting clients using the IP obtained from the rendezvous server and synchronizing the game state.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;First, deploy the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SLGamesCregg&#x2F;HolePuncher&quot;&gt;HolePuncher&lt;&#x2F;a&gt; python server. Make sure it is accessible from your development machine. You don’t need a domain: if your server has a static IP, you can use that to let your clients initiate a connection.&lt;&#x2F;p&gt;
&lt;p&gt;Next, download and include the HolePuncher Godot plugin in your game. Copy the &lt;code&gt;addons&lt;&#x2F;code&gt; folder from the HolePuncher repository and place it at the root of your Godot project folder. Afterwards, go to &lt;em&gt;Project &amp;gt; Project Settings &amp;gt; Plugins&lt;&#x2F;em&gt; and enable the HolePuncher plugin.
You can now add and configure a &lt;code&gt;HolePunch&lt;&#x2F;code&gt; node to your menu scene.
We’ll do that in the following script:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span&gt;hole_puncher &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;preload&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;#39;res:&#x2F;&#x2F;addons&#x2F;Holepunch&#x2F;holepunch_node.gd&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;new&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;# your rendezvous server IP or domain
&lt;&#x2F;span&gt;&lt;span&gt;hole_puncher&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;rendevouz_address &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot;1.1.1.1&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;# the port the HolePuncher python application is running on
&lt;&#x2F;span&gt;&lt;span&gt;hole_puncher&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;rendevouz_port &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;quot;3000&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;add_child&lt;&#x2F;span&gt;&lt;span&gt;(hole_puncher)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Refer to the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SLGamesCregg&#x2F;HolePuncher&quot;&gt;HolePuncher documentation&lt;&#x2F;a&gt; to learn more about its configuration. &lt;br &#x2F;&gt;
You can now start the NAT traversal by calling the hole puncher’s &lt;code&gt;start_traversal&lt;&#x2F;code&gt; method:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;# Generate a unique ID for this machine
&lt;&#x2F;span&gt;&lt;span&gt;var player_id &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;OS&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;get_unique_id&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;hole_puncher&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;start_traversal&lt;&#x2F;span&gt;&lt;span&gt;(game_code&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span&gt;is_host&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span&gt;player_id)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;# Yield an array of [own_port, host_port, host_ip]
&lt;&#x2F;span&gt;&lt;span&gt;var result &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= yield&lt;&#x2F;span&gt;&lt;span&gt;(hole_puncher&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;#39;hole_punched&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Run this code on both machines, with &lt;code&gt;is_host&lt;&#x2F;code&gt;
set to &lt;code&gt;true&lt;&#x2F;code&gt; on one machine, and to &lt;code&gt;false&lt;&#x2F;code&gt; on the other. &lt;br &#x2F;&gt;
The &lt;code&gt;game_code&lt;&#x2F;code&gt; variable is the match identifier, telling the server which peers’ IP addresses belong together. You can use any format, as long as it is unique for each match and both peers in a match use the same value. For an example on how to generate random game codes, look at &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pomme-grenade&#x2F;2planets&#x2F;blob&#x2F;8aa99e0e7549b7928763405bff5a198273ae4a63&#x2F;menu&#x2F;lobby_networking.gd#L141&quot;&gt;this function in the 2 Planets source code&lt;&#x2F;a&gt;. &lt;br &#x2F;&gt;
After generating, you can display the game code you generated in your menu for players to exchange it through voice or chat messaging.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;yield&lt;&#x2F;code&gt; call above turns your code into a &lt;a href=&quot;https:&#x2F;&#x2F;docs.godotengine.org&#x2F;en&#x2F;stable&#x2F;getting_started&#x2F;scripting&#x2F;gdscript&#x2F;gdscript_basics.html#coroutines-with-yield&quot;&gt;coroutine&lt;&#x2F;a&gt; which will continue running after all the network calls have succeeded, which can take a few seconds.
When the hole puncher is done, you can use the returned information to create a Godot network peer.
You might use it like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;# Start a host
&lt;&#x2F;span&gt;&lt;span&gt;var result &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= yield&lt;&#x2F;span&gt;&lt;span&gt;(hole_puncher&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;#39;hole_punched&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;var my_port &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;result[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;var peer &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;NetworkedMultiplayerENet&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;new&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;peer&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;create_server&lt;&#x2F;span&gt;&lt;span&gt;(my_port&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;get_tree&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;set_network_peer&lt;&#x2F;span&gt;&lt;span&gt;(peer)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;python&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span style=&quot;color:#616e88;&quot;&gt;# Connect a client to a host
&lt;&#x2F;span&gt;&lt;span&gt;var result &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= yield&lt;&#x2F;span&gt;&lt;span&gt;(hole_puncher&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;&amp;#39;hole_punched&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;var host_ip &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;result[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span&gt;var host_port &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;result[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span&gt;var own_port &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;result[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;var peer &lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;NetworkedMultiplayerENet&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;new&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;peer&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;create_client&lt;&#x2F;span&gt;&lt;span&gt;(host_ip&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span&gt;host_port&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color:#eceff4;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span&gt;own_port)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;get_tree&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;set_network_peer&lt;&#x2F;span&gt;&lt;span&gt;(peer)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;After starting a host on one machine and connecting a client on another, you can start the game! Congratulations, you now have peer-to-peer multiplayer support from (almost) any network!&lt;&#x2F;p&gt;
&lt;p&gt;Of course, this example only works for two players and is heavily simplified, you’ll need to add at least a way for players to exchange game codes and handle errors that appear during connection attempts. If you’d like to see a more realistic example, have a look at the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pomme-grenade&#x2F;2planets&#x2F;blob&#x2F;master&#x2F;menu&#x2F;lobby_networking.gd&quot;&gt;lobby code from &lt;span class=&quot;nowrap&quot;&gt;2 Planets&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;. If you’re interested in adding matchmaking, have a look at the source code of the HolePuncher server. It’s pretty compact.&lt;&#x2F;p&gt;
&lt;p&gt;I hope this guide was a good starting point for adding NAT hole punching to your game. NAT hole punching is a complex topic and you will probably have to look into other resources for a robust solution that fits your use-case, but getting started is surprisingly easy! If you have any questions or comments, feel free to reach out on &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;rafaelepplee&quot;&gt;twitter&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;user&#x2F;raffomania&quot;&gt;reddit&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Unity: Wishes for 2021</title>
        <published>2020-12-02T00:00:00+00:00</published>
        <updated>2020-12-02T00:00:00+00:00</updated>
        <author>
          <name>Unknown</name>
        </author>
        <link rel="alternate" href="https://www.rafa.ee/articles/unity/" type="text/html"/>
        <id>https://www.rafa.ee/articles/unity/</id>
        
        <content type="html">&lt;p&gt;I’ve recently finished building a &lt;a href=&quot;https:&#x2F;&#x2F;raffomania.itch.io&#x2F;dead-science&quot;&gt;VR project&lt;&#x2F;a&gt; in Unity. Since I’ve worked with Unity before and have gathered a little experience, I’m going to talk about that and highlight some topics I’d love to see addressed in the next few years.&lt;&#x2F;p&gt;
&lt;aside&gt;
    &lt;small&gt;Side note&lt;&#x2F;small&gt;&lt;br &#x2F;&gt;
    Check out my
    &lt;a href=&quot;https:&#x2F;&#x2F;raffomania.itch.io&#x2F;knakk&quot;
        &gt;free and open source game knakk&lt;&#x2F;a
    &gt;!
&lt;&#x2F;aside&gt;
&lt;h2 id=&quot;don-t-touch-that&quot;&gt;Don’t touch that&lt;&#x2F;h2&gt;
&lt;p&gt;Reading on how to implement a UI, I found this table in the unity docs explaining the situation at hand:&lt;&#x2F;p&gt;
&lt;img src=&quot;unity-ui-libs.png&quot; &#x2F;&gt;
&lt;p&gt;Apparently, there are three different toolkits, used for different scenarios and development styles. 
Some features are only available in one of them, some things are easier to do in another, and one of them is generally the preferred choice because it will replace the other two someday.
Chances are you’ll be using all three of them at some time, multiplying the cognitive overhead as you switch between paradigms, file formats and APIs.&lt;&#x2F;p&gt;
&lt;p&gt;A similar, but more frustrating situation is the state of VR support in Unity. There’s a legacy system and a new “plugin-based” system. The legacy system is deprecated, but the new system doesn’t support SteamVR – in my case forcing me to use the legacy system, the &lt;a href=&quot;https:&#x2F;&#x2F;docs.unity3d.com&#x2F;540&#x2F;Documentation&#x2F;Manual&#x2F;VRReference.html&quot;&gt;only reference to which I found in the Unity 5.4 docs&lt;&#x2F;a&gt;. &lt;&#x2F;p&gt;
&lt;p&gt;This seems to be a pattern with Unity development: after discovering a component has problems, a new one is written (or &lt;a href=&quot;https:&#x2F;&#x2F;docs.unity3d.com&#x2F;Manual&#x2F;com.unity.textmeshpro.html&quot;&gt;bought&lt;&#x2F;a&gt;). 
But instead of getting replaced, the old component stays alive like a half-supported, half-documented zombie providing features the new component doesn’t get because the old one is still there.&lt;&#x2F;p&gt;
&lt;p&gt;For me, this created an incredible amount of uncertainty while developing: &lt;em&gt;Am I using the right tool for this? Is this bug caused by me or by this unmaintained legacy system? How do I know if I used this right when the docs where last updated in 2015?&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;When even a tutorial for the new VR system &lt;a href=&quot;https:&#x2F;&#x2F;docs.unity3d.com&#x2F;Manual&#x2F;configuring-project-for-xr.html&quot;&gt;tells you to use a Prefab from a folder named &lt;code&gt;XR Legacy Input Helpers&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, you can’t help but feel like you’re building on top of a tower of tech debt that can topple without warning. This confusion is a hurdle for people coming to Unity, and the complexity of having multiple competing solutions also complicates the workflows of seasoned Unity developers, even if they might not notice it a lot.&lt;&#x2F;p&gt;
&lt;p&gt;I’ve seen the struggle with balancing new features and cleaning up legacy code in the teams I’ve worked with, and I sympathize with the Unity devs when thinking about the pressure that’s on them to ship fancy new features. However, It feels like the legacy systems are &lt;a href=&quot;https:&#x2F;&#x2F;forum.unity.com&#x2F;threads&#x2F;are-you-still-using-legacy-animation-system.289367&#x2F;&quot;&gt;piling up&lt;&#x2F;a&gt;, damaging developer productivity and adoption of Unity in the long run.&lt;&#x2F;p&gt;
&lt;p&gt;The obvious solution to this is to prioritize refactoring tasks on the roadmap – but I think there’s also a lesson to learn regarding software architecture. 
This issue highlights some of &lt;a href=&quot;https:&#x2F;&#x2F;www.joelonsoftware.com&#x2F;2000&#x2F;04&#x2F;06&#x2F;things-you-should-never-do-part-i&#x2F;&quot;&gt;the dangers of rewriting code from scratch&lt;&#x2F;a&gt;, and it’s useful to keep this situation in your head when deciding whether to deprecate a piece of software or to refactor it and add backwards-compatible features. &lt;&#x2F;p&gt;
&lt;p&gt;And if you decide to do the rewrite, pay attention to:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;documentation-communication&quot;&gt;Documentation &amp;amp; Communication&lt;&#x2F;h2&gt;
&lt;p&gt;To find out what’s up with these legacy systems, you often have to dig through old blog posts in the Unity archive, just to understand the context the different solutions were written in, plans for their future and their capabilities. Guidance, context and overview regarding Unity’s capabilities is something I’m sorely missing from the official documentation.&lt;&#x2F;p&gt;
&lt;p&gt;While the Unity docs are expansive in general, they often lack in details crucial to specific problems. Missing docs for legacy parts like the old VR system are one example, &lt;a href=&quot;http:&#x2F;&#x2F;answers.unity.com&#x2F;answers&#x2F;188809&#x2F;view.html&quot;&gt;basic rules regarding colliders&lt;&#x2F;a&gt; are another. &lt;&#x2F;p&gt;
&lt;p&gt;These are &lt;a href=&quot;https:&#x2F;&#x2F;forum.unity.com&#x2F;threads&#x2F;xr-plugins-and-subsystems.693463&#x2F;&quot;&gt;sometimes addressed by Unity employees in the forums&lt;&#x2F;a&gt;. Some are buried in in hours of videos on &lt;a href=&quot;https:&#x2F;&#x2F;learn.unity.com&quot;&gt;unity learn&lt;&#x2F;a&gt;. The community provides the rest of information on &lt;a href=&quot;https:&#x2F;&#x2F;answers.unity.com&quot;&gt;answers.unity.com&lt;&#x2F;a&gt;, Reddit, blog posts and YouTube videos. 
Trying to understand a specific behavior of Unity results in a journey through 30-minute videos, threads advising some form of power cycling and posts explaining basic concepts already covered in official materials, all to find a fact that could have been provided in a single sentence in the docs.&lt;&#x2F;p&gt;
&lt;p&gt;Not to say that these resources are worthless. The Unity community is awesome, friendly and helpful. I just think Unity is missing a big opportunity in not channeling all these resources into a more structured, accessible format: 
Allowing the community to submit PRs for the official docs. (Or encouraging their own devs to write docs instead of forum posts, for that matter.) Linking from the documentation to specific videos in unity learn. Creating an index of links to resources covering specific topics.&lt;&#x2F;p&gt;
&lt;p&gt;There are many specific questions and problems google can’t answer that a few sentences in the docs would clear up.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Unity is incredibly capable in many ways. Improving on the existing codebase and documentation would make it even better: more stable, beginner-friendly and faster to work with.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Automatically resolve Syncthing conflicts using a three-way merge</title>
        <published>2020-09-17T00:00:00+00:00</published>
        <updated>2020-09-17T00:00:00+00:00</updated>
        <author>
          <name>Unknown</name>
        </author>
        <link rel="alternate" href="https://www.rafa.ee/articles/resolve-syncthing-conflicts-using-three-way-merge/" type="text/html"/>
        <id>https://www.rafa.ee/articles/resolve-syncthing-conflicts-using-three-way-merge/</id>
        
        <content type="html">&lt;p&gt;I’m trying to move away from applications and services that store data using 
proprietary formats or servers that are not mine. I’m relying more and more on syncthing to do that,
and today I would like to show you a simple trick that I think opens up a whole range of 
possibilities for syncing app data.&lt;&#x2F;p&gt;
&lt;aside&gt;
    &lt;small&gt;Side note&lt;&#x2F;small&gt;&lt;br &#x2F;&gt;
    Check out my
    &lt;a href=&quot;https:&#x2F;&#x2F;raffomania.itch.io&#x2F;knakk&quot;
        &gt;free and open source game knakk&lt;&#x2F;a
    &gt;!
&lt;&#x2F;aside&gt;
&lt;p&gt;For files that don’t change often, syncthing is already quite nice, but for other files like 
todo lists, notes etc. that get changed on multiple devices before being synced,
syncthing doesn’t know how to combine the changes and creates a 
&lt;a href=&quot;https:&#x2F;&#x2F;docs.syncthing.net&#x2F;users&#x2F;faq.html#what-if-there-is-a-conflict&quot;&gt;conflict file&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;As a developer being used to git’s convenient merging, this bugged me a lot. 
I tend to overlook these files, and resolving them using &lt;code&gt;vimdiff&lt;&#x2F;code&gt; or &lt;code&gt;meld&lt;&#x2F;code&gt; is time consuming.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;git-merge-file&quot;&gt;git merge-file&lt;&#x2F;h2&gt;
&lt;p&gt;Enter &lt;code&gt;git merge-file&lt;&#x2F;code&gt;. It’s bundled with git, but can run completely independent 
of any git repository. It automatically merges two versions of a file with little
to no conflicts. Exquisite!&lt;&#x2F;p&gt;
&lt;p&gt;To work its magic, &lt;code&gt;merge-file&lt;&#x2F;code&gt; performs a “three-way merge”, where it looks at
three files - the two conflicting edited versions, and their common ancestor.
The common ancestor is the version that both conflicting versions are based on.
Using this, git can figure out whether lines missing in one of the versions were
actually added or removed - with only two files, this would be impossible (&lt;a href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;4129049&#x2F;why-is-a-3-way-merge-advantageous-over-a-2-way-merge&quot;&gt;See this stackoverflow post for a more thorough explanation&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;“But where do we get the common ancestor from?”, you might ask. 
This is where syncthing’s awesome versioning feature
comes into play. As it turns out, syncthing can automatically keep an older copy of files
when they are modified. You can go all in with this and keep multiple older versions
depending on different criteria, but I simply use “Trash Can” versioning which
just keeps the most recent “backup” of a file. We can then use this backup to perform
a three-way merge using &lt;code&gt;merge-file&lt;&#x2F;code&gt;!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-actually-do-this&quot;&gt;How to actually do this&lt;&#x2F;h2&gt;
&lt;p&gt;Assuming we have a simple syncthing folder containing a file called &lt;code&gt;notes.md&lt;&#x2F;code&gt; with the following content:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;md&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-md &quot;&gt;&lt;code class=&quot;language-md&quot; data-lang=&quot;md&quot;&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;- &lt;&#x2F;span&gt;&lt;span&gt;I like markdown
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On one node, I add a line at the start:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;md&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-md &quot;&gt;&lt;code class=&quot;language-md&quot; data-lang=&quot;md&quot;&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;- &lt;&#x2F;span&gt;&lt;span&gt;I like syncthing
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; I like markdown
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On the other node, I add a line at the end:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;md&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-md &quot;&gt;&lt;code class=&quot;language-md&quot; data-lang=&quot;md&quot;&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;- &lt;&#x2F;span&gt;&lt;span&gt;I like markdown
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; I also like git
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Once syncthing detects the conflict, it will create a &lt;code&gt;.sync-conflict&lt;&#x2F;code&gt; file, resulting
in this directory layout:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#2e3440;color:#d8dee9;&quot;&gt;&lt;code&gt;&lt;span&gt;├── notes.md
&lt;&#x2F;span&gt;&lt;span&gt;├── notes.sync-conflict-20200917-224540-5CYLJKE.md
&lt;&#x2F;span&gt;&lt;span&gt;└── .stversions
&lt;&#x2F;span&gt;&lt;span&gt;    └── notes~20200917-173218.md
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can now run &lt;code&gt;git merge-file&lt;&#x2F;code&gt;, providing version one, the common ancestor and then version two:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span&gt; merge-file notes.md .stversions&#x2F;notes&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;~&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;xxxxx&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;.md notes.sync-conflict-&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;xxxxx&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;.md 
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;after which &lt;code&gt;notes.md&lt;&#x2F;code&gt; contains the following:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;md&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-md &quot;&gt;&lt;code class=&quot;language-md&quot; data-lang=&quot;md&quot;&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;- &lt;&#x2F;span&gt;&lt;span&gt;I like syncthing
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; I like markdown
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; I like also git
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Alright! Git merged the three files without us doing anything!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;caveats&quot;&gt;Caveats&lt;&#x2F;h2&gt;
&lt;p&gt;Since syncthing only creates backups in &lt;code&gt;.stversions&lt;&#x2F;code&gt; when receiving changes from a remote
machine and not when the file is modified locally, you might end up in a situation 
without a common ancestor in &lt;code&gt;.stversions&lt;&#x2F;code&gt;. 
In this case, you can simply use an empty file as the common ancestor:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; style=&quot;background-color:#2e3440;color:#d8dee9;&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;touch&lt;&#x2F;span&gt;&lt;span&gt; empty.md
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#88c0d0;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span&gt; merge-file notes.md empty.md notes.sync-conflict-&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;xxxxx&lt;&#x2F;span&gt;&lt;span style=&quot;color:#81a1c1;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;.md
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This might result in more conflicts than usual, but in general git is pretty good 
at figuring out common lines between differing versions. &lt;&#x2F;p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;&#x2F;h2&gt;
&lt;p&gt;I’ve only been running this by hand since I didn’t have too many conflicts, but you could easily put this in a script that periodically scrubs your folder. This way, most merges run automatically, and if any lines actually conflict, you see them when editing the conflicting file the next time - way easier than manually looking for &lt;code&gt;.sync-conflict&lt;&#x2F;code&gt; files all the time.&lt;&#x2F;p&gt;
&lt;p&gt;I think this method has lots of potential to enable more use cases for syncthing.
For example, if you keep your playlists as &lt;code&gt;.m3u&lt;&#x2F;code&gt; files in a syncthing folder,
you might use &lt;code&gt;git merge-file --union&lt;&#x2F;code&gt; to tell git
to just keep both versions in case of conflicting lines. This way, at the cost of
some deleted songs re-appearing, you would never have a single conflict!&lt;&#x2F;p&gt;
&lt;p&gt;This is why I love storing data in plain text files: I now have convenient, robust,
offline-capable note synchronization without needing any server (be that from a third party or maintained on my own), all while being free to choose and switch between a wide range
of note-taking software. For me, storing more and more of my personal data like this is the logical path forward.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
