<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eana Hufwe</title>
    <description>The latest articles on DEV Community by Eana Hufwe (@blueset).</description>
    <link>https://dev.to/blueset</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F243878%2Fd8d3266e-0982-40b5-9b6c-828ddb896316.png</url>
      <title>DEV Community: Eana Hufwe</title>
      <link>https://dev.to/blueset</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blueset"/>
    <language>en</language>
    <item>
      <title>Rotated Background Patterns in CSS with SVG</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Thu, 06 Mar 2025 21:18:49 +0000</pubDate>
      <link>https://dev.to/blueset/rotated-background-patterns-in-css-with-svg-3b8j</link>
      <guid>https://dev.to/blueset/rotated-background-patterns-in-css-with-svg-3b8j</guid>
      <description>&lt;p&gt;While working on the WordPress theme &lt;a href="https://1a23.com/works/design/titsyul-amip-twenty-twenty-five/" rel="noopener noreferrer"&gt;Tìtsyul Amip Twenty Twenty-Five&lt;/a&gt;, a need arose for a more flexible way to implement rotated background patterns in CSS. Existing methods often involve trade-offs, and this exploration led to a technique leveraging the power of SVG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing Approaches and Their Limitations
&lt;/h2&gt;

&lt;p&gt;Traditionally, achieving rotated background images in CSS has involved several workarounds, each with its own set of challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS Transforms:&lt;/strong&gt; Using the &lt;code&gt;transform: rotate()&lt;/code&gt; property. This rotates the entire element, including its content, which is often not the desired outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pseudo-elements:&lt;/strong&gt; Employing &lt;code&gt;::before&lt;/code&gt; or &lt;code&gt;::after&lt;/code&gt; pseudo-elements to create a rotated background. While this allows rotating the background independently of the content, it can be complex to manage sizing and positioning. Additionally, if the &lt;code&gt;html&lt;/code&gt; or &lt;code&gt;body&lt;/code&gt; elements have a background color, pseudo-elements with a negative &lt;code&gt;z-index&lt;/code&gt; may fall behind them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-rotated Images:&lt;/strong&gt; Rotating the image using image editing software and then using the rotated image as the background. This is impractical for seamless patterns in most rotation angles or when dynamic canvas size is required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/blueset/embed/azbWGPq?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The SVG Pattern Technique
&lt;/h2&gt;

&lt;p&gt;Building on these insights, let’s now explore a method that leverages SVG to achieve rotated background patterns—a solution that delivers both flexibility and precise control.&lt;/p&gt;

&lt;p&gt;At the core of this approach is the SVG &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; feature, a versatile tool that enables the creation of seamless, repeating background patterns. The &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; element defines a tileable area that can be used as a fill for other SVG shapes, allowing for intricate and scalable designs. By leveraging attributes such as &lt;code&gt;patternTransform&lt;/code&gt;, you can easily manipulate these patterns with transformations like rotation, scaling, and translation, making it an ideal choice for dynamic and responsive backgrounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draw the Pattern Using SVG&lt;/strong&gt; ⸻ Begin by creating the desired pattern using SVG elements. This can include vector graphics or embedded bitmaps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;polygon&lt;/span&gt; &lt;span class="na"&gt;points=&lt;/span&gt;&lt;span class="s"&gt;"0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wrap in a &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; Definition&lt;/strong&gt; ⸻ Encapsulate the pattern within a &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; element inside the &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt; section of the SVG, specify the dimensions for the pattern, and use the &lt;code&gt;patternTransform&lt;/code&gt; attribute to apply transformations, such as rotation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;pattern&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"star"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;patternUnits=&lt;/span&gt;&lt;span class="s"&gt;"userSpaceOnUse"&lt;/span&gt; &lt;span class="na"&gt;patternTransform=&lt;/span&gt;&lt;span class="s"&gt;"rotate(20)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;polygon&lt;/span&gt; &lt;span class="na"&gt;points=&lt;/span&gt;&lt;span class="s"&gt;"0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/pattern&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create the Main SVG&lt;/strong&gt; ⸻ Create a new SVG element without a &lt;code&gt;viewBox&lt;/code&gt; attribute. Set the &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; to &lt;code&gt;100%&lt;/code&gt; to ensure it covers the entire area of the element. Then add a &lt;code&gt;&amp;lt;rect&amp;gt;&lt;/code&gt; element to cover the entire SVG area, and set its &lt;code&gt;fill&lt;/code&gt; attribute to reference the pattern defined in step 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;pattern&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"star"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;patternUnits=&lt;/span&gt;&lt;span class="s"&gt;"userSpaceOnUse"&lt;/span&gt; &lt;span class="na"&gt;patternTransform=&lt;/span&gt;&lt;span class="s"&gt;"rotate(20)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;polygon&lt;/span&gt; &lt;span class="na"&gt;points=&lt;/span&gt;&lt;span class="s"&gt;"0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/pattern&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#star)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Encode and Use in CSS&lt;/strong&gt; ⸻ Encode the SVG as a data URL. Use the data URL as the &lt;code&gt;background-image&lt;/code&gt; in your CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Cdefs%3E%3Cpattern id='star' width='10' height='10' patternUnits='userSpaceOnUse' patternTransform='rotate(20)&lt;/span&gt;&lt;span class="s2"&gt;'%3E%3Cpolygon points='&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="s2"&gt;' /%3E%3C/pattern%3E%3C/defs%3E%3Crect width='&lt;/span&gt;&lt;span class="m"&gt;100%25&lt;/span&gt;&lt;span class="s2"&gt;' height='&lt;/span&gt;&lt;span class="m"&gt;100%25&lt;/span&gt;&lt;span class="s2"&gt;' fill='&lt;/span&gt;&lt;span class="sx"&gt;url(%23star)&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;svg&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this technique, the pattern background can be transformed on its own, while still behave like any other CSS background to enjoy other features like background stacking and blend mode settings.&lt;/p&gt;

&lt;p&gt;This is an example of how a background using this technique could look like.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/blueset/embed/ogNejXK?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  SVG Rotated Background Generator
&lt;/h3&gt;

&lt;p&gt;You can also try it out using the generator below to create custom rotated background patterns with SVG. You can also adjust parameters such as pattern size, scaling, and rotation angle.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/blueset/embed/JojJKog?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2025/03/06/rotated-background-patterns-in-css-with-svg/" rel="noopener noreferrer"&gt;Rotated Background Patterns in CSS with SVG&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com" rel="noopener noreferrer"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>css</category>
      <category>svg</category>
      <category>frontend</category>
      <category>web</category>
    </item>
    <item>
      <title>Shift-JIS / UTF-8 文字化け解読：実はもうちょっと読めるかも</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Mon, 01 Apr 2024 04:49:43 +0000</pubDate>
      <link>https://dev.to/blueset/shift-jis-utf-8-wen-zi-hua-kejie-du-shi-hamoutiyotutodu-merukamo-746</link>
      <guid>https://dev.to/blueset/shift-jis-utf-8-wen-zi-hua-kejie-du-shi-hamoutiyotutodu-merukamo-746</guid>
      <description>&lt;p&gt;文字化け、それは一昔前のパソコンにおいて情報交換でよく起こる不具合である。近年になってからは Unicode（だいたい UTF-8）がネット上の文字コードの事実上の標準になっており、意図しないで発生する文字化けはほとんどみられなくなった。今では、よくみられる文字化けはほぼ創作でホラー要素、謎解き、あるいは隠しメッセージとして登場している。その中に、最もよく使われている文字化けの種類は「Shift-JiS / UTF-8」による文字化けで、いわゆる「繝繧」とか糸へんの漢字がいっぱい入ってるのやつ。この記事は、「Shift-JiS / UTF-8」による文字化けを既存ツールよりはもうちょっと解読できる方法を解説します。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dev.to に一部内容が表示できないため、&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" rel="noopener noreferrer"&gt;原文&lt;/a&gt;を参照してください。&lt;br&gt;
&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;原文&lt;/a&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  どれくらい解読できる？
&lt;/h2&gt;

&lt;p&gt;まず実際の解読例を見てみましょう。&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/blueset/embed/wvZopgd?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;元の文章&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;文字化けした日本語のテキストの例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;化けた文字列&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;譁�蟄怜喧縺代＠縺滓律譛ｬ隱槭�ｮ繝�繧ｭ繧ｹ繝医�ｮ萓�&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;既存のツールで解読&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;�字化けした日本語���キスト���&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;今回のツールで解読 &lt;br&gt; (JIS 第一水準漢字限定)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(文斉斌斎斐斑斗料斜)字化けした日本語(の)(ダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミム)キスト(の)(侃例侍供依侠)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;この例の通り、既存のツールで解読した「?」の部分に入りそうな文字の候補を提示してくれるのが今回のツールです。では、いったいどうやって候補を検出できるのかを解説していこう。&lt;/p&gt;

&lt;h2&gt;
  
  
  文字コードの前提知識
&lt;/h2&gt;

&lt;p&gt;細かい仕組みに踏み入る前に、まずは今回扱う2種類の文字コード、Shift-JIS と UTF-8 の構造を少し見てみよう。&lt;/p&gt;

&lt;p&gt;Shift-JIS は相対的に単純で、一文字は１バイトもしくは２バイトで構成して、それぞれで使える値の範囲が限られている。次の表の通り、１ビットは必ず &lt;code&gt;0x00–0x1F&lt;/code&gt; &lt;code&gt;0x20–0x27&lt;/code&gt; &lt;code&gt;0xA1–0xDF&lt;/code&gt; のどれかで、２バイトの場合は、１バイト目は &lt;code&gt;0x81–0x9F&lt;/code&gt; &lt;code&gt;0xE0–0xFC&lt;/code&gt; で、２バイト目は &lt;code&gt;0x40–0x9E&lt;/code&gt; &lt;code&gt;0x9F–0xFC&lt;/code&gt; に入ることになる。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dev.to に色づきの表は挿入できないため、Wikipedia にて &lt;a href="https://ja.wikipedia.org/wiki/Template:Shift_JIS%E6%8B%A1%E5%BC%B5%E3%81%AE%E7%AC%A6%E5%8F%B7%E8%A1%A8" rel="noopener noreferrer"&gt;Shift-JIS 拡張の符号表&lt;/a&gt;を参照してください。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;対して、UTF-8 が対応する範囲がかなり広いので、符号化の体系もより複雑になっている。U+0000 から U+10FFFF までの約 111 万の符号位置を表す必要があるため、UTF-8 が使用するバイト幅が１バイトから４バイトまでになる。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;セル結合した表は Dev.to に表示できないため、&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" rel="noopener noreferrer"&gt;原文&lt;/a&gt;を参照してください。&lt;br&gt;
&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;原文&lt;/a&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;パターン的に、１バイトから４バイトの構成ではそれそれ先頭のビットは確定されている。&lt;/p&gt;

&lt;h2&gt;
  
  
  実際に解読する
&lt;/h2&gt;

&lt;p&gt;Shift-JIS でエンコードされた文字列を UTF-8 でデコードすると、対応する文字が見つからない場合があり、それが文字化けの原因となる。先の例文を使って実際エンコードするとこうなります。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;セル結合した表は Dev.to に表示できないため、&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" rel="noopener noreferrer"&gt;原文&lt;/a&gt;を参照してください。&lt;br&gt;
&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;原文&lt;/a&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;上の表の�の部分は、データを Shift-JIS として解読する時にデータに対応する文字が見つからないため、仮に置かれるプレースホルダーである。元データと照らし合わせると、「テ」「の」「例」に対応するデータの一部が復元できないことがわかる。このため、一般の文字化け解読ツールは「�キスト��」までしか解読できない。&lt;/p&gt;

&lt;p&gt;ただし、この場合で失ったデータは１文字ごとに１バイトだけ、残った部分はまだ解読のヒントになれるはず。いくつかの仮定を立てて、この表をより細かく分けてみよう。&lt;/p&gt;

&lt;h3&gt;
  
  
  仮定その１：元データは有効な UTF-8 データ
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;セル結合した表は Dev.to に表示できないため、&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" rel="noopener noreferrer"&gt;原文&lt;/a&gt;を参照してください。&lt;br&gt;
&lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;原文&lt;/a&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;タイトルに書いた通り、当然元データは UTF-8 でなかったり、改変したりすると、そもそも大前提が成り立たない。この仮定のもとに、復元できない部分を UTF-8 の先頭ビット規則に当てはまると、実際に失われたデータはこの場合 8 ビットずつではなく 6 ビットずつになる。&lt;/p&gt;

&lt;p&gt;1 ビット、つまり二進数の一桁は 0 か 1 の二通りで構成するから、6 ビットの組み合わせは 2&lt;sup&gt;6&lt;/sup&gt; = 64 通りになる。これで無限にある可能性から一文字分 64 個の候補に絞られます。&lt;/p&gt;

&lt;p&gt;この仮定で絞られた候補はこちら：&lt;/p&gt;

&lt;p&gt;一文字目・二進数の &lt;code&gt;0011000011 ______&lt;/code&gt; に当てはまる Unicode 文字：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;二文字目・二進数の &lt;code&gt;0011 ______ 101110&lt;/code&gt; に当てはまる Unicode 文字：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;〮のギヮㄮㅮㆮ㇮㈮㉮㊮㋮㌮㍮㎮㏮㐮㑮㒮㓮㔮㕮㖮㗮㘮㙮㚮㛮㜮㝮㞮㟮㠮㡮㢮㣮㤮㥮㦮㧮㨮㩮㪮㫮㬮㭮㮮㯮㰮㱮㲮㳮㴮㵮㶮㷮㸮㹮㺮㻮㼮㽮㾮㿮
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;三文字目・二進数の &lt;code&gt;0100111110 ______&lt;/code&gt; に当てはまる Unicode 文字：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;侀侁侂侃侄侅來侇侈侉侊例侌侍侎侏侐侑侒侓侔侕侖侗侘侙侚供侜依侞侟侠価侢侣侤侥侦侧侨侩侪侫侬侭侮侯侰侱侲侳侴侵侶侷侸侹侺侻侼侽侾便
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;つまり、今のところの解読予想はこうである&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ&lt;/th&gt;
&lt;th&gt;キ&lt;/th&gt;
&lt;th&gt;ス&lt;/th&gt;
&lt;th&gt;ト&lt;/th&gt;
&lt;th&gt;〮のギヮㄮㅮㆮ㇮㈮㉮㊮㋮㌮㍮㎮㏮㐮㑮㒮㓮㔮㕮㖮㗮㘮㙮㚮㛮㜮㝮㞮㟮㠮㡮㢮㣮㤮㥮㦮㧮㨮㩮㪮㫮㬮㭮㮮㯮㰮㱮㲮㳮㴮㵮㶮㷮㸮㹮㺮㻮㼮㽮㾮㿮&lt;/th&gt;
&lt;th&gt;侀侁侂侃侄侅來侇侈侉侊例侌侍侎侏侐侑侒侓侔侕侖侗侘侙侚供侜依侞侟侠価侢侣侤侥侦侧侨侩侪侫侬侭侮侯侰侱侲侳侴侵侶侷侸侹侺侻侼侽侾便&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  仮定その２：元の文字列は日本語である
&lt;/h3&gt;

&lt;p&gt;さっきの仮定で 64 個の候補に絞られたが、並べるとまだ多すぎる気がする。ここで2つ目の仮定の登場です。原理的に UTF-8 でエンコードされたデータが Shift-JIS でデコードするだけだから、どんな言語でもできるが、「そもそも日本語じゃないと Shift-JIS で化ける必要なくない」と思っちゃうから、ここで原文が日本語であることを仮定する。&lt;/p&gt;

&lt;p&gt;上のリストに「ㅮ」や「侦」など日本語じゃほぼ絶対使わない文字が散見するが、文字単位で十分な言語判定はできないから、ここで大まかに Shift-JIS に符号化できる文字のみを「日本語」とします。&lt;/p&gt;

&lt;p&gt;この条件で絞られた候補は以下のとおりである。かなり少なくなったね。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶ・ーヽヾ&lt;/th&gt;
&lt;th&gt;キ&lt;/th&gt;
&lt;th&gt;ス&lt;/th&gt;
&lt;th&gt;ト&lt;/th&gt;
&lt;th&gt;のギヮ&lt;/th&gt;
&lt;th&gt;侃來侈例侍侏侑侖侘供依侠価侫侭侮侯侵侶便&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  仮定その３：化け戻すとデータはロストしなければならない
&lt;/h3&gt;

&lt;p&gt;タイトルはちょっと回りくどいが、まず以下の例を見てみよう。一文字目の候補の「ム」と「メ」の2文字に注目して、それぞれが当てはまる場合再び文字化けをする時の結果を求めよう。&lt;/p&gt;

&lt;p&gt;「ム」の場合&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;予想の原文：ムキスト…&lt;/li&gt;
&lt;li&gt;UTF-8 でエンコード：&lt;code&gt;E3 83 A0 E3 82 AD&lt;/code&gt; …&lt;/li&gt;
&lt;li&gt;Shift-JIS でデコード：繝�繧ｭ…&lt;/li&gt;
&lt;li&gt;解読したい文字列：繝�繧ｭ…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;「メ」の場合&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;予想の原文：メキスト…&lt;/li&gt;
&lt;li&gt;UTF-8 でエンコード：&lt;code&gt;E3 83 A1 E3 82 AD&lt;/code&gt; …&lt;/li&gt;
&lt;li&gt;Shift-JIS でデコード：繝｡繧ｭ…&lt;/li&gt;
&lt;li&gt;解読したい文字列：繝�繧ｭ…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一文字目に「ム」を代入して予想の原文を化け直すと最初から解読したい文字列に一致する結果が得られるが、「メ」の場合は化け直す文字列に�が含まれていない。つまり、背理法 （?）によって「メ」は一文字目ではないことが証明できる。この方法で全ての候補を化け直してチェックすると、候補をさらに絞られます。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミム&lt;/th&gt;
&lt;th&gt;キ&lt;/th&gt;
&lt;th&gt;ス&lt;/th&gt;
&lt;th&gt;ト&lt;/th&gt;
&lt;th&gt;の&lt;/th&gt;
&lt;th&gt;侃來侈侊例侍侏侑侒侔侖侘侚供依侠&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;これで５文字目が「の」であることを確定できました。&lt;/p&gt;

&lt;h3&gt;
  
  
  仮定その４：難しい漢字は使わない（任意）
&lt;/h3&gt;

&lt;p&gt;さっきの候補はすでにかなり絞られたが、中にはまだ「侈」や「侊」など普通じゃ読めない漢字がかなり残っている。解読したい文字列の出自にもよるが、難読漢字は出ない場合は多いだろうと仮定する。ここは上の候補の中にある漢字を &lt;a href="https://ja.wikipedia.org/wiki/JIS_X_0208#%E6%B0%B4%E6%BA%96%E5%88%86%E3%81%91" rel="noopener noreferrer"&gt;JIS 第一水準漢字&lt;/a&gt;に絞ると、こうしてさらに候補を減らせる。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミム&lt;/th&gt;
&lt;th&gt;キ&lt;/th&gt;
&lt;th&gt;ス&lt;/th&gt;
&lt;th&gt;ト&lt;/th&gt;
&lt;th&gt;の&lt;/th&gt;
&lt;th&gt;侃例侍供依侠&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;当然、自信があれば&lt;a href="https://ja.wikipedia.org/wiki/%E5%B8%B8%E7%94%A8%E6%BC%A2%E5%AD%97" rel="noopener noreferrer"&gt;常用漢字&lt;/a&gt;や&lt;a href="https://ja.wikipedia.org/wiki/%E6%95%99%E8%82%B2%E6%BC%A2%E5%AD%97" rel="noopener noreferrer"&gt;教育漢字&lt;/a&gt;などの文字集合に絞るのもありです。&lt;/p&gt;

&lt;h2&gt;
  
  
  補足：１バイトと２バイト⸺デコーダ仕様の違い
&lt;/h2&gt;

&lt;p&gt;最後に補足したいのは、文字化けはツール（デコーダ）の仕様によって結果はかならず同じではないこと。私はいろんな文字化けツールを試している中、一つ面白いことが気付いた。それは、ツールによっては同じ文章でも、文字化けにすると違う結果が出て、それぞれデコードできないデータの処理方法が違うことである。ここでデコーダをその仕様で「１バイト類」と「２バイト類」に分ける。&lt;/p&gt;

&lt;p&gt;「１バイト類」のデコーダは解読できないデータを見つけたら、１バイト飛ばして次から解読するものである。例えば、「&lt;code&gt;E3 83 86 E3 82 AD&lt;/code&gt;」をデコードするときはこういう風になる：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;E3&lt;/code&gt; ← 2バイト文字の第1バイト、次のバイトを見る&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;E3 83&lt;/code&gt; ← 繝&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝 86&lt;/code&gt; ← 2バイト文字の第1バイト、次のバイトを見る&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝 86 E3&lt;/code&gt; ← 対応する文字がない、次のバイトから再開&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝� E3&lt;/code&gt; ← 2バイト文字の第1バイト、次のバイトを見る&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝� E3 82&lt;/code&gt; ← 繧&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝�繧 AD&lt;/code&gt; ← ｭ&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝�繧ｭ&lt;/code&gt; ← 結果&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;対して、「２バイト類」のデコーダは解読できないデータを見つけたら、解読できない部分をまとめて飛ばすものである。例えば、同じく「&lt;code&gt;E3 83 86 E3 82 AD&lt;/code&gt;」をデコードするときはこういう風になる：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;E3&lt;/code&gt; ← 2バイト文字の第1バイト、次のバイトを見る&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;E3 83&lt;/code&gt; ← 繝&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝 86&lt;/code&gt; ← 2バイト文字の第1バイト、次のバイトを見る&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝 86 E3&lt;/code&gt; ← 対応する文字がない、まとめて飛ばす&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝� 82&lt;/code&gt; ← 2バイト文字の第1バイト、次のバイトを見る&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝� 82 AD&lt;/code&gt; ← く&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;繝�く&lt;/code&gt; ← 結果&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;このため、デコーダによっては、�が１バイトを意味する時があるし、２バイトを意味する時もある。冒頭にあるツールで「?」は１バイトを意味するため、�を１バイトや２バイトに変換するボタンを用意した。片方が解読できなかったら、もう一方も試してみよう。&lt;/p&gt;

&lt;p&gt;これからさらに絞りたいなら、たぶん文脈で頑張るしかないかなと思う。この方法で、最初の無限にある選択肢からかなり限られたオプションに絞られて、文脈で当てる難易度も結構下げたと思う。&lt;/p&gt;

&lt;p&gt;もしこの先にまたどこかで文字化けを解読したい時があったら、この記事・ツールが役に立つことができたらとても嬉しいです。ではまた。&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2024/04/01/shift-jis-utf-8-mojibake-kaidoku-jitsu-wa-mou-chotto-yomeru-kamo/" rel="noopener noreferrer"&gt;Shift-JIS / UTF-8 文字化け解読：実はもうちょっと読めるかも&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com" rel="noopener noreferrer"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>shiftjis</category>
      <category>unicode</category>
      <category>mojibake</category>
    </item>
    <item>
      <title>Reverse engineering an IL2CPP NSO binary: Case study of Mojipittan Encore</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Mon, 16 Jan 2023 23:29:35 +0000</pubDate>
      <link>https://dev.to/blueset/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore-161p</link>
      <guid>https://dev.to/blueset/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore-161p</guid>
      <description>&lt;p&gt;This is yet another random side project I was working on recently, and my first attempt to reverse engineer a real world application compiled into binary. In this article, I want to talk about how I reversed engineered an Unity IL2CPP binary compiled to NSO, in a step-by-step fashion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://blog.1a23.com/2023/01/13/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/#forewords"&gt;Forewords&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.1a23.com/2023/01/13/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/#preparation"&gt;Preparation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.1a23.com/2023/01/13/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/#extract-contents"&gt;Extract contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.1a23.com/2023/01/13/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/#unpack-resource-files"&gt;Unpack resource files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.1a23.com/2023/01/13/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/#preparing-for-decompilation"&gt;Preparing for decompilation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.1a23.com/2023/01/13/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/#decompile-the-binary"&gt;Decompile the binary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.1a23.com/2023/01/13/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/#debugging-with-emulator"&gt;Debugging with emulator&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Forewords
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Kotoba_no_Puzzle:_Mojipittan"&gt;Kotoba no Puzzle: Mojipittan&lt;/a&gt;&lt;/em&gt; is a word puzzle game series in Japanese, where the player makes words from letter pieces on a board. It sort of like Scrabble, but not exactly the same. I always wanted to give it a try, but it was quite an old game, available only on GBA, PSP, DS, and Wii. That’s until they made an Encore version on Nintendo Switch. This game is also one of the reasons I bought a Switch.&lt;/p&gt;

&lt;p&gt;Ever since I bought the game, I was wondering if there is a way to solve the game optimally. The first step to approach this is to first get the dictionary of the game. Despite the game has now released to multiple different platforms, there has no resource online on the word list used in the game. I thus decided to do it myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparation
&lt;/h2&gt;

&lt;p&gt;To follow this article, you would need &lt;a href="https://damota.me/ssnc/checker/"&gt;an exploitable Nintendo Switch&lt;/a&gt; with a purchased copy of &lt;em&gt;&lt;a href="https://store-jp.nintendo.com/list/software/70010000022654.html"&gt;Mojipittan Encore&lt;/a&gt;&lt;/em&gt; and a PC running Windows. Although there are &lt;a href="https://www.reddit.com/r/SwitchPirates/"&gt;other ways&lt;/a&gt; to do it with only a PC, I do not recommended shem for reasons. You might be able to also use other OSes, but a lot of the resources has precompiled binary for Windows, which involves less effort.&lt;/p&gt;

&lt;p&gt;To get the game data from the device, you need to dump it from the console. Yuzu has provided &lt;a href="https://yuzu-emu.org/help/quickstart/"&gt;a detailed guide on how to dump games&lt;/a&gt; and corresponding keys. I will use an XCI dump here as an example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract contents
&lt;/h2&gt;

&lt;p&gt;To extract the dumped XCI file, I used &lt;a href="https://github.com/SciresM/hactool"&gt;hactool&lt;/a&gt;, specifically this wrapped version &lt;a href="https://gbatemp.net/threads/how-to-easy-extract-game-files-from-nsp-xci.534724/"&gt;Unpackv2&lt;/a&gt;. &lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/SciresM"&gt;
        SciresM
      &lt;/a&gt; / &lt;a href="https://github.com/SciresM/hactool"&gt;
        hactool
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      hactool is a tool to view information about, decrypt, and extract common file formats for the Nintendo Switch, especially Nintendo Content Archives.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;hactool&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1ac32286ab717c2698273505318bf4c0de18836e2504f39c614109566ce4a9f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4953432d626c75652e737667"&gt;&lt;img src="https://camo.githubusercontent.com/1ac32286ab717c2698273505318bf4c0de18836e2504f39c614109566ce4a9f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4953432d626c75652e737667" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;hactool is a tool to view information about, decrypt, and extract common file formats for the Nintendo Switch, especially Nintendo Content Archives.&lt;/p&gt;
&lt;p&gt;It is heavily inspired by &lt;a href="https://github.com/profi200/Project_CTR/tree/master/ctrtool"&gt;ctrtool&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;Usage: hactool [options...] &amp;lt;file&amp;amp;gt
Options:
-i, --info        Show file info.
                      This is the default action.
-x, --extract     Extract data from file.
                      This is also the default action.
  -r, --raw          Keep raw data, don't unpack.
  -y, --verify       Verify hashes and signatures.
  -d, --dev          Decrypt with development keys instead of retail.
  -k, --keyset       Load keys from an external file.
  -t, --intype=type  Specify input file type [nca, xci, pfs0, romfs, hfs0, npdm, pk11, pk21, ini1, kip1, nax0, save, keygen]
  --titlekey=key     Set title key for Rights ID crypto titles.
  --contentkey=key   Set raw key for NCA body decryption.
  --disablekeywarns  Disables warning output when loading external keys.
NCA options:
  --plaintext=file   Specify file path for saving a decrypted copy of the NCA.
  --header=file      Specify Header file&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/SciresM/hactool"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Once the tool is downloaded and unzipped, follow the next steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the &lt;code&gt;prod.keys&lt;/code&gt; extracted form your device to the folder where &lt;code&gt;Unpack.cmd&lt;/code&gt; is found, and rename it to &lt;code&gt;keys.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Drop the dumped XCI file onto &lt;code&gt;Unpack.cmd&lt;/code&gt; to start the script.&lt;/li&gt;
&lt;li&gt;When prompted…
&lt;code&gt;If your patch was inside XCI, press "1" and ENTER
If you don't have a patch, just only press ENTER&lt;/code&gt;
…press Enter. This game exported did not come with a patch.&lt;/li&gt;
&lt;li&gt;Now, in the &lt;em&gt;Unpackv2&lt;/em&gt; folder, there will be a new &lt;em&gt;ExtractedXCI&lt;/em&gt; folder created with 4 NCA files of various sizes created.
When prompted…
&lt;code&gt;Drop here correct NCA patch file (probably the biggest one) from ExtractedXCI folder in&lt;/code&gt;
…, drop the 774MB &lt;em&gt;a0547397496b93fcb08f438bcaad2731.nca&lt;/em&gt; to the terminal window.&lt;/li&gt;
&lt;li&gt;Now the terminal print a list of files extracted, ending with the prompt…
&lt;code&gt;Press ENTER to delete all temporary files&lt;/code&gt;
Press Enter twice to finish the export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After finishing the export process, there will be a new folder created with the extracted content, with two subfolders: &lt;em&gt;exefs&lt;/em&gt; and &lt;em&gt;romfs&lt;/em&gt;. We will make use of these contents in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unpack resource files
&lt;/h2&gt;

&lt;p&gt;When you open the &lt;em&gt;romfs/Data&lt;/em&gt; folder, you will be welcomed with some familiar file names, like &lt;em&gt;resource.assets&lt;/em&gt;, &lt;em&gt;sharedassets0.assets&lt;/em&gt;, &lt;em&gt;level0&lt;/em&gt;, and &lt;em&gt;level1&lt;/em&gt;. Yes, if you have ever made or opened the directory of a Unity game, you will surely recognize these file names. Unity organizes their asset files in a pretty recognizable pattern, and is well studied by the community with multiple tools created.&lt;/p&gt;

&lt;p&gt;At this point, you are free to extract all static assets files found in the game, like text files and textures. The tool I used is &lt;a href="https://github.com/SeriousCache/UABE"&gt;Unity Asset Bundle Extractor&lt;/a&gt; (UABE). To extract assets, open the &lt;em&gt;.assets&lt;/em&gt; files with the tool, and export the assets using the built-in plugins to easily processable formats.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/SeriousCache"&gt;
        SeriousCache
      &lt;/a&gt; / &lt;a href="https://github.com/SeriousCache/UABE"&gt;
        UABE
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Asset Bundle Extractor
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Asset Bundle Extractor&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;.assets and AssetBundle editor.&lt;br&gt;
Not affiliated with Unity Technologies.&lt;/p&gt;
&lt;p&gt;UABE is an editor for 3.4+/4/5/2017-2021.3 .assets and AssetBundle files. It can create standalone mod installers from changes to .assets and/or bundles.&lt;/p&gt;
&lt;p&gt;There are multiple plugins to convert assets from/to common file formats :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Texture plugin can export and import .png and .tga files (Texture2D only) and decode&amp;amp;encode most texture formats used by Unity.&lt;/li&gt;
&lt;li&gt;The TextAsset plugin can export and import .txt files.&lt;/li&gt;
&lt;li&gt;The AudioClip plugin can export uncompressed .wav files from Unity 5+ AudioClip assets using FMOD, .m4a files from WebGL builds and Unity 4 sound files.&lt;/li&gt;
&lt;li&gt;The Mesh plugin can export .obj and .dae (Collada) files, also supporting rigged SkinnedMeshRenderers.&lt;/li&gt;
&lt;li&gt;The Utility plugin can export and import byte arrays and resources (StreamingInfo, StreamedResource) within the View Data editor.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Building&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;UABE can be built within Visual Studio (Community) 2022 using the Open Folder option (CMake).&lt;/p&gt;
&lt;p&gt;The…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/SeriousCache/UABE"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Besides the folder mentioned above, there is a subfolder &lt;em&gt;StreamingData/Switch/datas&lt;/em&gt;, which contains assets that are loaded after the game has initialized. Here, the files we are interested are the dictionary files under &lt;em&gt;romfs/​Data/​StreamingData/​Switch/​datas/​dictionary&lt;/em&gt;. Open it with UABE, we can see three text assets: &lt;em&gt;worddata.aid&lt;/em&gt;, &lt;em&gt;worddata.cot&lt;/em&gt;, and &lt;em&gt;worddata.dic&lt;/em&gt;. Extract them with the txt export plugin, we can get three binary files with some sort of patterns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;xxd worddata.aid | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
00000000: 5744 5000 0002 1ca1 0000 0001 0001 c1e6 WDP.............
00000010: 0000 0002 0001 ffee 0001 5eb2 0001 5eb3 ..........^...^.
00000020: 0000 0003 0001 ffef 0001 fff0 0000 0004 ................
00000030: 0000 0005 0000 0006 0001 5d48 0000 0007 ..........]H....
00000040: 0000 0008 0000 0009 0000 000a 0000 000b ................
00000050: 0001 92b4 0001 92b7 0001 92b3 0001 92b6 ................
00000060: 0001 92b5 0000 000c 0001 5a5f 0001 fff1 ..........Z_....
00000070: 0000 000d 0000 000e 0000 000f 0000 0010 ................
00000080: 0000 0011 0001 9019 0000 0012 0001 fff2 ................
00000090: 0000 0013 0001 fff3 0000 0014 0000 0015 ................
000000a0: 0000 0016 0000 0017 0000 0018 0000 0019 ................
000000b0: 0000 001a 0000 001b 0000 001c 0000 001d ................
000000c0: 0000 001e 0000 001f 0000 0020 0000 0021 ........... ...!
000000d0: 0000 0022 0000 0023 0000 0024 0000 0025 ...&lt;span class="s2"&gt;"...#...&lt;/span&gt;&lt;span class="nv"&gt;$.&lt;/span&gt;&lt;span class="s2"&gt;..%
000000e0: 0000 0026 0000 0027 0001 fff4 0000 0028 ...&amp;amp;...'.......(
000000f0: 0000 0029 0000 002a 0000 002b 0000 002c ...)...*...+...,
00000100: 0000 002d 0000 002e 0002 1191 0002 1838 ...-...........8
00000110: 0001 5f43 0000 0030 0001 fff6 0000 0031 .._C...0.......1
00000120: 0000 0032 0000 0033 0000 0034 0000 0035 ...2...3...4...5
00000130: 0001 6782 0000 0036 0001 6783 0001 6785 ..g....6..g...g.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The files start with a &lt;code&gt;WDP\0&lt;/code&gt; header (which was not found elsewhere on the internet), and a bunch of &lt;code&gt;0000&lt;/code&gt; bytes spread across the odd columns, but we can’t really interpret the meaning of these data by just staring the files. We definitely need the help of the code logic of the game.&lt;/p&gt;
&lt;h2&gt;
  
  
  Preparing for decompilation
&lt;/h2&gt;

&lt;p&gt;As it is commonly known, most logic of Unity games are written in C♯. When compiled to DLL files, C♯ code is rather easy to decompile. However, in environments where .NET runtime is hard to prepare, or where performance is critical, Unity offers an option called &lt;em&gt;&lt;a href="https://docs.unity3d.com/Manual/IL2CPP.html"&gt;Intermediate Language to C++&lt;/a&gt;&lt;/em&gt; (IL2CPP) that further compiles &lt;em&gt;Microsoft Intermediate Language&lt;/em&gt; (MSIL) into C++ and further into native code. This technique is commonly seen on Unity games running on mobile platforms. Nintendo Switch is of no exception.&lt;/p&gt;

&lt;p&gt;Nintendo Switch runs on a special binary format called NSO, which is a custom variant of &lt;a href="https://en.wikipedia.org/wiki/AArch64"&gt;AArch64&lt;/a&gt; &lt;a href="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format"&gt;ELF&lt;/a&gt; binary. To save space, a lot of NSO files are by default compressed. We need to first decompress it with hactool.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hactool &lt;span class="nt"&gt;--uncompressed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;exefs/main_unc exefs/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With the uncompressed binary, we can then use &lt;a href="https://github.com/Perfare/Il2CppDumper"&gt;IL2CPPdumper&lt;/a&gt; to extract the offset and signature of each function in the binary.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Perfare"&gt;
        Perfare
      &lt;/a&gt; / &lt;a href="https://github.com/Perfare/Il2CppDumper"&gt;
        Il2CppDumper
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Unity il2cpp reverse engineer
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Il2CppDumper&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://ci.appveyor.com/project/Perfare/il2cppdumper/branch/master/artifacts" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/cfa548252a219963021c2f98803453d45fc8e898bc43506f9973a51f0c0a7ea7/68747470733a2f2f63692e6170707665796f722e636f6d2f6170692f70726f6a656374732f7374617475732f616e68717733337663706d70386f66613f7376673d74727565" alt="Build status"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;中文说明请戳&lt;a href="https://github.com/Perfare/Il2CppDumperREADME.zh-CN.md"&gt;这里&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unity il2cpp reverse engineer&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Complete DLL restore (except code), can be used to extract &lt;code&gt;MonoBehaviour&lt;/code&gt; and &lt;code&gt;MonoScript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Supports ELF, ELF64, Mach-O, PE, NSO and WASM format&lt;/li&gt;
&lt;li&gt;Supports Unity 5.3 - 2022.2&lt;/li&gt;
&lt;li&gt;Supports generate IDA, Ghidra and Binary Ninja scripts to help them better analyze il2cpp files&lt;/li&gt;
&lt;li&gt;Supports generate structures header file&lt;/li&gt;
&lt;li&gt;Supports Android memory dumped &lt;code&gt;libil2cpp.so&lt;/code&gt; file to bypass protection&lt;/li&gt;
&lt;li&gt;Support bypassing simple PE protection&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Run &lt;code&gt;Il2CppDumper.exe&lt;/code&gt; and choose the il2cpp executable file and &lt;code&gt;global-metadata.dat&lt;/code&gt; file, then enter the information as prompted&lt;/p&gt;

&lt;p&gt;The program will then generate all the output files in current working directory&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Command-line&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;Il2CppDumper.exe &amp;lt;executable-file&amp;gt; &amp;lt;global-metadata&amp;gt; &amp;lt;output-directory&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Outputs&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;DummyDll&lt;/h4&gt;

&lt;/div&gt;

&lt;p&gt;Folder, containing all restored dll files&lt;/p&gt;

&lt;p&gt;Use &lt;a href="https://github.com/0xd4d/dnSpy"&gt;dnSpy&lt;/a&gt;, &lt;a href="https://github.com/icsharpcode/ILSpy"&gt;ILSpy&lt;/a&gt; or other .Net decompiler tools to view&lt;/p&gt;

&lt;p&gt;Can be used to extract Unity &lt;code&gt;MonoBehaviour&lt;/code&gt; and &lt;code&gt;MonoScript&lt;/code&gt;, for &lt;a href="https://github.com/mafaca/UtinyRipper"&gt;UtinyRipper&lt;/a&gt;, &lt;a href="https://7daystodie.com/forums/showthread.php?22675-Unity-Assets-Bundle-Extractor" rel="nofollow"&gt;UABE&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;ida.py&lt;/h4&gt;

&lt;/div&gt;

&lt;p&gt;For IDA&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;ida_with_struct.py&lt;/h4&gt;

&lt;/div&gt;

&lt;p&gt;For IDA, read il2cpp.h file and apply structure…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Perfare/Il2CppDumper"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Il2CppDumper exefs/main romfs/Data/Managed/Metadata/global-metadata.dat il2cppdump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In the new &lt;em&gt;il2cppdump&lt;/em&gt; folder, you can find the JSON file &lt;em&gt;script.json&lt;/em&gt; and a C++ header file &lt;em&gt;il2cpp.h&lt;/em&gt; with all the metadata, which we will use later to locate the function code during the actual decompilation.&lt;/p&gt;

&lt;p&gt;Browsing the &lt;em&gt;script.json&lt;/em&gt; file, we can find some interesting methods that might help us to decode the dictionary file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void CDictionary__GetDictionaryData (CDictionary_o*__this, System_String_o **strReading, System_String_o** strNotation, System_String_o** strMeaning, int32_t nWordId, const MethodInfo* method);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int32_t CDictionary___ConvertKey2String (CDictionary_o*__this, System_String_o** strOut, System_UInt32_array* apKey, uint32_t nLongFlag, const MethodInfo* method);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, not only the class and method names, even the parameter names are kept, which will help us a lot figuring out the code logic.&lt;/p&gt;

&lt;p&gt;To actually decompile the file, we will use &lt;a href="https://github.com/NationalSecurityAgency/ghidra"&gt;Ghidra&lt;/a&gt;, an open-source reverse engineering tool that works on multiple platforms. &lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/NationalSecurityAgency"&gt;
        NationalSecurityAgency
      &lt;/a&gt; / &lt;a href="https://github.com/NationalSecurityAgency/ghidra"&gt;
        ghidra
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Ghidra is a software reverse engineering (SRE) framework
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/NationalSecurityAgency/ghidraGhidra/Features/Base/src/main/resources/images/GHIDRA_3.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2X77FitT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://github.com/NationalSecurityAgency/ghidraGhidra/Features/Base/src/main/resources/images/GHIDRA_3.png" width="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Ghidra Software Reverse Engineering Framework&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Ghidra is a software reverse engineering (SRE) framework created and maintained by the
&lt;a href="https://www.nsa.gov" rel="nofollow"&gt;National Security Agency&lt;/a&gt; Research Directorate. This framework includes a suite of
full-featured, high-end software analysis tools that enable users to analyze compiled code on a
variety of platforms including Windows, macOS, and Linux. Capabilities include disassembly
assembly, decompilation, graphing, and scripting, along with hundreds of other features. Ghidra
supports a wide variety of processor instruction sets and executable formats and can be run in both
user-interactive and automated modes. Users may also develop their own Ghidra extension components
and/or scripts using Java or Python.&lt;/p&gt;
&lt;p&gt;In support of NSA's Cybersecurity mission, Ghidra was built to solve scaling and teaming problems
on complex SRE efforts, and to provide a customizable and extensible SRE research platform. NSA has
applied Ghidra SRE capabilities to a variety of problems that involve analyzing malicious code and
generating deep…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/NationalSecurityAgency/ghidra"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;However, Ghidra does not support NSO and IL2CPP binary out of the box, so we need some install something more to help us, namely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Adubbz/Ghidra-Switch-Loader"&gt;Ghidra Switch Loader&lt;/a&gt;, which can be installed by going to &lt;em&gt;File&lt;/em&gt; -&amp;gt; &lt;em&gt;Install Extensions…&lt;/em&gt; in Ghidra and click the &lt;em&gt;+&lt;/em&gt; button at the corner.
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Adubbz"&gt;
        Adubbz
      &lt;/a&gt; / &lt;a href="https://github.com/Adubbz/Ghidra-Switch-Loader"&gt;
        Ghidra-Switch-Loader
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Nintendo Switch loader for Ghidra
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Ghidra Switch Loader&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;A loader for Ghidra supporting a variety of Nintendo Switch file formats.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Building&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Ensure you have &lt;code&gt;JAVA_HOME&lt;/code&gt; set to the path of your JDK 17 installation.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;GHIDRA_INSTALL_DIR&lt;/code&gt; to your Ghidra install directory. This can be done in one of the following ways:
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: Running &lt;code&gt;set GHIDRA_INSTALL_DIR=&amp;lt;Absolute path to Ghidra without quotations&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macos/Linux&lt;/strong&gt;: Running &lt;code&gt;export GHIDRA_INSTALL_DIR=&amp;lt;Absolute path to Ghidra&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;-PGHIDRA_INSTALL_DIR=&amp;lt;Absolute path to Ghidra&amp;gt;&lt;/code&gt; when running &lt;code&gt;./gradlew&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;GHIDRA_INSTALL_DIR&lt;/code&gt; to your Windows environment variables.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;./gradlew&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You'll find the output zip file inside &lt;code&gt;/dist&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Installation&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Start Ghidra and use the "Install Extensions" dialog (&lt;code&gt;File -&amp;gt; Install Extensions...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Press the &lt;code&gt;+&lt;/code&gt; button in the upper right corner.&lt;/li&gt;
&lt;li&gt;Select the zip file in the file browser, then restart Ghidra.&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Adubbz/Ghidra-Switch-Loader"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Perfare/Il2CppDumper/blob/master/Il2CppDumper/ghidra.py"&gt;ghidra.py from IL2CPPdumper&lt;/a&gt;, which can be installed by copying the file to the &lt;em&gt;%USERPROFILE%/ghidra_scripts&lt;/em&gt; folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decompile the binary
&lt;/h2&gt;

&lt;p&gt;Finally, we can proceed to decompile the binary. To make full use of the metadata we extracted earlier, there are a few steps we need do before starting to read the source code.&lt;/p&gt;

&lt;p&gt;When the &lt;em&gt;main_unc&lt;/em&gt; bianry is first loaded into a Ghidra project, it will prompt you to start an automatic analysis. Since the binary contains about 47MB worth of data, it might take a considerable amount of time to conduct the analysis, and we are only interested in a small portion of the code. I thus chose to skip the analysis.&lt;/p&gt;

&lt;p&gt;The first step is to import the data types defined in the header file into Ghidra. Since the generated &lt;em&gt;il2cpp.h&lt;/em&gt; contains some data types that it does not recognize natively, we need to prepend these lines to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kr"&gt;__int8&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kr"&gt;__int16&lt;/span&gt; &lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kr"&gt;__int32&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="n"&gt;__int64&lt;/span&gt; &lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kr"&gt;__int8&lt;/span&gt; &lt;span class="kt"&gt;int8_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kr"&gt;__int16&lt;/span&gt; &lt;span class="kt"&gt;int16_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kr"&gt;__int32&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="n"&gt;__int64&lt;/span&gt; &lt;span class="kt"&gt;int64_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="n"&gt;__int64&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="kt"&gt;intptr_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="kt"&gt;uintptr_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With the modified header file, we can then return to Ghidra, open &lt;em&gt;File&lt;/em&gt; -&amp;gt; &lt;em&gt;Parse C Source…&lt;/em&gt; to import it. When the &lt;em&gt;Parse C Source&lt;/em&gt; dialog is opened, clear everything in the &lt;em&gt;Source files to parse&lt;/em&gt; and &lt;em&gt;Parse options&lt;/em&gt; section, then add the header file we prepared. Finally, click &lt;em&gt;Parse to Program&lt;/em&gt; to start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--USxZvQ99--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/image-802x1024.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--USxZvQ99--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/image-802x1024.png" alt="A screenshot of the Parse C Source dialog of Ghidra, configure as instructed." width="800" height="1021"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Clear everything and add only the il2cpp.h file to the list.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, we need to label the functions at their respective offsets. Open the Script Manager from &lt;em&gt;Windows&lt;/em&gt; -&amp;gt; &lt;em&gt;Script Manager&lt;/em&gt;, search for &lt;em&gt;ghidra.py&lt;/em&gt;, then click the green play  ⃝▶ button to run the script. When prompted for files, select the &lt;em&gt;script.json&lt;/em&gt; file exported from IL2CPPdumper.&lt;/p&gt;

&lt;p&gt;Once the script is finished, we will see there will be all the functions imported in the &lt;em&gt;Symbol Tree&lt;/em&gt; panel in the sidebar. In the &lt;em&gt;Filter&lt;/em&gt; box of at the bottom of the section, we can enter &lt;em&gt;CDictionary&lt;/em&gt; to find all the dictionary related method.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zGk__HLO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-11-193330.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zGk__HLO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-11-193330.png" alt="Symbol Tree panel with the imported functions" width="800" height="769"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Symbol Tree panel with the imported functions&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then we can select the &lt;code&gt;CDictionary$$GetDictionaryData&lt;/code&gt; in the &lt;em&gt;Symbol Tree&lt;/em&gt;, and Ghidra will direct us to the correct byte offset in the &lt;em&gt;Listing&lt;/em&gt; window. If you see a bunch of &lt;code&gt;??&lt;/code&gt; in the listing, press &lt;span&gt;D&lt;/span&gt; on the keyboard to disassemble the function. As it disassembles, some C-like source code will also show up on the right side in the &lt;em&gt;Decompile&lt;/em&gt; window.&lt;/p&gt;

&lt;p&gt;Inside the window, you may see a lot of types are set as &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;unknown8&lt;/code&gt;, and strange type that might not make sense. A lot of these can be fixed by correcting the function signature and let Ghidra to re-infer the types. To do so, right click the function name and click &lt;em&gt;Edit Function Signature&lt;/em&gt;. In the dialog opened, replace the arguments and return type in the large text box with what you can find in the corresponding &lt;code&gt;"signature"&lt;/code&gt; field in the &lt;em&gt;script.json&lt;/em&gt; file. Take note that you need to drop the &lt;code&gt;MethodInfo * method&lt;/code&gt; argument (which is always the last argument), as it is not decompiled by Ghidra.&lt;/p&gt;

&lt;p&gt;To help improving the readability of the decompiled source, here are some helpful shortcut keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;;&lt;/span&gt; to add comments&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;L&lt;/span&gt; to rename variable&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;Ctrl&lt;/span&gt; + &lt;span&gt;L&lt;/span&gt; to reassign type of a variable&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;Mouse middle click&lt;/span&gt; to highlight all reference of the variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With correct types, Ghidra is able to better infer some pointer offsets as struct properties or array indexes, which makes. the decompiled source easier to read.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KiuvxGDw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-11-193842.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KiuvxGDw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-11-193842.png" alt="Disassembled listing and annotated decompiled source code side by side" width="800" height="407"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Disassembled listing and annotated decompiled source code side by side&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Debugging with emulator
&lt;/h2&gt;

&lt;p&gt;With all the source disassembled and decompiled, it is sufficient to conduct a static analysis and recover most of the logic to parse the dictionary. However, there are still some portion of the code where the decompiled code does not make sense potentially due to some misassigned types.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CDictionary&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="n"&gt;_ConvertKey2String&lt;/span&gt;
              &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CDictionary_o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;__this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;System_String_o&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;strOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;System_UInt32_array&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;apKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;nLongFlag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;undefined8&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;CMojiBlock_o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mojiBlock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;System_String_o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;ulong&lt;/span&gt; &lt;span class="n"&gt;apKey10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;ulong&lt;/span&gt; &lt;span class="n"&gt;uVar1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;lVar2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;matrix03&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;apKey0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;apKeySize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;DAT_7102d7e960&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__this&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CDictionary_o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;InitializeMethodMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xc24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;DAT_7102d7e960&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;apKeySize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;apKey&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;apKeySize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;apKey&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apKeySize&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IndexOutOfRange_FUN_7100bd84b0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__this&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CDictionary_o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;throw_FUN_7100bd72c0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;apKeySize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;apKey&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;apKey0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apKey&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m_Items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;apKey0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apKey&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m_Items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apKeySize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IndexOutOfRange_FUN_7100bd84b0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;throw_FUN_7100bd72c0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmptyString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;apKey10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ulong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;apKey0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7fffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nLongFlag&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;apKey10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT44&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apKey&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m_Items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;apKey0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;uVar1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ulong&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;apKey10&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;strOut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmptyString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;apKey10&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;SingletonMonoBehaviour&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GlobalFunc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_TypeInfo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mh"&gt;0x127&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;SingletonMonoBehaviour&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GlobalFunc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_TypeInfo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mh"&gt;0xd8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ExclusiveMonitor_FUN_7100bb3c20&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;mojiBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CMojiBlock_o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="n"&gt;SingletonMonoBehaviour&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_CMojiBlock&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="n"&gt;get_Instance&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;SingletonMonoBehaviour&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GlobalFunc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_Instance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;matrix03&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mojiBlock&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;m_MojiMtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m03&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;lVar2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;uVar1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;matrix03&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lVar2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IndexOutOfRange_FUN_7100bd84b0&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;throw_FUN_7100bd72c0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="cm"&gt;/* What is this mess trying to get a string from a Unity matrix offset? */&lt;/span&gt;
      &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="n"&gt;Concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;System_String_o&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;matrix03&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lVar2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;uVar1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apKey10&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;apKey10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apKey10&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;strOut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;uVar1&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Without much clue to untangle this mess, I thought it would be easier to get the game running and attach a debugger to it to actually see how it works. Luckily there’s &lt;a href="https://github.com/yuzu-emu/yuzu/"&gt;Yuzu&lt;/a&gt;, a Nintendo Switch emulator that comes with GDB stub that can allow us to attach a GDB session to it.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/yuzu-emu"&gt;
        yuzu-emu
      &lt;/a&gt; / &lt;a href="https://github.com/yuzu-emu/yuzu"&gt;
        yuzu
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Nintendo Switch emulator
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;
  &lt;br&gt;
  &lt;a href="https://yuzu-emu.org/" rel="nofollow"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--u8iPKQGE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://raw.githubusercontent.com/yuzu-emu/yuzu-assets/master/icons/icon.png" alt="yuzu" width="200"&gt;&lt;/a&gt;
  &lt;br&gt;
  &lt;b&gt;yuzu&lt;/b&gt;
  &lt;br&gt;
&lt;/h1&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;
&lt;b&gt;yuzu&lt;/b&gt; is the world's most popular, open-source, Nintendo Switch emulator — started by the creators of &lt;a href="https://citra-emu.org" rel="nofollow"&gt;Citra&lt;/a&gt;
&lt;br&gt;
It is written in C++ with portability in mind, and we actively maintain builds for Windows, Linux and Android
&lt;/h4&gt;

&lt;/div&gt;

&lt;p&gt;
    &lt;a href="https://dev.azure.com/yuzu-emu/yuzu/" rel="nofollow"&gt;
        &lt;img src="https://camo.githubusercontent.com/f98b082a8ee1d3b45a20a1dcc93ec43b0fe6258dd3fe8546faae8d7f86e70a13/68747470733a2f2f6465762e617a7572652e636f6d2f79757a752d656d752f79757a752f5f617069732f6275696c642f7374617475732f79757a752532306d61696e6c696e653f6272616e63684e616d653d6d6173746572" alt="Azure Mainline CI Build Status"&gt;
    &lt;/a&gt;
    &lt;a href="https://discord.com/invite/u77vRWY" rel="nofollow"&gt;
        &lt;img src="https://camo.githubusercontent.com/b481f22b5a59fb85c7314995b8d2658f531c27841e051bf178cde846c00a53d5/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f3339383331383038383137303234323035333f636f6c6f723d353836354632266c6162656c3d79757a75266c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465" alt="Discord"&gt;
    &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/yuzu-emu/yuzu#compatibility"&gt;Compatibility&lt;/a&gt; |
  &lt;a href="https://github.com/yuzu-emu/yuzu#development"&gt;Development&lt;/a&gt; |
  &lt;a href="https://github.com/yuzu-emu/yuzu#building"&gt;Building&lt;/a&gt; |
  &lt;a href="https://github.com/yuzu-emu/yuzu#download"&gt;Download&lt;/a&gt; |
  &lt;a href="https://github.com/yuzu-emu/yuzu#support"&gt;Support&lt;/a&gt; |
  &lt;a href="https://github.com/yuzu-emu/yuzu#license"&gt;License&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Compatibility&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;The emulator is capable of running most commercial games at full speed, provided you meet the &lt;a href="https://yuzu-emu.org/help/quickstart/#hardware-requirements" rel="nofollow"&gt;necessary hardware requirements&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a full list of games yuzu supports, please visit our &lt;a href="https://yuzu-emu.org/game/" rel="nofollow"&gt;Compatibility page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Check out our &lt;a href="https://yuzu-emu.org/" rel="nofollow"&gt;website&lt;/a&gt; for the latest news on exciting features, monthly progress reports, and more!&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Development&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Most of the development happens on GitHub. It's also where &lt;a href="https://github.com/yuzu-emu/yuzu"&gt;our central repository&lt;/a&gt; is hosted. For development discussion, please join us on &lt;a href="https://discord.com/invite/u77vRWY" rel="nofollow"&gt;Discord&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to contribute, please take a look at the &lt;a href="https://github.com/yuzu-emu/yuzu/wiki/Contributing"&gt;Contributor's Guide&lt;/a&gt; and &lt;a href="https://github.com/yuzu-emu/yuzu/wiki/Developer-Information"&gt;Developer Information&lt;/a&gt;
You can also contact any of the developers…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/yuzu-emu/yuzu"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;To enable GDB on Yuzu, go to &lt;em&gt;Emulation&lt;/em&gt; -&amp;gt; &lt;em&gt;Configure…&lt;/em&gt;. In the popup, go to &lt;em&gt;General&lt;/em&gt; -&amp;gt; &lt;em&gt;Debug&lt;/em&gt; -&amp;gt; &lt;em&gt;Debug&lt;/em&gt;, and check &lt;em&gt;Enable GDB Stub&lt;/em&gt;. To the right end of the check box is the port number where Yuzu is listening for GDB connections.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pgUUFiHt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-12-193433.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pgUUFiHt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-12-193433.png" alt="GDB Stub settings in Yuzu" width="800" height="725"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;GDB Stub settings in Yuzu&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once GDB Stub is enabled, the game will only initialize the essential parts, and pause to wait for us to inspect it, and set up breakpoints before we ask it to &lt;code&gt;continue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While Yuzu conveniently provides a way for us to plug GDB into the emulator, not all GDB would work with it. As NSO binaries are essentially AArch64 binaries, we need a GDB that’s compiled to support this architecture to work with it. Fortunately, &lt;a href="https://devkitpro.org/wiki/Getting_Started"&gt;devKitPro&lt;/a&gt; has offered a GDB that’s compatible with AArch64. Once devKitPro is installed, run the following command to install GDB for AArch64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dkp-pacman &lt;span class="nt"&gt;-Syu&lt;/span&gt; devkitA64-gdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With this, we are ready start debugging with GDB and Ghidra on Yuzu. In the Ghidra project window, click the 🪲 icon in the toolbar to open the Ghidra debugger.&lt;/p&gt;

&lt;p&gt;In the debugger window, look for the &lt;em&gt;Debugger Targets&lt;/em&gt; panel to the left, and click the &lt;em&gt;Create a new connection to an (sic.) debugging agent&lt;/em&gt;. In the dialog, select &lt;em&gt;IN-VM GNU gdb local debugger&lt;/em&gt;, and enter the full absolute path of the previously installed &lt;code&gt;gdb&lt;/code&gt; for AArch64 in the &lt;em&gt;GDB launch command&lt;/em&gt; field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jK_DGRDD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/image-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jK_DGRDD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/image-1.png" alt="Screenshot of the Ghidra Connect dialog to specify the GDB command" width="800" height="416"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot of the Ghidra Connect dialog to specify the GDB command&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once finished, there will be a new &lt;em&gt;Interpreter&lt;/em&gt; panel shown up to the right, with a &lt;code&gt;(gdb)&lt;/code&gt; prompt at the bottom of the panel. To connect the GDB session to Yuzu, use the following command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(gdb) target extended-remote 127.0.0.1:5678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;…where &lt;code&gt;5678&lt;/code&gt; is the port number previously set in Yuzu settings.&lt;/p&gt;

&lt;p&gt;If the connection is successful, you can try to inspect the offset of the running game by running &lt;code&gt;monitor get info&lt;/code&gt;. You should get an output similar to this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(gdb) monitor get info
Process: 0x51 (main)
Program Id: 0x01006b900f436000
Layout:
  Alias: 0x108c600000 - 0x208c600000
  Heap: 0x208c600000 - 0x220c600000
  Aslr: 0x0008000000 - 0x8000000000
  Stack: 0x100c600000 - 0x108c600000
Modules:
  0x0008000000 - 0x0008003fff rtld
  0x0008004000 - 0x000b091fff main
  0x000b092000 - 0x000b7a5fff subsdk0
  0x000b7a6000 - 0x000c4a2fff sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In the output, we can see a line that says &lt;code&gt;0x0008004000 - 0x000b091fff main&lt;/code&gt;. This tells you the address range of our binary is mapped in the RAM. In this case, the starting address in the binary &lt;code&gt;0x710000000&lt;/code&gt; is corresponding to &lt;code&gt;0x0008004000&lt;/code&gt; in the RAM from the debugger.&lt;/p&gt;

&lt;p&gt;To let Ghidra to match the RAM data against our disassembled binary, we can first drag the &lt;code&gt;main_unc&lt;/code&gt; item from the Ghidra project window to the debugger, which allows us to see the RAM view and the decompiled listing side by side.&lt;/p&gt;

&lt;p&gt;Then, in the &lt;em&gt;Module&lt;/em&gt; tab of the right sidebar of the debugger, click the 📄 icon to open the &lt;em&gt;Static Mapping&lt;/em&gt; dialog. In the dialog, press the  button to create a mapping. In the &lt;em&gt;Add Static Mapping&lt;/em&gt; dialog, fill the following fields accordingly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static range&lt;/strong&gt; : [&lt;code&gt;ram&lt;/code&gt;: &lt;em&gt;starting address in listing&lt;/em&gt;, &lt;em&gt;&lt;/em&gt;]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic range&lt;/strong&gt; : [&lt;code&gt;ram&lt;/code&gt;: &lt;em&gt;starting address of the &lt;code&gt;main&lt;/code&gt; module&lt;/em&gt;,&lt;em&gt;ending address of the &lt;code&gt;main&lt;/code&gt; module&lt;/em&gt;]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length&lt;/strong&gt; : (auto filled)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifespan&lt;/strong&gt; : (-∞ .. +∞)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QE2gFQln--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-13-084954.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QE2gFQln--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/Screenshot-2023-01-13-084954.png" alt="Add Static Mappings dialog set with the instruction above" width="519" height="345"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Add Static Mappings dialog set with the instruction above&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;em&gt;Apply&lt;/em&gt; to save.&lt;/p&gt;

&lt;p&gt;Now, we have the disassembled and annotated binary mapped against the memory space. We can easily navigate like we did in the &lt;em&gt;CodeBrowser&lt;/em&gt;. Functions are listed in the &lt;em&gt;Symbol browser&lt;/em&gt; at the same place, and we can navigate back to &lt;code&gt;CDictionary$$GetDictionaryData&lt;/code&gt; from there.&lt;/p&gt;

&lt;p&gt;To set a breakpoint, select the instruction from the &lt;em&gt;Dynamic&lt;/em&gt;, &lt;em&gt;Listing&lt;/em&gt;, or &lt;em&gt;Decompile&lt;/em&gt; panel, and press &lt;span&gt;K&lt;/span&gt;.&lt;/p&gt;

&lt;p&gt;Once all necessary breakpoints are set, we can move back to the &lt;em&gt;Interpreter&lt;/em&gt; window, and type &lt;code&gt;continue&lt;/code&gt; to resume the game.&lt;/p&gt;

&lt;p&gt;While debugging, some important tools and informations can be found in different panels of the debugger window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step into, Step over, Continue&lt;/strong&gt; are available in the &lt;em&gt;Object&lt;/em&gt; panel to the left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call stack&lt;/strong&gt; is available at the &lt;em&gt;Stack&lt;/em&gt; panel at bottom left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register values&lt;/strong&gt; are available at the &lt;em&gt;Registers&lt;/em&gt; panel to the right. Right click a register can also jump to the &lt;em&gt;Dynamic&lt;/em&gt; view to the memory address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All GDB commands are still available from the &lt;em&gt;Interpreter&lt;/em&gt; panel_._&lt;/p&gt;

&lt;p&gt;To trigger the game to load up from the dictionary with predictable result, the easiest way is to search for a word in the game’s word lookup feature. Once we hit submit, the game is paused for us at the beginning of the &lt;code&gt;GetDictionaryData&lt;/code&gt; method as the first breakpoint hit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1NsVHN-4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/2023011308312800-90E1EBC1CB6A8579EB92000ACF674175-1024x576.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1NsVHN-4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2023/01/2023011308312800-90E1EBC1CB6A8579EB92000ACF674175-1024x576.jpg" alt="Screenshot of the word lookup feature in the game" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot of the word lookup feature in the game&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;With help of these tools and the memory value at each step, it is much easier to understand the logic of the code, and to untangle those sections that the decompiler did not handle properly.&lt;/p&gt;

&lt;p&gt;By inspecting the memory content around the matrix offset, It turns out that that part of the code is actually loading from a list of strings that converts the &lt;em&gt;key&lt;/em&gt; into a Hiragana, which happened to follow the Unicode order with a few exceptions.&lt;/p&gt;

&lt;p&gt;From there, I was able to fully decode the dictionary files and extract all words with their definitions, which wraps up the project with a success.&lt;/p&gt;

&lt;p&gt;I have released the parsing script, and the parsed dictionary data as JSON file to a &lt;a href="https://github.com/blueset/MojipittanDictionary"&gt;GitHub repository&lt;/a&gt;. There are a lot of binary processing due to the nature of the format, but is in general much more readable. Note that thre are still other data not used in the dictionary files, but since we are only interested in the words and definitions, I think I have achieved the goal.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/blueset"&gt;
        blueset
      &lt;/a&gt; / &lt;a href="https://github.com/blueset/MojipittanDictionary"&gt;
        MojipittanDictionary
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Extracted dictionary data from Kotoba no Puzzle: Mojipittan Encore.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Mojipittan Dictionary&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;Extracted dictionary data from &lt;em&gt;Kotoba no Puzzle: Mojipittan Encore&lt;/em&gt; (『ことばのパズル　もじぴったんアンコール』).&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Files&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;worddata.aid&lt;/code&gt;, &lt;code&gt;worddata.cot&lt;/code&gt;, &lt;code&gt;worddata.dic&lt;/code&gt; – original data from game&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decoder.py&lt;/code&gt; – decoder for the files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dictionary.json&lt;/code&gt; – extracted dictionary data&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/blueset/MojipittanDictionary"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Trivia&lt;/strong&gt;&lt;br&gt;
As it is obvious from the game’s word lookup feature, the longest word in the dictionary has 9 &lt;em&gt;kana&lt;/em&gt;, which means リバース・エンジニアリング (&lt;em&gt;reverse engineering&lt;/em&gt;) in the cover picture doesn’t actually exist in the dictionary, although リバース and エンジニアリング do. Also, ユニティー (&lt;em&gt;Unity&lt;/em&gt;) isn’t in the dictionary either despite being short enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fun fact&lt;/strong&gt;&lt;br&gt;
In order to save space, &lt;em&gt;Mojipittan Encore&lt;/em&gt; uses &lt;a href="https://en.wikipedia.org/wiki/Shift_JIS"&gt;Shift JIS&lt;/a&gt; instead of &lt;a href="https://en.wikipedia.org/wiki/UTF-8"&gt;UTF-8&lt;/a&gt; to store the phrases and definitions, as most characters used take 2 bytes in Shift JIS against 3 in UTF-8. However, this has a problem where some accented letters, like the é in café and cliché, are not encoded in Shift JIS. How the game solves it is to encode these characters as question marks in the dictionary, and hardcoded all the affected words in the game logic when converting them back to UTF-8.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2023/01/16/reverse-engineering-an-il2cpp-nso-binary-case-study-of-mojipittan-encore/"&gt;Reverse engineering an IL2CPP NSO binary: Case study of Mojipittan Encore&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reverseengineering</category>
      <category>ghidra</category>
      <category>il2cpp</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>Use WebVTT without actually using WebVTT: Another way to monitor playback progress of HTML Media Elements</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Sun, 15 Jan 2023 20:37:25 +0000</pubDate>
      <link>https://dev.to/blueset/use-webvtt-without-actually-using-webvtt-another-way-to-monitor-playback-progress-of-html-media-elements-3ek8</link>
      <guid>https://dev.to/blueset/use-webvtt-without-actually-using-webvtt-another-way-to-monitor-playback-progress-of-html-media-elements-3ek8</guid>
      <description>&lt;p&gt;Previously, I have introduced &lt;a href="https://blog.1a23.com/2021/01/06/how-lyricsx-keeps-track-of-progress-of-media-players/"&gt;how LyricsX handled playback progress of different players&lt;/a&gt;, and briefly talked about how I applied its principal to web audio with a &lt;code&gt;requestAnimationFrame()&lt;/code&gt; loop. In this article, I’ll talk about how to use WebVTT, a browser-native captioning feature to receive callbacks on specific time ranges.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API"&gt;Web Video Text Tracks&lt;/a&gt; (WebVTT) is a media captioning feature of HTML5 standard that has support from all major modern browsers. In its typical use cases, author of the webpage can provide a caption file in an SRT-like syntax defined by WebVTT, and link the file to a video element using a &lt;code&gt;&amp;lt;track /&amp;gt;&lt;/code&gt; tag. The browser will then be aware of the caption, and provide option to render it on top of the video.&lt;/p&gt;

&lt;p&gt;Besides rendering captions, WebVTT also provides a set of API that accepts a callback function when the media element enters a specific time range, which we can make use of to track the playback progress without using the sometimes-not-as-precise &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/timeupdate_event"&gt;&lt;code&gt;timeupdate&lt;/code&gt; event&lt;/a&gt;. While WebVTT is designed for video playbacks, the callback features would also work even the track is attached to an &lt;code&gt;&amp;lt;audio /&amp;gt;&lt;/code&gt; node.&lt;/p&gt;

&lt;p&gt;In WebVTT, each caption line with a specific time range is called a “cue”. To provide callback functions to a cue, the cue has to be inserted to a caption track (also known as a &lt;code&gt;textTrack&lt;/code&gt;) programmatically.&lt;/p&gt;

&lt;p&gt;To add a track, it is recommended to create a &lt;code&gt;&amp;lt;track /&amp;gt;&lt;/code&gt; node inside the audio/video node. Despite the Multimedia API offers a &lt;code&gt;.addTextTrack()&lt;/code&gt; method, tracks added in this way cannot have an ID, and cannot be removed until the page refreshes due to the lack of a corresponding &lt;code&gt;.removeTextTrack()&lt;/code&gt; method. Therefore, using DOM nodes is safer in terms of controllability.&lt;/p&gt;

&lt;p&gt;Below is an example of creating a text track programmatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;track&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uniqueId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`track-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uniqueId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subtitles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Track #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uniqueId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Minimum WebVTT track file&lt;/span&gt;
&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:text/vtt;base64,V0VCVlRUCgoK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Enable track in order to allow callbacks&lt;/span&gt;
&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case you need to remove the track to start over, it is as easy as removing the track element from the dom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebVTT tracks provides convenient API to add and remove tracks. When adding a track, it needs 3 parameters, start time and end time in seconds, and the subtitle content. In our use case, the subtitle content is optional, and you can leave it blank, or set it to any content you prefer for debugging purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VTTCue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you can add event listeners to the cue object you created. &lt;code&gt;VTTCue&lt;/code&gt; has two events, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/TextTrackCue#events"&gt;&lt;code&gt;enter&lt;/code&gt; and &lt;code&gt;exit&lt;/code&gt;&lt;/a&gt;. As the name suggest, these events are triggered when the current playback progress enters or exits the cue’s time range. It will trigger in normal playback, and also when user seeks the progress bar.&lt;/p&gt;

&lt;p&gt;For use cases where the events are single-ended, i.e. events only has a start time, and ends right before the next starts, it is possible to only set an &lt;code&gt;enter&lt;/code&gt; event listener, and let the enter event of another cue as the exit event of the current one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Entered cue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last but not least, the cue need to be added to a track to take effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you play the media element with the track attached, you should be able to see the events triggered as it enters the time duration of each cue.&lt;/p&gt;




&lt;p&gt;While this WebVTT implementation is more native to the browser and technically offering better performance than listening to &lt;code&gt;timeupdate&lt;/code&gt; and scan through all time ranges every time, the precision of this is still being limited by the browser’s WebVTT implementation itself. On Chrom* browsers, it does not guarantee a perfect sync with the media, which is also observed with ordinary WebVTT subtitles.&lt;/p&gt;

&lt;p&gt;If you need a better precision control, and only work with short audio files, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API"&gt;Web Audio API&lt;/a&gt; might be a better choice.&lt;/p&gt;

&lt;p&gt;If you are interested in a full example, and you are comfortable with TypeScript and React, here’s an exmaple where I apply this WebVTT track-based tracking to a custom React hook in &lt;em&gt;Lyricova Jukebox&lt;/em&gt;: &lt;a href="https://github.com/blueset/project-lyricova/blob/master/packages/jukebox/src/frontendUtils/hooks.ts#LL356C15-L356C15"&gt;hooks.ts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2023/01/15/use-webvtt-without-actually-using-webvtt-another-way-to-monitor-playback-progress-of-html-media-elements/"&gt;Use WebVTT without actually using WebVTT: Another way to monitor playback progress of HTML Media Elements&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webvtt</category>
    </item>
    <item>
      <title>Flexible and dynamic flow control of Azure DevOps YAML Pipelines using variables</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Sun, 15 Jan 2023 03:40:52 +0000</pubDate>
      <link>https://dev.to/blueset/flexible-and-dynamic-flow-control-of-azure-devops-yaml-pipelines-using-variables-357b</link>
      <guid>https://dev.to/blueset/flexible-and-dynamic-flow-control-of-azure-devops-yaml-pipelines-using-variables-357b</guid>
      <description>&lt;p&gt;Recently I was working on the release automation at work, and one of the requirements is to gap a specific number of hours between stages, and snap to normal business hours. While having a centralized scheduler won’t be a choice unless I want to flood the run logs, and a pipeline run would mostly be unconfigurable once started due to the constraint of Azure DevOps (AzDO) Pipelines, there are still some trickeries to achieve dynamic flow control within the pipeline. In this article, I’d talk about how I setup the flow control.&lt;/p&gt;

&lt;p&gt;Here are the two requirements that we want to address in this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To leave a gap of an arbitrary duration (computed at runtime) between two stages.&lt;/li&gt;
&lt;li&gt;To cut off a branch of the pipeline run, whose condition is determined at runtime, without leaving a failure status, while respecting other settings of the run like skipped stages and cancellations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of them can be achieved on the same YAML Pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arbitrary gap between stages
&lt;/h2&gt;

&lt;p&gt;While AzDO YAML pipelines does not come with a true delay-between-stages feature similar to the one in Classic Release Pipelines, inserting a delay within in a YAML pipelines Job is rather simple. AzDO Pipeline comes with a &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/delay-v1?view=azure-pipelines" rel="noopener noreferrer"&gt;Delay task&lt;/a&gt; that can wait for up to 60 days. We’ve tested it on our pipeline that it can wait for at least 72 hours without issue.&lt;/p&gt;

&lt;p&gt;There are two things need to be taken care of. One is that the Delay task is an &lt;em&gt;agentless&lt;/em&gt; task. If most of your tasks run on an agent, like PowerShell or Bash script, you would need to create a separate _agentless &lt;strong&gt;job&lt;/strong&gt; _ for the Delay task. The other is that AzDO Pipelines by default limit a job/task timeout to be 60 minutes. If you expect it to delay for longer than that, you should update the &lt;code&gt;cancelTimeoutInMinutes&lt;/code&gt; of both the task and the containing job to the maximum duration you’d expect, or simply put &lt;code&gt;0&lt;/code&gt; to eliminate the limit.&lt;/p&gt;

&lt;p&gt;To set a duration dynamically, we can calculate the duration in a job, and set the result with an output variable. In agent tasks, you can set an output variable by &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/set-variables-scripts?view=azure-devops&amp;amp;tabs=bash" rel="noopener noreferrer"&gt;printing a logging command to &lt;code&gt;stdout&lt;/code&gt;&lt;/a&gt;. You can also invoke the REST API of AzDO Pipelines if you are running a custom agentless task to achieve similar outcome.&lt;/p&gt;

&lt;p&gt;Below is an example demonstrating a way to schedule a gap for at least 48 hours, and the gap will always ends on a weekday.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GapScheduler&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gap scheduler&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Scheduler&lt;/span&gt;
    &lt;span class="na"&gt;powershell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;$delayDuration = New-TimeSpan -Hours 48&lt;/span&gt;
      &lt;span class="s"&gt;while (&lt;/span&gt;
        &lt;span class="s"&gt;(Get-Date).Add($delayDuration).DayOfWeek -eq 'Saturday' -or &lt;/span&gt;
        &lt;span class="s"&gt;(Get-Date).Add($delayDuration).DayOfWeek -eq 'Sunday'&lt;/span&gt;
      &lt;span class="s"&gt;) {&lt;/span&gt;
        &lt;span class="s"&gt;$delayDuration = $delayDuration.Add(New-TimeSpan -Hours 24)&lt;/span&gt;
      &lt;span class="s"&gt;}&lt;/span&gt;
      &lt;span class="s"&gt;Write-Host "##vso[task.setvariable variable=delayMinutes;isoutput=true]$($delayDuration.TotalMinutes)"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InterStageGap&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Inter-stage gap&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GapScheduler&lt;/span&gt;
  &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;server&lt;/span&gt;
  &lt;span class="na"&gt;cancelTimeoutInMinutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5760&lt;/span&gt; &lt;span class="c1"&gt;# 4 days&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delay@1&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;delayForMinutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$[dependencies.GapScheduler.outputs['Scheduler.delayMinutes'] ]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cut off a pipeline branch
&lt;/h2&gt;

&lt;p&gt;By cutting off a pipeline branch, I mean that when a certain condition is met, all stages depending on it, both directly and indirectly, should be skipped without leaving an error state. Other stages not on the dependency chain shall not be affected. Also, this shall not affect stages that has their dependency disabled at trigger time. It shall also not affect existing behavior when there are legitimate failures.&lt;/p&gt;

&lt;p&gt;To decide if a stage should be ran, the condition should be decided before it starts running, that is, in its dependency stage. The decision can be made in any sort of task that can set an output variable. Since the output would be a boolean-like value, and the only type you can set for a variable is string, you can put whatever value you like as the true/false value, here we will use &lt;code&gt;"true"&lt;/code&gt; and &lt;code&gt;"false"&lt;/code&gt; for simplicity.&lt;/p&gt;

&lt;p&gt;To skip a stage based on an output of a previous stage, we need to use the stage &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/conditions?view=azure-devops&amp;amp;tabs=yaml%2Cstages" rel="noopener noreferrer"&gt;&lt;code&gt;condition&lt;/code&gt; property&lt;/a&gt;. Apart from the existing default condition, we also need to check if the previous stage has told us to skip.&lt;/p&gt;

&lt;p&gt;There could be three kinds of “output” from the previous stage decision, &lt;code&gt;"true"&lt;/code&gt;, &lt;code&gt;"false"&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; which could happen if the previous stage is disabled at trigger time. Since we want the stage to run in both the &lt;code&gt;"true"&lt;/code&gt; case and the &lt;code&gt;null&lt;/code&gt; case, we only need to check if the variable is set to &lt;code&gt;"false"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The default condition for a stage is &lt;code&gt;succeeded()&lt;/code&gt;, which has covered all existing conditions that can properly handle failure and disabled cases. It is also tested that &lt;code&gt;succeeded()&lt;/code&gt; evaluates to &lt;code&gt;false&lt;/code&gt; when the previous stage is skipped by a &lt;code&gt;condition&lt;/code&gt; &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops" rel="noopener noreferrer"&gt;expression&lt;/a&gt; that was evaluated to &lt;code&gt;false&lt;/code&gt;. In this way, we can safely extend from there with an &lt;code&gt;and()&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;Below is an example that skips &lt;code&gt;Stage2&lt;/code&gt;, and subsequently &lt;code&gt;Stage3&lt;/code&gt; on every Monday.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stage1&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Other jobs go here...&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CutOffDecider&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cut-off decider&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Decider&lt;/span&gt;
      &lt;span class="na"&gt;powershell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;$decision = "true"&lt;/span&gt;
        &lt;span class="s"&gt;if (Get-Date.DayOfWeek -eq "Monday") {&lt;/span&gt;
          &lt;span class="s"&gt;$decision = "false"&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;Write-Host "##vso[task.setvariable variable=shouldRunNextStage;isoutput=true]$decision"        &lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stage2&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stage1&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;and(succeeded(), ne(dependencies.Stage1.outputs['CutOffDecider.Decider.shouldRunNextStage'], 'false'))&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Jobs go here...&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stage3&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stage2&lt;/span&gt;
  &lt;span class="c1"&gt;# Stage 3 is also skipped as a part of the dependency tree when Stage 2 is skipped&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Jobs go here...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Dynamic gap and cut-off control together forms two of the building blocks of the foundation of a flexible and dynamic multi-stage release automation system that works on Azure DevOps YAML pipelines.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2023/01/15/flexible-and-dynamic-flow-control-of-azure-devops-yaml-pipelines-using-variables-using-variables/" rel="noopener noreferrer"&gt;Flexible and dynamic flow control of Azure DevOps YAML Pipelines using variables&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com" rel="noopener noreferrer"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>azuredevops</category>
      <category>pipelines</category>
      <category>yaml</category>
    </item>
    <item>
      <title>Content-aware Infinite Scroll Loop using JavaScript</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Mon, 11 Jul 2022 02:52:58 +0000</pubDate>
      <link>https://dev.to/blueset/content-aware-infinite-scroll-loop-using-javascript-184l</link>
      <guid>https://dev.to/blueset/content-aware-infinite-scroll-loop-using-javascript-184l</guid>
      <description>&lt;p&gt;This project came out from a key highlight of the &lt;a href="https://1a23.com/works/open-source/luna-for-ctfd/"&gt;Luna for CTFd theme&lt;/a&gt;, a CTFd theme trying to reproduce the atmosphere of the game &lt;a href="https://pjsekai.sega.jp/"&gt;&lt;em&gt;Project SEKAI: Colorful Stage feat. Hatsune Miku&lt;/em&gt;&lt;/a&gt;. In order to recreate the unique and symbolic music selection interface, I went forward to write this piece of code out myself.&lt;/p&gt;

&lt;p&gt;Below is a simple demo of the final outcome.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/anon/embed/preview/eYMOwgK?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Before I started coding, the first thing I did was to look for existing implementations of scroll loops. There are mainly two types of implementation, one is to have the scroll content overflow the container, and &lt;a href="https://codepen.io/vincentorback/pen/zxRyzj"&gt;programmatically manipulate the scroll position&lt;/a&gt;, the other is to &lt;a href="https://stackoverflow.com/a/64536089/"&gt;append elements dynamically to the end of the scroll content&lt;/a&gt; when user scrolling near to the end.&lt;/p&gt;

&lt;p&gt;I went with the former strategy because the latter would only allow scrolling in a single direction. However, the former strategy relies on the fact that the list stretching beyond the view size, and has no consideration on the selected item within the list.&lt;/p&gt;

&lt;p&gt;Here is a list of features I implemented for the scroll loop list, and I will talk about them one by one in detail afterwards.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repeat the list items at least once both above and below the original list to cover the entire screen.&lt;/strong&gt; This is to ensure that we have enough content for the loop transition to happen smoothly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scroll in both directions should jump forward or backwards accordingly&lt;/strong&gt; to ensure a smooth loop.

&lt;ul&gt;
&lt;li&gt;When an item is selected, once it is being scrolled out of the screen, it should appear again in the opposite side following the scroll direction once appeared. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;When an item is selected, it should be &lt;strong&gt;highlighted&lt;/strong&gt; , and there should be &lt;strong&gt;a smooth transition to move it to the center of the screen&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The list should able to be &lt;strong&gt;updated when filtered with different criteria&lt;/strong&gt; , and the selected item will be cleared when the criteria changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Repeating the list items
&lt;/h3&gt;

&lt;p&gt;Repeating the list items is seemingly the easiest problem to solve here. The only factor we need to determine the number of times we need to repeat each side. Usually this factor is determined by the size of the viewport, and must be updated when the window resizes. However, in this example, we are going to use the screen size as the basis for performance reasons. In this way, we do not need to keep track of the size of window constantly.&lt;/p&gt;

&lt;p&gt;Here we assume that the screen size is constant, that means the script cannot handle cases where the window is being dragged to another screen with a larger size, or is rotated 90°. Yet these the size will be recalculated when the user switches to another category, so it is not too much of an issue.&lt;/p&gt;

&lt;p&gt;The number of times we need to repeat the list can be found with an easy division:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;⌈height of screenheight of an item×number of items⌉
\left\lceil{\text{height of screen}\over\text{height of an item}\times\text{number of items}}\right\rceil
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter"&gt;&lt;span class="delimsizing size3"&gt;⌈&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;height of an item&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;number of items&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;height of screen&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter"&gt;&lt;span class="delimsizing size3"&gt;⌉&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Hereafter, we will refer to the list at the center as the “center segment” of the list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scroll jumps to the opposite direction for a smooth loop
&lt;/h3&gt;

&lt;p&gt;This is the most basic requirement to achieve a scroll loop. When the user scrolls to the top or the bottom end of the list, we let the browser jump back to the opposite end to allow the user to continue scrolling. This is done by listening to the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scroll_event"&gt;&lt;code&gt;scroll&lt;/code&gt; event&lt;/a&gt; of the scrolling container and update the scroll offset when necessary.&lt;/p&gt;

&lt;p&gt;Below is an animation illustrating the logic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://blog.1a23.com/2022/07/11/content-aware-infinite-scroll-loop-using-javascript/"&gt;Watch the animation on my blog.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As illustrated, despite there is a sudden jump in the scroll position, visually the user would feel like the list is kept on scrolling because the content in the viewport remains the same before and after the jump.&lt;/p&gt;

&lt;p&gt;The remaining question would be when we jump and how far we jump. I will talk about these case by case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Design decision: Item selection&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The original game has taken an approach to always select the centered item while scrolling. While this approach may eliminate the need of differentiate the jump logic, in my use case, each selection of an item would send an HTTP request to the server. Selecting items while scrolling would unnecessarily increase the load of the server, and the strategy is thus abandoned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Scenario 1: No items are selected
&lt;/h4&gt;

&lt;p&gt;When no items are selected, all of the repeating segments will be fully identical to the center one. In this case, we will use the center of scrolling container as a basis. When the center segment is scrolled pass the basis, we jump the scroll position for a full height of the center segment to ensure that the basis line always overlap with the center segment.&lt;/p&gt;

&lt;p&gt;The length of the jump must always be a multiple of the center segment height to ensure that we do not create any visual disruption while manipulating the scroll position.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scenario 2: An item is selected
&lt;/h4&gt;

&lt;p&gt;Once an item is selected, the center segment will have an item highlighted comparing to the repeating segments. With the highlighted row, users would tend to use it as a visual probe when scrolling through the list. This means we can no longer jump through the list bluntly, as for shorter lists the selected item will be jumping around in the middle of the screen, which is less ideal visually.&lt;/p&gt;

&lt;p&gt;Instead, we want the select item loop across the screen like &lt;a href="https://en.wikipedia.org/wiki/Snake_(video_game_genre)"&gt;the Snake&lt;/a&gt;, while still keeping it always within the scroll window whenever possible.&lt;/p&gt;

&lt;p&gt;The logic I had for this scenario can be summarized in the pseudo code below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if the selected item is out of view:
  if the same item in the previous or next repeating segment is in the view:
    // Jump to the furthest selected item in the view.
    jump max(⌈viewHeight ÷ centerHeight⌉, 1) × centerHeight
  else:
    // The view is in between two selected item
    Follow the logic in Scenario 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason of not highlighting the item in every repeating segment is because that the view will be visually too busy when multiple segments are shown in the view. We only want to keep the selected item in the center segment.&lt;/p&gt;

&lt;p&gt;Below is an animation illustrating an example of this logic, where ★ signifies the selected item.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://blog.1a23.com/2022/07/11/content-aware-infinite-scroll-loop-using-javascript/"&gt;Watch the animation on my blog.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Smooth transition to the center when an item is selected
&lt;/h3&gt;

&lt;p&gt;With modern browser, smooth scroll to a specified offset is no longer hard, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo"&gt;the &lt;code&gt;scrollTo&lt;/code&gt; API offers an option &lt;code&gt;behavior: "smooth"&lt;/code&gt;&lt;/a&gt;. Specifying the offset, and the browser will do the transition animation for you. The only one trick we would need do is to ensure that the item clicked is from the center segment for highlighting.&lt;/p&gt;

&lt;p&gt;The logic is rather simple, first check if the item clicked is within the center segment. If not, jump the scroll offset by the distance between the clicked item and the corresponding item in the center segment. Finally do a scroll transition.&lt;/p&gt;

&lt;p&gt;This transition gives users a positive feedback that the web app has responded to their selection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the list with filters
&lt;/h3&gt;

&lt;p&gt;Updating the list is in fact mostly outside of the scope of the scroll loop project, expect that the number of repeat times need to be recalculated when the length of filtered items changes.&lt;/p&gt;

&lt;p&gt;To achieve this, you can choose whichever framework you like, or even vanilla JavaScript. In the example above, I used &lt;a href="https://alpinejs.dev/"&gt;Alpine.js&lt;/a&gt; which is the front end framework used by the default CTFd theme.&lt;/p&gt;

&lt;p&gt;There is one thing you may need to take note when using Alpine.js: clearing selection must be done after populating the filtered list. Otherwise the repeated list might not be updated correctly due to a rendering disruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser Compatibility
&lt;/h3&gt;

&lt;p&gt;While this solution works for most modern browsers, there is a specific problem with Firefox (Gecko) for Windows. As outlined in &lt;a href="https://firefox-source-docs.mozilla.org/performance/scroll-linked_effects.html"&gt;their Wiki article about scroll-linked effects&lt;/a&gt;, scrolling events in Firefox for Windows are sent asynchronously, and usually comes with a delay. That means the user scroll will sometimes override the value set by JavaScript, causing the screen to jitter as a result.&lt;/p&gt;

&lt;p&gt;While the article has proposed various JavaScript API for use cases that need to manipulate the scroll position itself, none of them is implemented at the moment. With this, I had to resort to a suboptimal solution, &lt;a href="https://lodash.com/docs/4.17.15#debounce"&gt;debounce&lt;/a&gt; the scroll position update event calls until the user stops scrolling. While this may make the scroll manipulation seem slow, it is still better than constantly jittering user’s screen during scrolling.&lt;/p&gt;

&lt;p&gt;For reference, Firefox for Android, macOS and Linux works just fine as Chrome and Safari do.&lt;/p&gt;




&lt;p&gt;While some of the solutions may sound hacky and not perfect across all forms, this is the best I can come up with at the moment. I definitely look forward to more advanced scroll manipulation API being implemented for a simpler and better solution that this.&lt;/p&gt;

&lt;p&gt;Feel free to leave a comment if you have any questions or suggestions. Hope you enjoyed this!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2022/07/11/content-aware-infinite-scroll-loop-using-javascript/"&gt;Content-aware Infinite Scroll Loop using JavaScript&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>infinitescroll</category>
      <category>scrollloop</category>
      <category>web</category>
    </item>
    <item>
      <title>Extract and Visualize Your Telegram Group Network</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Fri, 02 Jul 2021 04:52:04 +0000</pubDate>
      <link>https://dev.to/blueset/extract-and-visualize-your-telegram-group-network-5el</link>
      <guid>https://dev.to/blueset/extract-and-visualize-your-telegram-group-network-5el</guid>
      <description>&lt;p&gt;This was one of the ideas I wanted to implement for a while: to visualize the members of all groups I joined on Telegram, and see how many circles I had on the single platform. Thanks to the openness of Telegram’s client API, this was rather easy to achieve.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aOJZAH4P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2021/06/image-1024x778.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aOJZAH4P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2021/06/image-1024x778.png" alt="A sneak peek of what I got." width="800" height="608"&gt;&lt;/a&gt;A sneak peek of what I got.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get prepared
&lt;/h2&gt;

&lt;p&gt;Here is a list of things you’ll need to build your own graph&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.python.org/downloads/"&gt;Python 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pyrogram/pyrogram"&gt;Pyrogram&lt;/a&gt; (&lt;code&gt;pip3 install pyrogram&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://networkx.org/"&gt;Networkx&lt;/a&gt; (&lt;code&gt;pip3 install networkx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Telegram account and &lt;a href="https://core.telegram.org/api/obtaining_api_id"&gt;Client API ID&lt;/a&gt; (&lt;em&gt;not to be confused with Bot API Token&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gephi.org/"&gt;Gephi&lt;/a&gt;, &lt;a href="https://www.graphistry.com/"&gt;Graphistry&lt;/a&gt; or other graph visualization tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gather data
&lt;/h2&gt;

&lt;p&gt;This time I used &lt;a href="https://github.com/pyrogram/pyrogram"&gt;Pyrogram&lt;/a&gt; instead of &lt;a href="https://github.com/LonamiWebs/Telethon"&gt;Telethon&lt;/a&gt;, as I started working on this in a REPL instance in Python, and it used to have a synchronous-flavor that served the purpose best. But I was wrong, they have already got rid of the sync flavor in a previous version. So I chose to turn to iPython for REPL because it can automatically run any async function without the bootstrap code.&lt;/p&gt;

&lt;p&gt;Anyway, no worry for you, I’ve already consolidated it to a Python script that handles all the async stuff properly. The code is not complicated in any way, just iterate through all the dialogs and try load through the members whenever possible, and then dump the data into two &lt;a href="https://docs.python.org/3/library/pickle.html"&gt;Pickles&lt;/a&gt; files. It should be really easy to rewrite this part with other libraries like Telethon if you already have a session file ready.&lt;/p&gt;

&lt;p&gt;Remember to replace the API ID and API hash with your own ones. If you have already logged in with Pyrogram, rename the session name with proper one for Pyrogram to pick up the proper session file you have, otherwise you should be prompted to log in for the first time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyrogram&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="n"&gt;api_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="c1"&gt;# your API ID here
&lt;/span&gt;&lt;span class="n"&gt;api_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1234567890abcdef1234567890abcdef&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# your API hash here
&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_pyrogram_session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;dialogs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_dialogs&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

    &lt;span class="n"&gt;chat_members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dialogs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_members&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
            &lt;span class="n"&gt;chat_members&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;member(s) loaded.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dialogs.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dialogs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;members.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Construct graph
&lt;/h2&gt;

&lt;p&gt;Now, with the full data collected, we can construct a graph for other tools to visualize. In this example, we will use Networkx to construct the graph data and optimise it for visualization.&lt;/p&gt;

&lt;p&gt;You may want to get rid of all nodes that connects to yourself for a better visualization. Get your ID on Telegram with any bot that’s capable of doing it, and put it on line 4 of the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;networkx&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;

&lt;span class="n"&gt;MY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345678&lt;/span&gt;

&lt;span class="c1"&gt;# Load data
&lt;/span&gt;&lt;span class="n"&gt;dialogs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dialogs.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;members.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Build graph
&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Graph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;MY_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have the graph, we can continue to work on it to make the visualization look better. Since my graph had 10k+ of nodes and 20k+ of edges, it would be very difficult to visualize graphs of this scale. I then removed all nodes that only has one edge, that is, all people that shares only 1 common group with me.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;g2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this removal, I now have 8k+ nodes and 18k+ edges in the graph, reducing the size by about 20%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualize with Gephi
&lt;/h2&gt;

&lt;p&gt;Gephi is an open-source tool for graph visualization, and works especially well on large graphs like ours. However, getting graphs into the software last updated in 2017 could be tricky.&lt;/p&gt;

&lt;p&gt;Through some experimentations, I found out that DOT format works the best importing to Gephi, so let’s get started.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;networkx.drawing.nx_pydot&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;write_dot&lt;/span&gt;
&lt;span class="nf"&gt;write_dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;graph.dot&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the &lt;code&gt;graph.dot&lt;/code&gt; file in your working directory with Gephi, and you should be able to see the graph shown in the window. Usually, it will automatically generate a layout for you. If you are not satisfied with it, I’d recommend to reset it with &lt;em&gt;Random Layout&lt;/em&gt; and build a new layout again with &lt;em&gt;ForceAtlas 2&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eMsDiQ74--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2021/06/image-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eMsDiQ74--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2021/06/image-1.png" alt="Choose the layout engine you want and then click run. Note that you might need to click Stop manually if a layout engine runs for too long." width="672" height="406"&gt;&lt;/a&gt;Choose the layout engine you want and then click run. Note that you might need to click &lt;em&gt;Stop&lt;/em&gt; manually if a layout engine runs for too long.&lt;/p&gt;

&lt;p&gt;When you are satisfied with the graph, you can export it as a PNG, PDF, or SVG files for sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding metadata for Gephi
&lt;/h2&gt;

&lt;p&gt;If you think your computer is powerful enough, you can also consider adding labels and other medatada to the graph, so that you can explore it better in Gephi. Here is an example of adding chat names to the graph as labels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dialogs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;is_deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%DELETED_ACCOUNT%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dmap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has no name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case there are people with uncommon Unicode character in their names (mainly control characters and combining characters), here is a script to keep only letter characters in the label.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFKC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMZPS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, just re-export the chart in DOT format, and import it with Gephi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some interesting analytics
&lt;/h2&gt;

&lt;p&gt;Besides the chart, you can also get some interesting analytics out of the data that you can’t get easily with an official Telegram client, desktop or mobile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connected components and their sizes
&lt;/h3&gt;

&lt;p&gt;In an undirected graph, a connected component is an induced subgraph in which any two vertices are connected to each other by paths, and which is connected to no additional vertices in the rest of the graph&lt;sup id="fnref1"&gt;1&lt;/sup&gt; In this context, it shows you how many parts are your network on Telegram separated with only you but no one else in common. This command shows you how large each of part of your network is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connected_components&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Most connected nodes
&lt;/h3&gt;

&lt;p&gt;Most connected nodes here are either groups with most members, or people that shares most groups in common with you. Here is a simple code script that shows you the top 100 most connected nodes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;sdg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dg2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdg&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pairs with most common neighbours
&lt;/h3&gt;

&lt;p&gt;Through pairs with most common neighbours, you can find out which 2 groups or which 2 people are the most similar. For groups, the most similar pair shares the most number of common users. Vice versa, the most similar users share the most number of common groups.&lt;/p&gt;

&lt;p&gt;This script prints the top 100 pairs of nodes with common neighbors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;combinations&lt;/span&gt;

&lt;span class="n"&gt;pair_common&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;combinations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;inb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;jnb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jnb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inb&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;jnb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pair_common&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;

&lt;span class="n"&gt;pair_common&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair_common&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other visualization tools
&lt;/h2&gt;

&lt;p&gt;Apart from Gephi, there is another tool that can visualize graphs of large sizes. &lt;a href="https://t.me/sharzy_talk/1125"&gt;Sharzy&lt;/a&gt; recommended &lt;a href="https://www.graphistry.com/"&gt;Graphistry&lt;/a&gt; as an alternative tool that renders in a web browser and offers colors. Here’s a sample from Sharzy’s Telegram Channel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://t.me/sharzy_talk/1125"&gt;https://t.me/sharzy_talk/1125&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; : To render with Graphistry, you need to upload your graph data to their server, take caution before you use it.&lt;/p&gt;

&lt;p&gt;Here is a sample snippet to upload your graph to Graphistry for rendering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;graphistry&lt;/span&gt;
&lt;span class="n"&gt;graphistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store_token_creds_in_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graphistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hub.graphistry.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_TOKEN_GOES_HERE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;graphistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dst&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nodeid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2021/07/02/extract-and-visualize-your-telegram-group-network/"&gt;Extract and Visualize Your Telegram Group Network&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;From &lt;a href="https://en.wikipedia.org/wiki/Component_(graph_theory)"&gt;Wikipedia&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>python</category>
      <category>telegram</category>
      <category>visualization</category>
    </item>
    <item>
      <title>Use GraphQL subscription to show progress of time-consuming operations</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Thu, 28 Jan 2021 17:41:28 +0000</pubDate>
      <link>https://dev.to/blueset/use-graphql-subscription-to-show-progress-of-time-consuming-operations-3boh</link>
      <guid>https://dev.to/blueset/use-graphql-subscription-to-show-progress-of-time-consuming-operations-3boh</guid>
      <description>&lt;p&gt;GraphQL is feature-rich query language designed to fit majority of needs of generic online applications. The 3 main types of operations in GraphQL are &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;mutation&lt;/code&gt; and &lt;code&gt;subscription&lt;/code&gt;. While &lt;code&gt;quer&lt;/code&gt;ies and &lt;code&gt;mutation&lt;/code&gt;s are send in HTTP requests, &lt;code&gt;subscription&lt;/code&gt;s are running on WebSocket, which enables it to receive updates in real time. In this article, I would like to talk about using the &lt;code&gt;subscription&lt;/code&gt; to report real-time results and/or progresses to time-consuming operations.&lt;/p&gt;

&lt;p&gt;Usually, for time-consuming tasks like server-side downloads, or gathering data from multiple remote sources, it is usually favourable to provide a visual feedback to the user about the progress – or even better – partial results. In GraphQL, we can make use of &lt;code&gt;subscription&lt;/code&gt; for the server to stream responses while during the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema design
&lt;/h2&gt;

&lt;p&gt;Suppose that we have a query that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;multiSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can refactor it as follows for progress reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MultiSearchProgress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;totalSources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;completedSources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;multiSearchStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MultiSearchProgress&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;multiSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So here, we allow the &lt;code&gt;multiSearch&lt;/code&gt; query to take in an optional string parameter &lt;code&gt;sessionId&lt;/code&gt;. The client generates a strong random string as a session ID, supplied when making the query. In the meantime, the client can subscribe to to the &lt;code&gt;multiSearchStatus&lt;/code&gt; channel with the same session ID to get real time progress from the server.&lt;/p&gt;

&lt;p&gt;When the server receives the query and starts its computation, it can take the session ID from the query, and report it to the corresponding subscription channel with progresses. In the subscription payload, we included some common data like total and loaded numbers.&lt;/p&gt;

&lt;p&gt;We have also included the &lt;code&gt;succeeded: Boolean&lt;/code&gt; to indicate the status of query. This property can has 3 states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt;: query in progress&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;true&lt;/code&gt;: succeeded&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;false&lt;/code&gt;: failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can also give the client a hint of when the query finishes, as an additional safeguard to the actual query outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  More payloads
&lt;/h2&gt;

&lt;p&gt;If you want to include more information like partial results, we can also add that to the progress type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MultiSearchProgress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;totalSources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;completedSources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;newResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this new definition, we can stream any new search results retrieved during the progress. These new results can be displayed to the user in real time during the query. Optionally, the final query result can be used to replace data retrieved in the subscription, to avoid any loss of results in WebSocket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points to note
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;As &lt;code&gt;subscription&lt;/code&gt;s require a WebSocket to set up, and not commonly used in most GraphQL servers, it could be left out in your environment if GraphQL have already been used. In this case, additional effort is needed throughout the connection path (server, forwarder, load balancer, CDN, etc.) in order for WebSocket to work.&lt;/li&gt;
&lt;li&gt;The client is recommended to subscribe with the generated ID &lt;strong&gt;before&lt;/strong&gt; the query is sent, so as not to lose any update issued right after the query started.&lt;/li&gt;
&lt;li&gt;If you have no authorization in place for subscriptions, all such progress reports will be available to anyone with the correct session ID at the right time. If the data sent through subscriptions are sensitive or private. It is recommended to use a cryptographically strong random string generator on the client to generate session IDs, and apply authentication wherever possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code sample
&lt;/h2&gt;

&lt;p&gt;Here is a small sample of implementation in this concept in TypeScript. This sample makes use of &lt;a href="https://typegraphql.com/"&gt;TypeGraphQL&lt;/a&gt;, &lt;a href="http://reactjs.org/"&gt;React&lt;/a&gt;, &lt;a href="https://www.apollographql.com/"&gt;Apollo Server and Client&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mock search source interface&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SearchSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Generic interface for data with session ID&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PubSubSessionPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Resolver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MultiSearchResolver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nl"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchSource&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;returns&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;LyricsKitLyricsEntry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;multiSourceSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sessionId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MULTI_SOURCE_SEARCH_PROGRESS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Publisher&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PubSubSessionPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MultiSourceSearchStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outcomes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// send partial results for each source&lt;/span&gt;
        &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})));&lt;/span&gt;

    &lt;span class="c1"&gt;// Send end-of-query update&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// join SearchResult[][] into SearchResult[]&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outcomes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Subscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MultiSourceSearchStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MULTI_SOURCE_SEARCH_PROGRESS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;multiSourceSearchProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PubSubSessionPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MultiSourceSearchStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sessionId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;MultiSourceSearchStatus&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MULTI_ENGINE_SEARCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  query($query: String!, $sessionId: String) {
    multiSourceSearch(query: $query, sessionId: $sessionId) {
      // fields go here
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MULTI_ENGINE_SEARCH_PROGRESS_SUBSCRIPTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  subscription ($sessionId: String!) {
    multiSourceSearchProgress(sessionId: $sessionId) {
      // fields go here
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apolloClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useApolloClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searchResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSearchResults&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSecureRandomString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setSearchResults&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;apolloClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;multiSourceSearchProgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MULTI_ENGINE_SEARCH_PROGRESS_SUBSCRIPTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zenSubscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiSourceSearchProgress&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setSearchResults&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiSourceSearchProgress&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Finished with error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;apolloClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;multiSourceSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MULTI_ENGINE_SEARCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setSearchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiSourceSearch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;zenSubscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2021/01/28/use-graphql-subscription-to-show-progress-of-time-consuming-operations/"&gt;Use GraphQL subscription to show progress of time-consuming operations&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>node</category>
      <category>apollo</category>
    </item>
    <item>
      <title>Measure per-letter dimension of text in JavaScript</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Sat, 09 Jan 2021 05:14:49 +0000</pubDate>
      <link>https://dev.to/blueset/measure-per-letter-dimension-of-text-in-javascript-255n</link>
      <guid>https://dev.to/blueset/measure-per-letter-dimension-of-text-in-javascript-255n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Create a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Range"&gt;Range&lt;/a&gt;, set proper start and end points up to the text node with proper offset, then use &lt;code&gt;Range.getBoundingClientRect()&lt;/code&gt; to get the dimensions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a part of &lt;em&gt;&lt;a href="https://github.com/blueset/project-lyricova/tree/master/packages/jukebox"&gt;Lyricova Jukebox&lt;/a&gt;&lt;/em&gt;, we wanted to support inline karaoke swipe animation. With the time tags in the data, it is easy to figure out the time when the animation must reach a certain character. Then we need to figure out a way measure per character dimension for the animation to work.&lt;/p&gt;

&lt;p&gt;Initially, I used to do in the way &lt;a href="https://github.com/adobe/balance-text/"&gt;BalanceText&lt;/a&gt; does, modifying DOM content and CSS properties to measure the DOM node itself. BalanceText is a JavaScript library I recently read on, it inserts &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; tags in to text in order to achieve a balanced line length by overriding the DOM HTML source of the targeted node directly.&lt;/p&gt;

&lt;p&gt;This indeed could work in a way, especially for BalancedText where it operates based on the “break opportunities” within the pure HTML source. However, that brings a problem to some applications that relies on those DOM elements. Unlike BalanceText, we don’t necessary need to modify the content, especially the DOM structure, just to measure its dimensions. The way BalanceText does discards the old DOM subtree, and replaces them with a new one by directly modifying the &lt;code&gt;innerHTML&lt;/code&gt; property of the subtree. This means any event listeners and other references to the element are all unusable after the measurement. This is certainly something that we don’t want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Range API
&lt;/h2&gt;

&lt;p&gt;After some digging through the internet, I found this &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Range"&gt;Range&lt;/a&gt; API of the web, which can select a region of the webpage and measure its dimension. This API is used to represent &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Selection"&gt;selections by users&lt;/a&gt; in a webpage, but it isn’t necessary to modify the selection in order to use it.&lt;/p&gt;

&lt;p&gt;In this way, we can pretty much say that we can measure the per-character width of text on the webpage without any modification to the DOM tree or even the UI.&lt;/p&gt;

&lt;p&gt;I will demonstrate this feature by measuring an accumulating width of the text per character, i.e. the width of text from the beginning to the end of the &lt;em&gt;n_th character for all _n&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The general idea is to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Locate the starting point. In this example we want to measure from the beginning of a node, so we can just use the &lt;code&gt;Range. __proto__.setStartBefore(el)&lt;/code&gt; method.&lt;/li&gt;
&lt;li&gt;Recursively locate all text nodes in the sub-DOM tree.&lt;/li&gt;
&lt;li&gt;For each text node found

&lt;ol&gt;
&lt;li&gt;Set the range to end on each character of the node by using &lt;code&gt;Range. __proto__.setEnd(el, count)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Get the width of the range with &lt;code&gt;Range. __proto__.getBoundingClientRect()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Below is a sample code for the process in TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * A generator that recursively find all text nodes in a subtree rooted at el.
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;recursivelyFindTextNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Generator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEXT_NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ELEMENT_NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;recursivelyFindTextNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childNodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRange&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStartBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// expand all text nodes found.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nf"&gt;recursivelyFindTextNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textNode&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;textNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;textLength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Get the total length of the text&lt;/span&gt;
&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEndAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;Since this API is measuring things rendered on screen directly, if you want to measure something, it has to be rendered first.&lt;/p&gt;

&lt;p&gt;Also, if you want to measure the length of a long string of text in pixels, the text must be rendered in one line. This is achievable by a few lines of CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;white-space&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;display&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;inline&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;fit-content&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Finally, here is a simple CodePen demo:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/blueset/embed/rNMrdNO?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2021/01/09/measure-per-letter-dimension-of-text-in-javascript/"&gt;Measure per-letter dimension of text in JavaScript&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>html</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How LyricsX keeps track of progress of media players</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Wed, 06 Jan 2021 05:50:57 +0000</pubDate>
      <link>https://dev.to/blueset/how-lyricsx-keeps-track-of-progress-of-media-players-4334</link>
      <guid>https://dev.to/blueset/how-lyricsx-keeps-track-of-progress-of-media-players-4334</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/ddddxxx/LyricsX"&gt;LyricsX&lt;/a&gt; is an open source software for macOS to download and display lyrics of current playing track on Music (previously iTunes), Spotify, Audirvana, Vox, Swinsian, or the &lt;em&gt;Now Playing&lt;/em&gt; indicator in the OS. It gets time-tagged lyrics files from local storage or internet, and then display the lyrics in sync with the player.&lt;/p&gt;

&lt;p&gt;As a crucial component of &lt;a href="https://twitter.com/i/events/1301509405490511872"&gt;the development of Lyricova Jukebox&lt;/a&gt;, I have researched multiple implementations of real time lyrics display programs, and I found the mechanism behind LyricsX particularly interesting. Here in this article, I’d share with you how LyricsX tracking the player progress in a unique and resource-saving way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common practices
&lt;/h2&gt;

&lt;p&gt;Before we go to LyricsX, let’s first take a look at how the majority of other open source software work on this. In short, there are 2 major ways of doing it: rely on the player to send out update events, or query the player for progress at a regular time interval. Both ways are not ideal when it comes to the extreme cases where the user adjusts the player progress a lot, and the lyrics having a lot of lines in a short period of time.&lt;/p&gt;

&lt;p&gt;The first way, &lt;strong&gt;relying on the player to send out progress update events&lt;/strong&gt;, is not reliable because the update interval totally depends on the platform. Some platforms may issue updates several times a second, but some may just send once a couple of seconds. In the worst case, some lines might just be skipped over due to the long update interval. Even worse some might just lack of such feature exposed to the public, rendering this way unusable in those cases.&lt;/p&gt;

&lt;p&gt;The second way, &lt;strong&gt;query the player for progress on a regular time interval&lt;/strong&gt;, could indeed solve the problems mentioned above, but just in an ideal system where the query itself is as simple as reading a variable. In contrary, it is almost never the case in real life. Usually to query the progress, you had to go through some kind of RPC, or running a tiny script. Either way it is, it already sounds a lot complicated and resource-consuming, in a worse case, some may even need to send a request through the internet for the playing progress. Depends on the precision, you might want to run the query multiple times a second. But if a single query can already take half a second, it is almost impossible to achieve a better precision this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did LyricsX do it?
&lt;/h2&gt;

&lt;p&gt;The way LyricsX solved all the problems above is really cleaver. Instead of keep asking for the player for the current progress, it maintains a timer inside itself, and try to sync it with the player by using only a few basic events.&lt;/p&gt;

&lt;h3&gt;
  
  
  State
&lt;/h3&gt;

&lt;p&gt;The program maintains a global state of the player, with the following properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Player status&lt;/strong&gt; : Obviously, this can be either &lt;em&gt;playing&lt;/em&gt; or &lt;em&gt;paused&lt;/em&gt;. For the &lt;em&gt;Stop&lt;/em&gt; state of some players, there is no track standby for playing, hence no timeline for lyrics, and thus out of our consideration.&lt;/li&gt;
&lt;li&gt;When the state is &lt;em&gt;playing&lt;/em&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal &lt;code&gt;t_0&lt;/code&gt; time&lt;/strong&gt; : This is calculated by &lt;code&gt;current time in internal clock - player progress&lt;/code&gt;, serving as a reference point of the current play session. The &lt;em&gt;internal clock&lt;/em&gt; can be anything that is fast enough to retrieve, like the system clock of the real world time, or a clock to measure code performance. We only care about the relative difference here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback speed&lt;/strong&gt; : 1.0 for normal speed, 0.5 for half speed, 2.0 for twice the speed, so on and so forth. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;When the state is &lt;em&gt;paused&lt;/em&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playback progress&lt;/strong&gt; : the progress of the player when it is being paused.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The global state is updated in the following player events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Status change (on play, on pause, on stop)&lt;/li&gt;
&lt;li&gt;Manual time change (on seek)&lt;/li&gt;
&lt;li&gt;Rate change (on rate change)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see the event set we have is pretty basic, and mostly essential for such a software to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sync the UI
&lt;/h3&gt;

&lt;p&gt;With the player state in hand, it is much simpler to get the current player progress through just a simple system call and a calculation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;progress = ( t() - t_0 ) / rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;t_0&lt;/code&gt; is a function to get the current time in the &lt;em&gt;internal clock&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Apart from that, since we now maintain a copy of the player state, we can simply assume that the player will continue the playback in the indicated speed as long as the state doesn’t change. With that, we can put more optimizations in place, such as generating animation sequence timelines ahead. All those preparation work only need to be done when the player state changes, which is much less frequent the update of playback progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  In &lt;em&gt;Lyricova Jukebox&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;In my attempt to port this logic in &lt;em&gt;Lyricova Jukebox&lt;/em&gt;, I registered the &lt;code&gt;play&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;seeked&lt;/code&gt; and &lt;code&gt;ratechange&lt;/code&gt; events, and for the internal clock, we used &lt;code&gt;performance.now()&lt;/code&gt; for its high precision and the convenience to use in &lt;code&gt;requestAnimationFrame()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the timeline, LyricsX used a seemingly reliable internal method &lt;code&gt;DispatcherQueue.schedule()&lt;/code&gt; provided by Apple. However, the scheduling function in web – &lt;code&gt;window.setTimeout()&lt;/code&gt; – does not have the honor to have a high enough precision for us. We then resorted to have a &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop running when the media is playing. Thus, to start a loop when the player state turns to &lt;em&gt;Start&lt;/em&gt;, and stop the loop when it turns to &lt;em&gt;Stop&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;requestAnimatioFrame&lt;/code&gt; loop, we check for the current time against the end time of the current keyframe. Since the loop is run every single frame in the browser, we keep the logic in the loop simple, and only advance a keyframe when the current frame ends. The frame pointer will only be updated in the hooks mentioned above to make sure the pointer is always pointing to the right frame.&lt;/p&gt;




&lt;p&gt;If you are interested in the implementation details, you can take a look at the source code of LyricsX and its submodule MusicPlayer which is responsible for translating events from different players.&lt;/p&gt;

&lt;p&gt;Alternatively, if you are more comfortable with React and TypeScript, I have ported this logic to my Lyricova Jukebox, which operates on the &lt;code&gt;&amp;lt;audio /&amp;gt;&lt;/code&gt; tag of the browser.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ddddxxx/LyricsX"&gt;ddddxxx/LyricsX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ddddxxx/MusicPlayer"&gt;ddddxxx/MusicPlayer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/blueset/project-lyricova/blob/9a46256088e688a0cb9366e66c1f21220c717c43/packages/jukebox/src/frontendUtils/hooks.ts#L152"&gt;blueset/project-lyricova: &lt;code&gt;usePlayerState()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Acknowledgement
&lt;/h2&gt;

&lt;p&gt;Thanks a lot to &lt;a href="https://github.com/ddddxxx"&gt;ddddxxx&lt;/a&gt;, the main maintainer of LyricsX, for the brilliant idea and implementation of the software.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2021/01/06/how-lyricsx-keeps-track-of-progress-of-media-players/"&gt;How LyricsX keeps track of progress of media players&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>swift</category>
      <category>typescript</category>
      <category>lyrics</category>
    </item>
    <item>
      <title>Tonguess: Solver and Traffic Analysis</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Thu, 02 Jul 2020 09:03:33 +0000</pubDate>
      <link>https://dev.to/blueset/tonguess-solver-and-traffic-analysis-3okb</link>
      <guid>https://dev.to/blueset/tonguess-solver-and-traffic-analysis-3okb</guid>
      <description>&lt;p&gt;Similar to the &lt;a href="https://blog.1a23.com/2019/08/29/rwg-an-experiment-on-semi-automated-ai-agent-for-genkai-shiritori-mobile/"&gt;last post I did on a mobile game&lt;/a&gt;, this time again is another online battle game from Baton and QuizKnock — &lt;em&gt;Tonguess&lt;/em&gt; (&lt;a href="https://play.google.com/store/apps/details?id=com.baton.tonguess"&gt;Android&lt;/a&gt;, &lt;a href="https://apps.apple.com/AU/app/id1506867759?l=en"&gt;iOS&lt;/a&gt;). In short, Tonguess is a word version of &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Bulls_and_Cows"&gt;Bulls and Cows&lt;/a&gt;&lt;/em&gt;, once &lt;a href="https://youtu.be/CDhXXbUWoO4"&gt;played by members of QuizKnock in a video&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to play
&lt;/h2&gt;

&lt;p&gt;Rules of &lt;em&gt;Tonguess&lt;/em&gt; is simple: there are 2 players in a game, each player first comes out with a heterogram (word with no repeated letters) word in the dictionary of the game app. Length of the word to guess is decided prior to the game – either 3 letters or 4. Then they shall take turn to make guesses of the word of their opponent. Once a player makes a guess, the app will tell you how close you are to the answer. It will tell you 2 numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Touch&lt;/em&gt; (a.k.a. &lt;em&gt;cow&lt;/em&gt;, &lt;em&gt;eat, B&lt;/em&gt;) is the number of letters you got correct but in the wrong place.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Get&lt;/em&gt; (a.k.a. &lt;em&gt;bull&lt;/em&gt;, &lt;em&gt;bite, A&lt;/em&gt;) is the number of letters you got correct and &lt;strong&gt;also&lt;/strong&gt; in the right place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite the word you choose at the beginning must be in the dictionary, you can use any combination of letters&lt;/p&gt;

&lt;p&gt;Once a player guessed the word correctly in less rounds than their opponent, the game is won by the player. Similarly, it will be a draw round if both players got the word of each other in the same number of guesses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/06/tonguess-src0.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mSj8toF5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/06/tonguess-src0.png" alt="" width="349" height="620"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/06/tonguess-src1.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5t4EtYA0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/06/tonguess-src1.png" alt="" width="349" height="620"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
Screenshots of the game, taken from the Google Play Store.



&lt;h2&gt;
  
  
  Solving the game
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The dictionary
&lt;/h3&gt;

&lt;p&gt;Dictionary plays a great part throughout the gameplay of &lt;em&gt;Tonguess&lt;/em&gt;. The custom in-game keyboard used in both setting and guessing words provides strong visual hints on what words are in the dictionary. In the game settings menu in the home screen, there is even an option for you to go through all words in its dictionary and redirect you to a Google search on tap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164711.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bm_ZgFqB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164711-512x1024.png" alt="" width="512" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164720.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a5zcHMSC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164720-512x1024.png" alt="" width="512" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164724.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LuVq33nf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164724-512x1024.png" alt="" width="512" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164728.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IaiOZoX---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164728-512x1024.png" alt="" width="512" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164733.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Dmj8201e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/Screenshot_20200702-164733-512x1024.png" alt="" width="512" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
Screenshots showing how the keyboard changes when typing to show the available words in the dictionary



&lt;p&gt;Hence, effectively every player can get hold of the dictionary and know whether a word is valid or not. What’s even better is that, after unzip the Android APK file, somewhere hidden in a binary file in the assets folder is the entire word list. That has later saved me a lot of time cracking the game.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gameplay
&lt;/h3&gt;

&lt;p&gt;As introduced previously, the strategy is pretty static. Apart from the answer to guess, nothing really changes from game to game, all other information like the word size and the dictionary are known prior to the games. It’s completely possible to build a single decision tree out of what we already have and use it across all future games.&lt;/p&gt;

&lt;p&gt;While I was reading on relevant materials online on solving similar games, all of them were solving the number version of the game, which has the same &lt;em&gt;range&lt;/em&gt; and &lt;em&gt;domain&lt;/em&gt;, i.e. what you can guess is exactly what they can put as their answers. In this case, the path exploration process is more “symmetrical”, it needs to focus more on the number patterns in order to narrow down the candidates. Also, in this case, the exact number combination you start with doesn’t really matter, as wherever you start with a certain pattern, you can explore with the same strategy.&lt;/p&gt;

&lt;p&gt;However, in &lt;em&gt;Tonguess&lt;/em&gt;, the &lt;em&gt;range&lt;/em&gt; and &lt;em&gt;domain&lt;/em&gt; is not the same. The &lt;em&gt;domain&lt;/em&gt; – what you can guess – is the entire &lt;em&gt;n&lt;/em&gt; letter combination without replacement, &lt;code&gt;26 choose n&lt;/code&gt;; while the &lt;em&gt;range&lt;/em&gt; – what you can put as an answer – is only limited to the number of entries in the dictionary.&lt;/p&gt;

&lt;p&gt;Running through the binary file I found in the &lt;em&gt;Tonguess&lt;/em&gt; APK with all dictionary words with a few Linux commands, I then got 2 files: one for those in 3 letters, and the other for 4 letters. They each had 711 words and 1799 words respectively, which is really not a lot comparing to the &lt;em&gt;domain&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In this way, it probably makes more sense to use other strategies to build the decision tree statistically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the decision tree
&lt;/h3&gt;

&lt;p&gt;At the beginning, I was thinking of methods like MCTS, but due to the fact that back then my access to a decent computing power (i.e. my laptop) is greatly limited, I did not have the luxury to do trial-and-error with a Monte Carlo method that might not work after all.&lt;/p&gt;

&lt;p&gt;Instead, I turned to other more brute force-y methods of building the tree. My first (and by far the only) idea is inspired by binary search. For every guess sent of &lt;em&gt;n&lt;/em&gt; letters, there will be a maximum of &lt;code&gt;1+2+...+n+(n+1)&lt;/code&gt; possible feedbacks. Despite the nature of the game that more candidates are inclined to appear in those with less confirmed letters, with some even max out at 1, we want the distribution of candidates to be as even as possible for each and every guess. With my bare minimum knowledge from my &lt;em&gt;Data Processing 101&lt;/em&gt; course, I used &lt;strong&gt;standard deviation&lt;/strong&gt; of branches as the heuristic function to decide which branch to explore.&lt;/p&gt;

&lt;p&gt;The algorithm is rather straight forward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Treat every game state as a tree node, with the current candidate words for answers. 

for each node
    go through every possible guess
        find the one with the best heuristic value
        record down the guess
        then build a node for each feedback to explore later
    repeat until a tree node has 0 or 1 candidate word.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One small detail to note: when there are multiple guesses share the same heuristic value, we want to choose the one that has a &lt;em&gt;n&lt;/em&gt;-get-0-touch, as that would potentially save us one guess to win the game. The reason why we are looking at the actual candidate rather than checking if the guess is in the dictionary is that, sometimes a guess may be in the dictionary, but it could have been eliminated way up in the tree.&lt;/p&gt;
&lt;h3&gt;
  
  
  Coding it out
&lt;/h3&gt;

&lt;p&gt;After I had the dictionary sorted out, I started write functions and scripts to help generate data I need along the way.&lt;/p&gt;

&lt;p&gt;Before we get into the code, there are some terminologies I want to clarify here to better navigate you thorough the way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;candidates&lt;/strong&gt; are the words that the opponent that could possibly put as their answer to the best of our knowledge at the current state;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a guess&lt;/strong&gt; is a word we guess, or could guess in the game;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;an outcome&lt;/strong&gt; , or &lt;strong&gt;a feedback&lt;/strong&gt; , is two numbers that we get after making a guess, could be formatted as &lt;em&gt;x&lt;/em&gt;-get-&lt;em&gt;y&lt;/em&gt;-touch, &lt;em&gt;x – y&lt;/em&gt;, or (&lt;em&gt;x, y&lt;/em&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the word length&lt;/strong&gt; is the number of letters in the word we are guessing, i.e., either 3 or 4, usually denoted with &lt;em&gt;n;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the dictionary size&lt;/strong&gt; is the number of words in the dictionary of a specific word length, denoted with w_n.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first building block is &lt;code&gt;cal_outcome(guess, answer) -&amp;gt; Tuple[int, int]&lt;/code&gt;. As the name suggests, calculate the outcome from a guess and an answer. Apart from a few little quirks here and there, this is a pretty trivial function.&lt;/p&gt;

&lt;p&gt;Coming after that, I built a lookup table for candidates grouped by outcomes for every guess at the initial state. This too was simple, but leaving all guesses and candidates in their string form has already taken me a few hundred MB just for 3 letter words in the plain Python Pickle syntax. I then tried to store the guesses as their indexes (in lexicographical order), and the grouping as a &lt;em&gt;w&lt;/em&gt;-bit integer instead. (If a certain word is in that group, the corresponding bit is set to 1.)&lt;/p&gt;

&lt;p&gt;With those little tricks, the mapping size has been reduced to 8 MB and 603 MB respectively. I have also used multiprocessing to speed up process.&lt;/p&gt;

&lt;p&gt;Following that, I now can start building the decision tree. I almost completely followed the pseudo code laied out before, except using an external library to help counting number of 1s in a binary integer, and multiprocessing to parallelize the heuristic calcuation.&lt;/p&gt;

&lt;p&gt;I couldn’t be bothered to think of ways to trim edges by looking into the nature of the gameplay strategies, my laptop has waaaay more time to compute than I have to put my code on it.&lt;/p&gt;

&lt;p&gt;The time complexity of this one is about &lt;code&gt;O(26 choose n} · n² log wₙ )&lt;/code&gt;. Sounds like a lot, but we’re not working on a competitive programming problem anyway. Looking at the CPU usage history graph, the 3-letter tree took a bit over 20 minutes, and the 4 letter one took about 4.5 hours.&lt;/p&gt;

&lt;p&gt;Now we have the trees. Just one script to translate the numbers back to words, and another REPL-like script to move down the tree, it is finally ready to be played with.&lt;/p&gt;
&lt;h3&gt;
  
  
  What about entropy?
&lt;/h3&gt;

&lt;p&gt;While reading on some more materials on this, I found out that entropy could be a better heuristic than standard deviation in this specifically setup with an inclined distribution.&lt;/p&gt;

&lt;p&gt;I then duplicated my old script, replaced the heuristic function with entropy, and then a script to compare the two types of trees.&lt;/p&gt;

&lt;p&gt;Surprisingly, with the help of &lt;code&gt;numpy&lt;/code&gt;, the entropy trees were built almost twice as fast as the standard deviation one. The results of comparison shows that the 2 methods are basically on the same track, with the entropy one finishes a few dozen words earlier than the other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-2.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hjY2opQM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-2.png" alt="" width="361" height="319"&gt;&lt;/a&gt;Chart for 3-letter words&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-3.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zgzFezE7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-3.png" alt="" width="361" height="320"&gt;&lt;/a&gt;Chart for 4-letter words&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
There is an observable improvement on the overall number of guesses shifting towards the left by switching to entropy as the heuristic function.


&lt;h3&gt;
  
  
  The decision tree
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/blueset/tonguess-toolbox/master/entropy_tree.svg"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ocjbkjtx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-1.png" alt="" width="800" height="468"&gt;&lt;/a&gt;A preview of the decision tree, click for the &lt;a href="https://raw.githubusercontent.com/blueset/tonguess-toolbox/master/entropy_tree.svg"&gt;full version&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Taking a look at the tree we’ve got here, it does confirm with some of the strategies I find online to solve &lt;em&gt;Tonguess&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Iterating through all possibilities &lt;em&gt;n&lt;/em&gt; letters at a time when you have n-1 letters confirmed.&lt;sup&gt;[&lt;a href="https://blog.1a23.com/2020/07/02/tonguess-solver-and-traffic-analysis/#footnote_0_15823" id="identifier_0_15823" title="英単語ヌメロン「Tonguess」攻略・コツなど — またりんの限界日記"&gt;1&lt;/a&gt;]&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-5.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ALc_K9RD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-5.png" alt="" width="676" height="376"&gt;&lt;/a&gt;Part of the decision tree that guesses through all the &lt;code&gt;*AST&lt;/code&gt; candidates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start a game with a word containing O, A or E.&lt;sup&gt;[&lt;a href="https://blog.1a23.com/2020/07/02/tonguess-solver-and-traffic-analysis/#footnote_1_15823" id="identifier_1_15823" title="【世界最速！！！】Tonguess攻略② — しゅぎもん"&gt;2&lt;/a&gt;]&lt;/sup&gt; The generated tree started with the word &lt;em&gt;OAT&lt;/em&gt; and &lt;em&gt;TARE&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  A web solver
&lt;/h3&gt;

&lt;p&gt;Once we got the tree, which isn’t any big at all, we can use some JavaScript and a static web page to make a &lt;em&gt;Tonguess&lt;/em&gt; solver.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A3rk-GDt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-916x1024.png" alt="" width="800" height="894"&gt;&lt;/a&gt;A screenshot of the solver&lt;/p&gt;

&lt;p&gt;The solver page and all source code mentioned above are available on GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.1a23.com/tonguess/"&gt;Try the solver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/blueset/tonguess-toolbox"&gt;Source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/blueset"&gt;
        blueset
      &lt;/a&gt; / &lt;a href="https://github.com/blueset/tonguess-toolbox"&gt;
        tonguess-toolbox
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Tonguess toolbox
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Tonguess Toolkit&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Read more in the &lt;a href="https://blog.1a23.com/?p=15823" rel="nofollow"&gt;blog article&lt;/a&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Files&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;build_mapping.py&lt;/code&gt;: Build guess-feedback mappings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dict&lt;/code&gt;: Full dictionary text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dict3&lt;/code&gt;: Dictionary of 3 letter words&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dict4&lt;/code&gt;: Dictionary of 4 letter words&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entropy_tree.py&lt;/code&gt;: Build the entropy trees&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entropy_tree.svg&lt;/code&gt;: Visualization of the entropy trees&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entropy_tree3&lt;/code&gt;: Pickle of entropy tree of length 3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entropy_tree3.json&lt;/code&gt;: JSON of entropy tree of length 3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entropy_tree4&lt;/code&gt;: Pickle of entropy tree of length 4&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entropy_tree4.json&lt;/code&gt;: JSON of entropy tree of length 4&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;guess.py&lt;/code&gt;: Word guessing helper functions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;interactive.py&lt;/code&gt;: CLI solver&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mapping3&lt;/code&gt;: Mapping for 3 letter words (need to build)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mapping4&lt;/code&gt;: Mapping for 4 letter words (need to build)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;std_dev.py&lt;/code&gt;: Standard deviation helper functions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stdev_tree.py&lt;/code&gt;: Build the standard deviation trees&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stdev_tree3&lt;/code&gt;: Pickle of standard deviation tree of length 3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stdev_tree3.json&lt;/code&gt;: JSON of standard deviation tree of length 3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stdev_tree4&lt;/code&gt;: Pickle of…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/blueset/tonguess-toolbox"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;
  
  
  Further improvements
&lt;/h3&gt;

&lt;p&gt;In the future, this strategy of tree building can be further improved. Some of these potential improvements including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge trimming strategies&lt;/strong&gt;
This could significantly improve the efficiency of the current tree building algorithm. For instance, in states where multiple letters are eliminated from the candidates, all guesses including these letters could potentially be reduced into a few due to the identical result they produce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put more weight on more common words&lt;/strong&gt;
The current strategy assumes all the words appear in the same frequency, which might not be the case for human. Incorporating the frequency may increase the chance of winning when playing against real players.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore more options&lt;/strong&gt;
There are potentially more info that we can exploit, or other strategies that could solve this game even faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Traffic analysis
&lt;/h2&gt;

&lt;p&gt;As I explore further into the mechanism of the game, I realised that &lt;em&gt;Tonguess –&lt;/em&gt; despite also being a game developed under the QuizKnock brand in the same genre – is designed with quite some difference from their first game &lt;em&gt;Genkai Shiritori Mobile&lt;/em&gt; (GSM).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Tonguess&lt;/em&gt; keeps all the valid words locally, potentially due to the limited nature of dictionary, while GSM requires player to send the word up to their server to verify.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;GSM&lt;/em&gt; has a global ranking for scores and words used and resets every month. &lt;em&gt;Tonguess&lt;/em&gt; has no hint of any ranking at all, except a “rating” of your own which you can find in the app settings, and the rating of the opponent only available during the game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taking a chance, I fired up a packet inspector on my phone to capture the traffic.&lt;/p&gt;

&lt;p&gt;As usual, those connected to Google servers, potentially for Play Services and IAPs, are HTTPS/TLS encrypted and not of our interest here. Yet the other connection, to connect for the actual gameplay, is not linked to Firebase anymore like GSM, but to a server claims to be &lt;code&gt;gctok000.exitgames.com&lt;/code&gt;&lt;sup&gt;[&lt;a href="https://blog.1a23.com/2020/07/02/tonguess-solver-and-traffic-analysis/#footnote_2_15823" id="identifier_2_15823" title="000 can be any numbers from 009 to 012"&gt;3&lt;/a&gt;]&lt;/sup&gt;, in an unusual port (TCP &lt;code&gt;4531&lt;/code&gt;). After some digging, it seems that this time they are using a game networking service called &lt;em&gt;&lt;a href="https://www.photonengine.com/en-US/Realtime"&gt;Photon Realtime&lt;/a&gt;&lt;/em&gt;. More importantly, the traffic is not encrypted, and that has enabled me to take a better look at what is going on behind the game.&lt;/p&gt;

&lt;h3&gt;
  
  
  The traffic
&lt;/h3&gt;

&lt;p&gt;Photon Realtime uses a custom protocol on top of the classic TCP. Most of the traffic is ASCII data surrounded by binary components. I didn’t go too deep into decrypting those binary structure, but the ASCII payload itself is interesting enough for us.&lt;/p&gt;

&lt;p&gt;When an opponent is first matched, the metadata of player is exchanged in a big JSON object, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Player ID (UUID)&lt;/li&gt;
&lt;li&gt;Nickname&lt;/li&gt;
&lt;li&gt;Rating as a float number with a long chunk of decimal places not shown to the player&lt;/li&gt;
&lt;li&gt;Statistics

&lt;ul&gt;
&lt;li&gt;Number of rounds won, lost and drew for 3-letter rounds&lt;/li&gt;
&lt;li&gt;And that for 4-letter rounds&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Complete history of all unique 3-letter words the player has played (responsible to show the number of unique words played and the recent word history),&lt;/li&gt;
&lt;li&gt;And that for 4-letter words.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-6.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ftRR0K6R--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-6-1024x598.png" alt="" width="800" height="467"&gt;&lt;/a&gt;A part of the packet that contains the big JSON object from the opponent.&lt;/p&gt;

&lt;p&gt;It turns out, all these data are stored locally. For Android phones, they are URL encoded in one of the preference storage XMLs, and could be easily tampered with Root access. I guess, by storing the metadata of players locally, it could save a lot of server resources of the game maker, and thus reduce the operation cost. That could be why &lt;em&gt;Tonguess&lt;/em&gt; comes with no global ranking compared to GSM.&lt;/p&gt;

&lt;p&gt;After that, when the game actually starts, the game to guess was exchanged between players, again in plain text. This is actually very surprising to me. Despite that it could be a solution to avoid player missing the answer of their opponent once the connection is dropped, sending the answer in plain text enables players to cheat in the game in whatever way they like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-7.png"&gt;&lt;img alt=""&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pZTyL4yy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/07/image-7-1024x202.png" alt="" width="800" height="158"&gt;&lt;/a&gt;The packet that contains the word (ROCK) played by the opponent in a game.&lt;/p&gt;

&lt;p&gt;They can even just straight out call out the answer at the beginning and win the game straightaway. But given there is no global ranking in the game, only a rating number – only shown when playing against others and can be changed even more easily – there could really lack of a motivation for anyone to do so.&lt;/p&gt;

&lt;p&gt;Traffic after that isn’t particularly interesting anymore, just commands to update and switch the timer, delivering guesses to and fro, and heartbeat packets to ensure both players are still around (which could be what the communication library doing instead of the game devs).&lt;/p&gt;




&lt;p&gt;Overall speaking, the game design and engineering is roughly up to the standard it aims to be — intelligently triggering, but not too complicated for both developers and players. Interest of the game may drop over time, but doesn’t make it too hard for it to maintain playable for returning players.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://matarin1725.com/archives/tonguess_kotu.html#toc8"&gt;英単語ヌメロン「Tonguess」攻略・コツなど&lt;/a&gt; — またりんの限界日記&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://note.com/shugimon95/n/n2059013fd364"&gt;【世界最速！！！】Tonguess攻略②&lt;/a&gt; — しゅぎもん&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;000&lt;/code&gt; can be any numbers from &lt;code&gt;009&lt;/code&gt; to &lt;code&gt;012&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2020/07/02/tonguess-solver-and-traffic-analysis/"&gt;Tonguess: Solver and Traffic Analysis&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>analysis</category>
      <category>app</category>
      <category>algorithms</category>
      <category>network</category>
    </item>
    <item>
      <title>Why I am moving away from Disqus (totally)</title>
      <dc:creator>Eana Hufwe</dc:creator>
      <pubDate>Tue, 21 Apr 2020 19:08:31 +0000</pubDate>
      <link>https://dev.to/blueset/why-i-am-moving-away-from-disqus-totally-3kc4</link>
      <guid>https://dev.to/blueset/why-i-am-moving-away-from-disqus-totally-3kc4</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; : because their spam detection strategy is horrible.&lt;/p&gt;

&lt;p&gt;Disqus is a comment service provider that gives free services to website owners to setup comment systems. I have started using Disqus a few years ago when I first setting up websites and blogs, because of its effortless way to add a comment box to seemingly any website, and the social log in feature. All seemed so good and peaceful until a day last year.&lt;/p&gt;

&lt;p&gt;In an afternoon on the day, I got an email from Disqus suggesting my account is compromised and I need to reset my password.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hi Eana,&lt;/p&gt;

&lt;p&gt;Your Disqus account appears to have been used by an unauthorized 3rd party to post spam comments. We removed any newly posted spam comments and restored any previous posts that may have been edited.&lt;/p&gt;

&lt;p&gt;To prevent further unauthorized access, we have disabled the password for your account. To set a new password, please follow the link below:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[link goes here]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We recommend that you change your password on other services that share the same password and use strong unique passwords for each. Having a shared password across multiple services is likely how the third party was able to access your account.  A password manager like LastPass or 1Password can help prevent this type of access.&lt;/p&gt;

&lt;p&gt;Regards,&lt;br&gt;&lt;br&gt;
The Disqus Team&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;The email from Disqus&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the moment when I saw the email, just a few minutes after it arrived, I logged in and changed my password. By the time there are already tons of spams sent with my account.&lt;/p&gt;

&lt;p&gt;At that time things have already started to become weird. I tried to delete all the spam messages that the hacker sent, but all I got was the fake visual effect clicking on the delete button. Everything was back after refreshing the page. Back then I didn’t care much about that, since I thought I have recovered my account back, and they know that these spams were &lt;em&gt;unauthorized&lt;/em&gt;, I should be safe.&lt;/p&gt;

&lt;p&gt;But not soon after, when I started to comment on some of the websites using Disqus, I saw the problem. All comments, I mean &lt;strong&gt;all comments&lt;/strong&gt; , no matter how harmless they are (or they may look like), all got the “detected as spam” label next to them. I had to manually contact the site admin through other methods to get an approval. I still didn’t care much about that at the time, as I don’t really comment on blogs that much anyway.&lt;/p&gt;

&lt;p&gt;Then after a few months I started to feel like this should be a problem to be solved. I tried to look for channels to contact their support, but all I got is their community forum, which uncoincidentally happened to be yet another Disqus Channel. (Their customer support is only dedicated to enterprise users, which makes sense in someway. Surely nobody wants to waste manpower on users never gonna pay.) I tried to post my question there, and this time there are no marks about spam on the post (that mark usually appears next to the comment I send in blogs). I had my post there and hopefully looking forward to a reply from them.&lt;/p&gt;

&lt;p&gt;Months later until recently I encountered another blog using Disqus, which reminded me about its spam disaster. I went back to my profile page, and saw that forum post itself is also “marked as spam”.&lt;/p&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TQoEPZXn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/04/image-16-1024x255.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TQoEPZXn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/04/image-16-1024x255.png" alt="" width="800" height="199"&gt;&lt;/a&gt;&lt;a href="https://disqus.com/home/discussion/channel-discussdisqus/bug_reports_feedback_getting_detected_as_spam_after_recovering_account_from_unauthorized_access/"&gt;That forum post&lt;/a&gt; shown on my profile page.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Well, that’s really a bummer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I then tried to comment on a few other posts trying to get the attention of the moderator, unsurprisingly all of them are barred for spam.&lt;/p&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x1UBnB-5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/04/image-18-1024x711.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x1UBnB-5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.1a23.com/wp-content/uploads/sites/2/2020/04/image-18-1024x711.png" alt="" width="800" height="555"&gt;&lt;/a&gt;Two comments I made, manifestly legit comment, but barred as spam anyway.&lt;/p&gt;

&lt;p&gt;By looking at the &lt;em&gt;Spam&lt;/em&gt; tag in the forum, there are a lot of other users facing similar situations (obviously not the same, otherwise they can’t post there either).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://disqus.com/home/discussion/channel-discussdisqus/admin_why_are_my_posts_with_pictures_being_labeled_as_spam/"&gt;Admin: Why are my posts with pictures being labeled as “spam?”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://disqus.com/home/discussion/channel-discussdisqus/embed_detected_as_spam_53/"&gt;Embed: Detected as spam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://disqus.com/home/discussion/channel-discussdisqus/bug_reports_feedback_spam_filter_is_a_plague_on_discussion/"&gt;Bug Reports &amp;amp; Feedback: Spam filter is a plague on discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://disqus.com/home/discussion/channel-discussdisqus/bug_reports_feedback_submit_button_disapears_and_many_comments_being_marked_as_spam/"&gt;Bug Reports &amp;amp; Feedback: Submit button disapears (and many comments being marked as spam)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://disqus.com/home/discussion/channel-discussdisqus/embed_my_comments_keep_getting_detected_as_spam/"&gt;Embed: My comments keep getting detected as spam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There should be more, but I couldn’t find the search box of the forum.&lt;/p&gt;

&lt;p&gt;With that, I have switched back to the built-in comment system as I move back from Hexo to WordPress, and it makes me to think twice when commenting on other sites running Disqus, as I had to contact the site admin through other methods to get my comment approved. Sometimes I might just tell the admin what I thought.&lt;/p&gt;

&lt;p&gt;Not to mention other aspects that repel site owners from Disqus, like annoying UI/UX, ads mixed in comments, extra network request burden, reducing number of comments, bad moderation experience, user tracking, so on and so forth.&lt;/p&gt;

&lt;p&gt;If you are using a static site, there are plenty of alternatives that you can try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://utteranc.es/"&gt;Utterances&lt;/a&gt; on GitHub Issues&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gitalk.github.io/"&gt;Gitalk&lt;/a&gt; on GitHub Issues&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://posativ.org/isso/"&gt;Isso&lt;/a&gt; (self-hosted)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://schnack.cool/"&gt;Schnack&lt;/a&gt; (self-hosted)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://commento.io/pricing"&gt;Commento&lt;/a&gt; (self-hosted, or pay-what-you-want cloud hosting)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://talk.hyvor.com/"&gt;Hyvor Talk&lt;/a&gt; (freemium cloud hosting)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://commentbox.io/"&gt;CommentBox.io&lt;/a&gt; (freemium cloud hosting)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://valine.js.org/en/"&gt;Valine&lt;/a&gt; (hosted on LeanCloud in China)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://livere.com/"&gt;LiveRe&lt;/a&gt; (freemium cloud hosting in Korea)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclaimer&lt;/em&gt;: I’m just listing a few that I find interesting online. I haven’t used most of them, and I do not endorse any of them either.&lt;/p&gt;




&lt;p&gt;Lastly, despite the fact that I’m leaving Disqus in most possible ways, I’m not deleting my account. Not that I want to give it a second chance (they didn’t give one either), but I hate to see the &lt;em&gt;“comment is removed”&lt;/em&gt; label appear on top of my comment. I left them in hope that someone might find them useful, and I don’t want to disappoint people this way.&lt;/p&gt;

&lt;p&gt;Tell me what you think about Disquz and its alternatives in the &lt;em&gt;WordPress comment box&lt;/em&gt; below! If you have difficulty using this one, feel free to let me know via email, Twitter, Telegram, etc., and I’ll consider switching to yet another platform.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://blog.1a23.com/2020/04/22/why-i-am-moving-away-from-disqus-totally/"&gt;Why I am moving away from Disqus (totally)&lt;/a&gt; appeared first on &lt;a href="https://blog.1a23.com"&gt;1A23 Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>disqus</category>
      <category>spam</category>
    </item>
  </channel>
</rss>
