Planeta ANSOL

Os conteúdos abaixo são artigos de blogues de sócios da ANSOL agregados automaticamente, e são da inteira responsabilidade dos seus respetivos autores. Assim sendo...

  • não representam nenhuma opinião ou posição formal da ANSOL mesmo que possam coincidir ou a sua autoria ser de membros dos órgãos sociais;
  • não podem ser censurados ou modificados quaisquer conteúdos devendo tais solicitações ser orientadas aos respetivos blogues agregados, bem como tais solicitações poderão eventualmente ser expostas publicamente em repúdio.

Conversas em Código Episode #32 released

Publicado por Hugo Peixoto em 28 de Setembro de 2021 às 01:00

I’ve published a new episode of Conversas em Código (in Portuguese):

Episode 32: Outubro e Software Livre

Outubro está a chegar, e é mês de Software Livre. Neste episódio falamos do Hacktoberfest e de alguns eventos e projectos da ANSOL.

See the full episode list here:

https://conversas.porto.codes

Conversas em Código Episode #31 released

Publicado por Hugo Peixoto em 27 de Setembro de 2021 às 01:00

I’ve published a new episode of Conversas em Código (in Portuguese):

Episode 30: Participação na RustConf 2021

Neste episódio falamos sobre a experiência do Peixoto em fazer uma apresentação na RustConf 2021.

See the full episode list here:

https://conversas.porto.codes

My talk at RustConf 2021 is now available

Publicado por Hugo Peixoto em 20 de Setembro de 2021 às 01:00

It’s 14 minutes long, and you can check it out on youtube:

Identifying Pokémon Cards

Here are some related resources:

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

Status update, August 2021

Publicado por Hugo Peixoto em 6 de Setembro de 2021 às 01:00

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

This month I took a break from Cyberscore to work on other projects. I was all in on Pokémon stuff. I also took a week of vacations, which is why this update is a bit late.

August accomplishments

I worked on extracting the pokédex flavor texts from the 3DS game ROMs (https://github.com/hugopeixoto/ctr-rs) and wrote a blog post of the process.

Before working on the pokédex extraction thing, I worked on a quick and dirty tool to OCR the pokédex flavor entries from Pokémon cards, to help seed content on https://pkmncards.com. This is available here: https://github.com/hugopeixoto/ptcg-ocr/

I’ve also spent around a week’s worth of time polishing and rehearsing for my talk for RustConf. Initially I was going through the technical details of a bunch of algorithms, but being limited to 15 minutes, I had to cut a lot and really focus on the essentials. It was an interesting experiment, but it took forever. It’s the first talk I’m giving outside of Porto codes, so that’s going to be fun. I hope people like it.

September plans

My to-do list for September is kind of huge, not sure how I’m going to fit everything in.

First, there’s the actual RustConf talk, scheduled for the 14th.

There are a couple of https://cyberscore.me.uk important bugs that need fixing, and I’d like to add user registration to https://fzerocentral.org, I haven’t worked on it in a while.

AlumniEI’s Mentorship program is supposed to launch this month, so I’ll need to spend some time on it.

I also volunteered to work on a project for D3 and a couple of ANSOL projects this month.

I wanted to work on extracting the pokédex flavor texts from the Switch games and add them to Veekun, but I think it will have to wait. I volunteered to do a C++ related talk in November, but I’ll probably only start working on it in October.

I’m getting stressed just looking at this list. I’ll probably start with Cyberscore and the mentorship program, since they’re closed scope tasks and kind of time critical, and then move to the D3 and ANSOL ones, since they’re are a bit more squishy and lengthy. Not sure where I’m going to fit F-Zero Central. Getting this blog post out of the way is already a great help kickstarting things.

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

Data-mining 3DS Pokémon games

Publicado por Hugo Peixoto em 18 de Agosto de 2021 às 01:00

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

I’ve spent the last couple of weeks learning how to data mine Nintendo 3DS Pokémon games (data-mine as in “extract information from”, not “discover patterns from large amounts of data”). I’m not even sure how to explain how I got here, so I guess I’ll start from the beginning.

Pkmncards and OCR

Last month I worked on detecting pokémon TCG cards using rust (I’ll be speaking about it at rustconf 2021). I needed to grab a dataset of all the cards for that, so I scraped PkmnCards and started hanging out in their discord. This month a new TCG set comes out (Evolving Skies), and they’ll need to upload the new cards. From what I understand, most of the work is automated, scraped from the PTCGO client.

There’s one part that is manual, though. Each Pokémon card has a small Pokédex flavor text in the bottom-right corner (it has no effect on gameplay). This text is not available as structured data on the PTCGO dump, it’s only present in the card image. Luckily, the text always matches a pokédex entry for that species in one of the main games. Take these two Pikachu cards: Pikachu from Cosmic Eclipse and Pikachu from Crimson Invasion. They read, respectively:

Its nature is to store up electricity. Forests where nests of Pikachu live are dangerous, since the trees are so often struck by lightning.

A plan was recently announced to gather many Pikachu and make an electric power plant.

The first one is the pokédex entry in Pokémon Ultra-Sun, while the second one is from Pokémon Sun. Even though we don’t know which entry a card will have, we do know what the possible entries are. Running an OCR program (tesseract) on that section of the card, after some preprocessing with imagemagick, gives us a pretty close approximation of the text, so we can take that and search the closest match from a database of all known entries. I grabbed a pokédex entry dataset from Veekun and implemented a basic string matcher:

1 2 3 4 5 6 7 8 # find the entry that contains the most number of words of the OCR text. def find_match(ocr_text, entries) ocr_words = ocr_text.split(/[.,\s]+/) entries.max_by do |entry| entry_words = entry.split(/[.,\s]+/) ocr_words.count { |word| entry_words.include?(word) } end end

I had to tweak the image preprocessing quite a bit. Tesseract works by first transforming the image to binary (black and white), and different pokémon cards have different background and foreground colors, so the quality of the binarization varied wildly. By making the binarization in the preprocessing step based on the palette of the card, the results improved a lot. The code for this is available here: https://github.com/hugopeixoto/ptcg-ocr/

The whole process was working fine, except for Galarian and Alolan forms, which seemed to match a random pokédex entry. That’s when I discovered that Veekun doesn’t have pokédex entries for alternate forms. It only contains a single entry per species/game pair. This is problem for other species like Castform, Oricorio, Urshifu, etc, which also have multiple entries depending on their form.

Adding these entries to Veekun felt like a good contribution to make, so I started looking into it. I’ve messed with their codebase in the past, but I needed to get the information from somewhere. There are plenty of online sources containing the pokédex entry information, but Veekun’s preferred source is the games themselves, so I started looking into extracting data from ROMs. Alternate form pokédex entries were introduced in generation 7 (Sun/Moon), probably to deal with the addition of the Alolan forms, so that’s where I started.

Data-mining 3DS roms

Going in, I knew nothing of how 3DS games are stored. Twenty years ago I explored the contents of Red and Blue ROMs, so I have some idea of the efforts and techniques involved, but the 3DS is a much more advanced system.

Eevee, the creator of Veekun, has a great introductory post to data-mining pokémon games: https://eev.ee/blog/2017/08/02/datamining-pokemon/

If any of the techniques mentioned on the rest of this write-up feel overwhelming, her post has a section going over the basics, give it a read

If you jailbreak your 3DS, you can install software that lets you dump your cartridge’s ROM. To guarantee that you dumped it properly, there is a preservation project, No-Intro, that created a database of SHA checksums of properly dumped games, so you can compare your hashes. Dumped ROMs are encrypted using AES, with the keys being stored in the handheld’s operating system. You can get these keys out of your 3DS and use them to decrypt the ROMs. The checksums of the decrypted ROMs are also available on No-Intro, so you can be double-sure that you have a good backup of the game.

There are already a bunch of tools that let you inspect and extract the contents of 3DS ROMs, but I wanted to implement something myself. I need all the excuses I can get to practice my Rust skills. These tools, though, can be useful as documentation. I found a wiki that documents most of the file formats involved, but having more than one reference is always good.

The main container format of the ROM is NCSD. It has a bunch of header info and a set of 8 partitions. Each partition is in the NCCH format. The first tool I built was to parse the NCSD header and extract each NCCH partition into its own file.

Each NCCH partition has a fixed number of sections, and the most interesting ones are ExeFS and RomFS. ExeFS contains the game’s code, while RomFS is used for file storage. The second tool I built extracted all of these sections.

Assuming that the data I’m looking for is not hardcoded, I ignored ExeFS and looked into the RomFS format. This format, IVFC, is a file system containing arbitrarily nested directories and files. There is also a data structure with a bunch of hashes which wasn’t completely documented. Since it didn’t affect the ability to extract the directory structure, I ignored the hashes and wrote another tool to extract the directory tree.

Data-mining Pokémon Sun

Up until this point, I was able to find documentation on all these formats. Now that I was looking at a particular game’s file system, I had to rely on practically undocumented tools and forum posts to understand what was going on.

Part of the filesystem has human readable names, but the most of the data is inside a directory called a whose structure look like this:

1 2 3 4 5 6 7 8 9 10 11 $ find a -type f | sort a/0/0/0 a/0/0/1 ... a/0/0/9 a/0/1/0 ... a/0/9/9 a/1/0/0 ... a/3/1/0

Not having filenames makes it a bit harder to figure out what’s going on. The size of each of those 311 files can go from a few bytes to hundreds of megabytes. I started looking at the first 4 bytes of each file, looking for magic numbers.

All of them, turns out, are GARC files. GARC is yet another container format, with one level of nesting. It has no filename information, so the extractor tool I built, when applied to GARC file a/0/0/0, generated filenames like a/0/0/0.0.0, a/0/0/0.0.1, a/0/0/0.1.0, etc. Some GARC files had only one file, while others had thousands. None of them had more than one sub-entry, though.

While I couldn’t find any documentation for these files, there are some projects that let you make modifications to your Pokémon ROMs, so I used their source code to find the file that contained the pokédex text entries.

Game text is stored in the subfiles of GARC file a/0/3/2, in yet another format, FATO. After implementing yet another parser, I was able to dump all of the strings in every subfile of a/0/3/2. Grepping for known pokédex entries, I found that they are stored in subfiles 119.0 and 120.0 (for Sun and Moon respectively, I’m assuming). Each file contains 1063 strings, where the first 803 have the dex entry of the respective national dex number (string #0 is a dummy entry, and string #25 is Pikachu’s entry). From 804 onward, we have the entries for the alternate forms, in no obvious order at first sight.

At this point, this would be good enough for my initial purpose of using them to OCR the TCG cards. But if I wanted to import these strings onto Veekun, I would have to figure out how to match the extra entries to the forms. This is where I had to do some research on my own, since I couldn’t find any information about this.

The other files inside a/0/3/2 all contain strings, so I couldn’t find anything by searching nearby files. The next step was to think of other known places where forms might be used. Base stats, for example, vary by form, and they’re easy to find because they’re well known values. As expected, the ROM hacking tool had mapped the files where base stats occur. Base stats are stored in GARC a/0/1/7, one pokémon per subfile. Those files have two other interesting fields, form_count and form_stats_index. In Pikachu’s case, its values are 7 and 950. This means that Pikachu’s 6 alternate forms have their base stats in subfiles a/0/1/7.950.0 to a/0/1/7.955.0. At first I thought that these indexes could match the pokedex text entries, but they don’t. Some Pokémon, like Silvally, has 18 forms with pokédex text entries but no alternate form stats. This led me to think that I would have to find a table specifically for pokédex entries.

I wasn’t sure what shape the data I was looking for had, and no clues where to look, so I decided to search for something that I expected to be nearby. Since it looked like this mapping was only used for pokédex related stuff, I assumed it was near other pokédex data structures, so I started looking for those instead.

Finding Pokédex-related structures

Although each pokémon species has its global pokédex number (known as the national dex), each game has its own regional pokédex, with a different ordering and only a subset of species. Squirtle, for example, is #7 on the national dex, but #232 on Johto’s regional dex, and not present at all on Hoenn’s dex. If I look at a few consecutive entries in the Alolan dex and take their national dex numbers, I will probably get a pretty distinct sequence of numbers. I know that pokédex numbers are usually encoded in 16 bits using little endian. So if I take the Alolan entries #13 to #20, I’d get the following byte sequence:

1 2 3 4 5 6 7 8 DE 02 # yungoos 734 DF 02 # gumshoos 735 13 00 # rattata 20 14 00 # raticate 21 0A 00 # caterpie 10 0B 00 # metapod 11 0C 00 # butterfree 12 A5 00 # ledyba 165

This felt like a good byte sequence to search for. Since there could be other information packed along the dex number, I built a tool to search for sequences but allowing for evenly spaced gaps. After a few rounds of optimization, I searched the whole a/ directory for matches, and it found two matches in a single file: a/1/5/2.0.0.

Why two matches? Well, Alola’s Pokédex has four smaller sub-pokédexes, one for each island in the game (Melemele, Akala, Ula’ula, and Poni). While the regional dex has 302 entries, each island’s dex has around 120 entries. Melemele’s dex is practically the same as Alola’s, except for the 120th entry. This led me to believe that those two matches were pointing to those two pokédexes. I searched for sequences unique to the other island’s dexes and they all matched exactly once in this file. With this information, and using my sequence searching tool, I was able to find the starting offset of the five pokédexes:

1 2 3 4 5 0678 / 1656: Alolan dex 0cbc / 3260: Melemele dex 1300 / 4864: Akala dex 1944 / 6468: Ula'ula dex 1f88 / 8072: Poni dex

The distance between them is way more than ~240 bytes (120 entries of 2 bytes each), so I needed to figure out what else was there. The dex numbers were contiguous, with no extra bytes between them, so it was either extra information, or other unrelated tables. Instead of trying to guess, it was time to look at the first bytes of the file to see if I could extract any information from them. Maybe I could figure out the header structure. These are the first 0x50 bytes:

1 2 3 4 5 00000000: 424c 0b00 3400 0000 7806 0000 bc0c 0000 BL..4...x....... 00000010: 0013 0000 4419 0000 881f 0000 cc25 0000 ....D........%.. 00000020: 182e 0000 6436 0000 b03e 0000 fc46 0000 ....d6...>...F.. 00000030: b885 0000 0100 0200 0300 0400 0500 0600 ................ 00000040: 0700 0800 0900 0a00 0b00 0c00 0d00 0e00 ................

I registered a few things while looking at this:

  • I could see the starting offsets of the pokédexes (0678, 0cbc, etc)
  • the first two bytes were valid ASCII, BL, a potential magic number
  • From 0x34 onwards, there was an increasing sequence of two bytes each
  • 0x34 was also on the first few bytes
  • the third byte, 0x0b, is a small number, probably a count

Using that information, the header can be interpreted like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0000: 424c # magic number 0002: 0b00 # number of tables 0004: 3400 0000 # start of 1st table 0008: 7806 0000 # start of 2nd table / end of 1st table 000c: bc0c 0000 # ... 0010: 0013 0000 # ... 0004: 4419 0000 # ... 0008: 881f 0000 # ... 000c: cc25 0000 # ... 0020: 182e 0000 # ... 0004: 6436 0000 # ... 0008: b03e 0000 # ... 000c: fc46 0000 # start of 11th table / end of 10th table 0030: b885 0000 # file size / end of 11th table 0034: 0100 ... # first table contents

With this potential header structure, I built a decoder tool that printed the nth table of the file. Tables #2 - #6 store the five regional pokédexes, but I still had to figure out what the extra bytes in each table represented.

Looking at table #2, the Alolan pokédex, I started looking for patterns. I noticed that it had 802 entries, and that after the 301st entry, the numbers started at 1 and increased up until 721, with some gaps. I sorted the table entries and discovered that each number appeared only once. This means that the extra entries are the pokémon that are not present in this pokédex, ordered by national pokédex number. I double checked the other four tables and the theory was confirmed. I don’t know why they have that information there, but it’s not what I was looking for anyway.

Table #1 contains all numbers from 1 to 802 in increasing order. It probably represents the national pokédex.

Tables #7 - #10 all have 2124 bytes. If I assume that they’re also 16 bit entries, that would give me 1062 entries per table, which is the number of forms! The numbers on table #7 follow an interesting pattern: they’re all unique, and the numbers from 0 to 802 are all there. There’s no 803 or 804, though. The following numbers are 1027, 1030, 1033, 1039, and they go all the way up to 27849. The numbers are too high to be indexes, but they’re all unique. This makes me think that maybe the 16 bits might actually be two separate fields. To represent 802 entries we’d need 10 bits, leaving six bits for whatever the other field is. 1 << 10 is 1024, which kind of fits the fact that the number following 802 is 1027. If we print the table numbers sorted first by pokedex number and then by the 6 bit field, it looks something like this:

1 2 3 4 5 6 0:00 1:00 2:00 3:00 3:01 4:00 5:00 6:00 6:01 6:02 7:00 8:00 9:00 9:01 10:00 11:00 12:00 13:00 14:00 15:00 15:01 16:00 17:00 18:00 18:01 19:00 19:01 20:00 20:01 20:02 21:00 22:00 23:00 24:00 25:00 25:01 25:02 25:03 25:04 25:05 25:06 26:00 26:01 27:00 27:01 28:00 28:01 29:00 30:00 31:00 32:00 33:00 34:00 35:00 36:00 37:00 37:01 38:00 38:01 39:00

The first thing that draws my attention is that pokédex number 25 has seven entries. This matches the fact that Pikachu has seven forms. Another number that has a lot of occurences is 201. Pokémon no. 201 is Unown, which has 28 forms. Venusaur (#3), Charizard (#6) and Blastoise (#9) all have alternate forms. This means that this table is sorting every form, unsure by what field. Let’s see who are the first entries:

1 2 3 4 5 0: 321:00 - Wailord (regular form) 1: 103:01 - Exeggutor (alternate form #1, assuming Alolan) 2: 384:01 - Rayquaza (alternate form #1, assuming Mega) 3: 208:01 - Steelix (same, Mega) 4: 382:01 - Kyogre (same, Mega)

Now, Wailord is known for being a huge pokémon, and Alolan Exeggutor is kind of a meme for being super tall. Could it be that this list is a ranking of pokémon by height? Checking Bulbapedia’s list of pokémon by height, we can see that Eternatus is the tallest pokémon, but Eternatus didn’t exist in Sun/Moon. The next tallest Pokémon is… Wailord, followed by Alolan Exeggutor, followed by Mega Rayquaza!

Knowing what table #7 means, I quickly found the meaning of tables #8 - #10:

  • table #7: pokémon ranked from tallest to shortest
  • table #8: pokémon ranked from shorted to tallest
  • table #9: pokémon ranked from heaviest to lightest
  • table #10: pokémon ranked from lightest to heaviest

Knowing this, I loaded the game on my 3DS and checked the Pokédex functionality. These four rankings match the sorting options available.

The final table has 16060 bytes. This would be 8030 entries of 16 bits, which seems like a lot. But wait.. 8030 looks like 803*10, and 803 is practically the number of pokémon (802 + 1 dummy entry, maybe?). So maybe the entries in this table have not 2 but 20 bytes each. Let’s assume that each entry has 10 fields of 16 bits each and print the first few entries, ignoring the first one (since it’s all zeros and probably a dummy entry for padding). This is what we get:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 1 2 3 0 0 0 0 0 0 3 1 2 3 0 0 0 0 0 0 3 1 2 3 0 0 0 0 0 0 3 4 5 6 0 0 0 0 0 0 3 4 5 6 0 0 0 0 0 0 3 4 5 6 0 0 0 0 0 0 3 7 8 9 0 0 0 0 0 0 3 7 8 9 0 0 0 0 0 0 3 7 8 9 0 0 0 0 0 0 3 10 11 12 0 0 0 0 0 0 3 10 11 12 0 0 0 0 0 0 3 10 11 12 0 0 0 0 0 0 3 13 14 15 0 0 0 0 0 0 3 13 14 15 0 0 0 0 0 0 3 13 14 15 0 0 0 0 0 0 3 16 17 18 0 0 0 0 0 0 3 16 17 18 0 0 0 0 0 0 3 16 17 18 0 0 0 0 0 0 3 19 20 0 0 0 0 0 0 0 2 19 20 0 0 0 0 0 0 0 2 21 22 0 0 0 0 0 0 0 2 21 22 0 0 0 0 0 0 0 2 23 24 0 0 0 0 0 0 0 2 23 24 0 0 0 0 0 0 0 2 172 25 26 0 0 0 0 0 0 3

Just by looking at the general shape of the table, it feels right. If we look at entry #25, and assume that it represents Pikachu, we see 172, 25, and 26. 25 is Pikachu’s number, 26 is Raichu’s number, so that means that 172 is probably Pichu! If we look at entries #1, #2, and #3, they’re all the same. This means that each entry in this table represents the evolution line of the pokémon in question. This would mean that the final number represents the number of elements in the evolution line. I double checked and Eevee’s line has a value of 9 and no zero entries.

I’ve gone through all the contents of a/1/5/2.0.0. On one side, it’s nice to be able to figure out everything, but I didn’t find what I was looking for.

There is another file belonging to the same GARC: a/1/5/2.1.0. This file also starts with BL, and it follows the same header structure. I can see that it has seven tables, so we just repeat the process again. Start by printing the first table. Here are the first few entries:

1 2 3 4 5 6 7 entries: 1064 / bytes: 2128 0 0 0 804 0 0 805 0 0 807 0 0 0 0 0 808 0 0 809 810 811 0 0 0 0 813 819 820 821 0 0 0 0 0 0 0 0 822 823 0 0 0 0 0 0 0 0 0 0 0 824 825 826 827 0 0 0 0 0 0

The number of entries matches the number of forms. They are mostly zeros, but the usual 3/6/9 entries are not. These match Venusaur/Charizard/Blastoise, all of which have a Mega evolution alternate form. All non-zero numbers are between than 804 and 1600. Seems good so far. The value on entry #26, Raichu, is 819. If I check what is the 819th string on the a/0/3/2.119.0 file, I get:

It only evolves to this form in the Alola region.\nAccording to researchers, its diet is one of the\ncauses of this change.

That’s is Alolan’s Raichu pokédex entry! I did the same thing for other entries and they all match, so it seems like we have a winner. For species with multiple forms, we have to follow them like a linked list:

1 2 3 4 5 6 7 table[25] => 813 table[813] => 814 table[814] => 815 table[815] => 816 table[816] => 817 table[817] => 818 table[818] => 0

Knowing this, I can map every (species number, form index) pair to the respective pokédex entry. This gets me what I was looking for. I didn’t bother to explore the other tables on this BL file yet.

Next steps

Now that I know where to get the information, I need to actually build a thing to dump it from the ROM in a structured format. I’m having some trouble figuring out the right API for this parsing library, since I don’t want to create a ton of temporary files on the file system and don’t want to load more than necessary onto memory.

When that’s ready, I’ll need to repeat the process for Pokémon Ultra-Sun / Ultra-Moon. I bought the game a couple of days ago, haven’t even started playing it. The process should be similar: from what I understand, things haven’t change that much between generations. I was able to use the same parsers to extract information from Pokémon Y, the main difference is that the files in the a/ directory get moved around.

The final step would be to start working on Pokémon Sword and Shield, for the Switch. From the little that I read about it, it should be similar, with the exception that they started using real filenames instead of the a/1/2/2 mess.

Final thoughts

I really enjoyed doing this. It’s super fun to figure these things out, looking for patterns and playing with bits. Sometimes you get stuck, but going to sleep and returning the following day helps. I had no idea what tables #7 - #10 were until I started writing this blog post and decided to push it a bit further.

I would’ve probably benefited from using some more advanced tools, like a proper hex editor. It would be nice to visually mark the file sections that I had already cracked. I kept opening irb to make decimal to hexadecimal conversions and vice versa, and looking up pokédex numbers on bulbapedia. The sequence searcher tool that I built probably already exists. I also thought of getting an emulator and running a ROM with some flipped bits to confirm my findings, but I couldn’t bother.

It bugs me that there’s no proper documentation of this information and that you have to reverse engineer other people’s reverse engineering projects. I kind of want to start a documentation project that describes the format of every known file inside the RomFS of these games. I need to investigate how people document this kind of things. Maybe I should set up a wiki.

I’m also a bit clueless about the legality of all of this, to be honest. I own every game that I mentioned here, but I didn’t name most projects I relied on on purpose, to avoid shedding any unwanted light on them. You can easily find them by searching for any file formats or keywords I used anyway.

I’m not sure if any of this will end up on Veekun, or if it’ll be used by PkmnCards, but I had fun working on it nonetheless.

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

Status update, July 2021

Publicado por Hugo Peixoto em 1 de Agosto de 2021 às 01:00

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

I spent most of July playing with computer vision stuff in rust, but also managed to get some work done on Cyberscore. I didn’t touch F-Zero Central this month.

Cyberscore

The biggest change this month on cyberscore is that we’re now email-capable again! It took a while to get AWS SES to grant us production access, but it’s working. It also meant I had to add composer to get the AWS SDK package installed, so that’s progress. I also added twig to render the email templates, so maybe I’ll end up reusing it for the actual website page templates.

The other significant thing I added was a JSON view to some pages. This was requested by someone working on a Discord bot for New Pokémon Snap scores, and it might see some changes in the future.

Finally, I removed a bunch of explicitly unused code (attic/ directory):

$ git diff master@{"1 month ago"} --shortstat 316 files changed, 4338 insertions(+), 40417 deletions(-)

I’m currently working on understanding the details of every scoreboard, trying to.. formalize its structure? The calculations related to each scoreboard are spread over multiple files by funcionality instead of by scoreboard, making it a bit hard to add new scoreboards.

Computer vision and speaking at Rustconf

I mentioned a few months ago that I was digitizing my Pokémon TCG collection. This month, I started playing around with the idea of automatically detecting cards from a video stream to make the process easier.

I built a toy project using rust, and I got accepted to talk about it at RustConf 2021! The conference will be on September 14th.

The code is available on my github account, but it’s pretty undocumented and experiment-y. I started by using a few crates with basic algorithms but ended up writing them myself to tweak and adapt them.

I didn’t bother to document anything yet, but I’ve uploaded some images of the inner workings of the algorithm if you want to see some pretty pictures: https://hugopeixoto.net/pokemon/detection/

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

Crianças de alta competição.

Publicado por Ludwig Krippal em 31 de Julho de 2021 às 14:52

Um post de Joana Amaral Dias chama atenção para o problema de Leonor Baptista, uma ginasta de 11 anos, pelas minhas contas (1), que «foi campeã nacional este ano, [...] Tem declaração da federação atestando “atleta de elevado potencial desportivo" [e] treina cinco horas por dia» (2). Por isso, defende Dias, devia ter um lugar na escola Filipa de Lencastre, que fica perto de sua casa. Dias lamenta também que o Estado não apoie a Leonor, que «o desporto que deveria estar no nervo da educação, que é tão importante para a coesão nacional, é assim tratado. As crianças, que são o nosso futuro, desprezadas. Portugal mata o sonho e a realização dos mais novos, desportistas ou não» e assim por diante. Eu acho que todas as crianças deviam ir para escolas próximas de suas casas, dentro do possível, mas discordo que o Estado discrimine crianças pelo "potencial desportivo" ou incentive que as treinem para alta competição.

A prática moderada de desporto é saudável mas o treino de alta competição não visa a saúde dos praticantes. Visa maximizar o desempenho dos atletas na competição. É para ganhar medalhas. Isto não é apropriado para crianças, cujo desenvolvimento pode ser prejudicado pelo treino excessivo e demasiado especializado. Além disso, o dever do Estado é incentivar todas as crianças à prática saudável de desporto e não o treino intensivo de algumas seleccionadas por clubes ou federações pelo seu potencial para ganhar. É uma falácia justificar o treino de alta competição de algumas crianças invocando que o desporto traz saúde a todos.

A competição cria conflitos de interesse entre o atleta e pessoas e instituições envolvidas na sua formação. Se bem que isto seja mais notório nas competições de topo, que geralmente (mas nem sempre) envolvem adultos, seria ingénuo assumir que as crianças que participam em torneios nacionais ou internacionais não estão sob pressão para ganhar. Ou que os treinadores e clubes não estão sob pressão para maximizar o número de troféus que os atletas conquistam*.

Isto é evidente nos argumentos pelos incentivos ao treino de alta competição. Não defendem o desporto pela saúde das crianças. Isso seria exigir aulas de natação para crianças com peso a mais, por exemplo. O que exigem é o treino de crianças com "potencial desportivo" para glória do clube, federação e país. É para "ganharmos" medalhas. Esta instrumentalização dos atletas é admissível com a Patrícia Mamona ou o Jorge Fonseca porque são adultos e sabem no que se estão a meter. Se querem ganhar medalhas e os portugueses querem sentir vicariamente que também ganham, todos beneficiam se o Estado ajudar. Mas com crianças é diferente.

O fabrico de ganhadores é muito ineficiente e as crianças não só são incapazes de avaliar adequadamente o custo de dedicarem anos a treinar cinco horas por dia como não conseguem perceber quão improvável é terem os benefícios com os quais são aliciadas. Se lhes mostram o exemplo da Sunisa Lee vão ficar maravilhadas com o seu percurso e motivadas a imitá-la no esforço e dedicação. Se lhes elogiarem o talento vão ficar convencidas de que podem chegar onde Lee chegou. Mas não vão perceber que por cada Sunisa Lee há centenas de crianças que sacrificaram o mesmo mas nunca chegaram às medalhas. Ou até sacrificaram mais em lesões e outros problemas. Não podemos assumir que uma criança que entra aos sete ou oito anos neste processo o faz de forma voluntária e consciente. Pelo contrário. O mais provável é que vá ao engano, com ilusões irrealistas acerca do seu percurso e empurrada por interesses que não são necessariamente os seus. Sem garantia que o sacrifício é voluntário e consciente não é aceitável que o Estado incentive a transformação de crianças em ganhadores de medalhas.

Esta minha posição tem beneficiado de algumas críticas que me fizeram. Uma é a de ser paternalista. Admito que sim mas proponho que o paternalismo é inteiramente justificado em questões com impacto na vida de crianças tão jovens. Outra é que a alta competição incentiva crianças a praticar desporto, o que é saudável. A essa crítica contraponho que a alta competição não incentiva as crianças a praticar desporto de forma saudável. Será melhor usar outros incentivos. Finalmente, há o testemunho seleccionado de pessoas que devem o seu sucesso ao treino de alta competição ou que competiram e correu tudo bem. O problema desses exemplos é o enviesamento. O enviesamento na amostragem, por seleccionar em retrospectiva os casos mais convenientes, e o enviesamento pessoal análogo a quem defende o direito de dar palmadas aos filhos porque também levou e não lhe fez mal nenhum. Se uma criança treina cinco horas por dias dos 8 aos 18 anos e mais tarde perguntarmos se valeu a pena, é de esperar alguma resistência à conclusão que mais valia ter dado prioridade à brincadeira e outras coisas. Estas críticas não resolvem o problema fundamental: enquanto é de louvar a dedicação extrema do medalhado adulto, o mesmo sacrifício vindo de uma criança de 8 anos, ou 11, ou 15, deixa a dúvida se será realmente voluntário ou se a criança está a ser manipulada contra os seus interesses por quem quer medalhas e glória. Esse risco é suficiente para justificar a mesma prudência com que lidamos em geral com tudo o que afecta as crianças.

Se uma criança quer passar cinco horas por dia a fazer ginástica, defendo que seja livre de o fazer. Se a querem treinar para ganhar medalhas, seria bom mitigar o risco da criança se sacrificar por interesses de terceiros e em detrimento dos seus mas não sei como regular esta forma de trabalho infantil. O que me parece claro é que o Estado não deve incentivar que façam isto às crianças com "elevado potencial desportivo". Não só porque o incentivo à prática de desporto devia ser para todos, e para que praticassem desporto de forma saudável, como também porque incentivos ao treino de competição iriam agravam riscos já significativos para crianças, que por serem especialmente vulneráveis são quem o Estado tem maior obrigação de proteger.

* Os meus filhos mais velhos participaram em competições quando eram pequenos. Não era alta competição. Nem perto disso. Mas mesmo assim a pressão de pais e treinadores sobre as crianças era tão grande, tão ridiculamente grande, que eu disse aos meus filhos que o importante no desporto não é vencer mas sim humilhar completamente o adversário. Os meus perceberam que era piada mas temo que para muitos era a sério...

1- Venceu na categoria de infantis em 2018 e na de iniciados em 2019, GCP, campeões
2- No Facebook

A ciência não prova.

Publicado por Ludwig Krippal em 29 de Julho de 2021 às 18:52

Em países como o nosso, os crentes religiosos resolvem a contradição entre as suas crenças e a ciência apontando que a ciência não pode provar que não existem deuses, que não há alma e essas coisas*. É uma solução modesta. Acreditar só por não se provar o contrário é pouco exigente. E se bem que isto esteja superficialmente correcto, porque a ciência não pode provar o que quer que seja, no fundamental é uma confusão acerca da prova e do que a ciência faz.

Conseguimos provar que 4 é um número par porque tudo o que é relevante para esta afirmação está sob o nosso controlo. Definimos nós o que são números inteiros, o que é a divisão, o que é um múltiplo de 2 e assim por diante, por isso 4 ser número par não depende de qualquer hipótese acerca da qual haja incertezas. Esta é uma condição necessária para se poder provar algo**. Já agora, aplica-se o mesmo a provar que 4 não é um número ímpar. Se alguém alegar que não pode provar uma negativa, está enganado. Isso não é problema nenhum. O problema é a incerteza inevitável quando lidamos com a realidade. Fora do domínio controlado da lógica e da matemática falta-nos as certezas necessárias à prova. Por exemplo, não podemos provar que a Lua existe porque pode ser uma ilusão criada por extraterrestres, podemos estar a viver numa simulação e assim por diante. Sem controlo absoluto sobre as premissas não se pode provar nada. Portanto, a ciência não pode provar a inexistência de deuses pela razão trivial de não se poder provar seja o que for acerca da realidade. Provar é só para conceitos abstractos.

Sendo fútil tentar provar hipóteses isoladas acerca da realidade, o que a ciência faz é avaliar comparativamente as alternativas em função do que contribuem para as melhores explicações. Esta inferência à melhor explicação lida sem problemas com elementos sobrenaturais, negativas e o que mais calhar. Marcha tudo. Consideremos, por exemplo, o mito grego das estações do ano. Perséfone, filha de Deméter, tem de passar metade do ano com Hades porque comeu sementes de romã quando lá esteve. Isso faz Deméter ficar triste e, como é a deusa da fertilidade, temos o Outono e o Inverno. Quando Perséfone volta a casa Deméter fica feliz e vem a Primavera e o Verão. A ciência não pode provar que esta hipótese é falsa. Tem elementos sobrenaturais, seria provar uma negativa, mas nada disso é o problema. O problema é que não se pode provar nada acerca da realidade. No entanto, a ciência pode comparar esta hipótese com a explicação alternativa de que as estações do ano são consequência do ângulo de 23.5º entre o eixo de rotação da Terra e a perpendicular ao seu plano orbital. E esta explicação é bem melhor.

O ângulo do eixo de rotação pode ser medido de forma independente do seu papel na explicação das estações do ano. Deméter, Perséfone e companhia são postuladas ad hoc para servir a explicação. O ângulo tem de ser mesmo 23.5º para encaixar na duração do dia e da noite ao longo do ano, em cada latitude, e o percurso aparente do Sol no céu. A rábula da Perséfone, Deméter e Hades pode ser adaptada de muitas maneiras. Deméter é a esposa de Hades e Perséfone é amante ou Perséfone fugiu com Hades e a mãe está zangada com ela, por exemplo. Porque entre a narrativa e os dados há uma folga enorme. A orientação do eixo da Terra explica porque é que as estações não são simultâneas nos dois hemisférios. O mito grego pode ser adaptado para explicar isso. Se calhar Deméter tem uma casa na Áustria e outra na Austrália. Mas é outra coisa que tem de ser inventada ad hoc sem confirmação independente. Por estas razões, a orientação do eixo da Terra é uma explicação melhor para as estações do ano. E é assim que a ciência nos permite rejeitar o mito grego. Não por provar que é falso, porque isso seria impossível, mas por mostrar que há explicações melhores.

É teoricamente possível que fumar faça bem à saúde e que todos os indícios em contrário resultem de uma enorme conspiração. Mas o mais plausível é que faça mal. Não podemos provar que a bruxaria não funciona mas a melhor explicação para a ineficácia observada de feitiços e previsões com búzios e cartas é que é tudo treta. É este o problema que a ciência cria aos dogmas religiosos. É um método de comparação de hipóteses que não favorece o que os religiosos gostariam que favorecesse. Por exemplo, a hipótese de termos uma alma imortal e indestrutível não só postula esta entidade sem justificação como exige muitas explicações ad hoc para o efeito observado de lesões cerebrais, que nos podem privar de memórias, impedir raciocínio, tirar vontade e agência e até alterar a personalidade. O cristianismo alega que o criador deste universo se revelou a uma pequena tribo na idade do bronze e encarnou, mais tarde, como fundador de uma seita minoritária dessa tribo. A melhor explicação para o cristianismo é que resulta do mesmo tipo de acidentes sociais e políticos que popularizaram o islão, o hinduísmo, o budismo e por aí fora, que obviamente não podem ser todas a única e verdadeira religião.

As religiões são incompatíveis com a ciência. Não se pode conciliar a procura pelas melhores explicações com a abordagem dogmática de presumir verdades. E este problema não desaparece alegando que são domínios diferentes ou que é impossível provar uma negativa porque a função da ciência não é produzir provas dedutivas. A função da ciência é explicar, que é algo muito mais abrangente e poderoso. É graças a isso que, por exemplo, agora tratamos a epilepsia com medicação em vez de exorcismos. Não por ter ficado provado que demónios não existem mas porque se encontrou explicações melhores para esta patologia.

* Em países como o Irão e a Arábia Saudita ainda recorrem a soluções medievais.
** Mas, como Gödel demonstrou, não é condição suficiente.

O vestido.

Publicado por Ludwig Krippal em 10 de Julho de 2021 às 13:37

Depois de apreciar o tronco nu do Renato Saches e criticar o vestido da Lenka da Silva, Fernanda Câncio justificou-se como quem quer sair de um buraco escavando o fundo. Explicou que o problema não é o vestido da Lenka mas «O princípio constitucional [...] da igualdade de género [e] "a prossecução de políticas ativas de igualdade entre mulheres e homens" como "dever inequívoco de qualquer governo e uma obrigação de todos aqueles e aquelas que asseguram o serviço público em geral"». É por isto que Câncio quer que o Estado combata os tais «estereótipos de género» regulando o que as mulheres vestem na TV. Mas só as mulheres. Os homens até podem aparecer em tronco nu, que Câncio aprecia, porque «os estereótipos de género não objetificam nem sexualizam os homens» (1). Estranha igualdade.

A igualdade de género que faz sentido é a igualdade perante a lei. Mas para isso bastava fazer como a igualdade de direitos em função da beleza ou da inteligência: omitir da lei qualquer referência a esses atributos. É claro que isto não impede as pessoas de discriminar feios e belos ou burros e inteligentes. Mas isso compete a cada um decidir, bem como se valoriza estas coisas mais numas pessoas que noutras. Câncio parece querer o contrário. Quer regulação intrusiva e discriminatória para combater a «visão diferenciada do que é suposto valorizar-se nas mulheres e nos homens». Que não lhe compete nem a ela nem ao Estado decidir pelos outros. Eu dou mais importância à beleza nas mulheres do que nos homens, reivindico o direito de o fazer e não reconheço ao Estado legitimidade para me regular estes valores. O papel do Estado é garantir direitos. Não é manipular preferências. Se Câncio quer igualdade nisto, então que dê ela mais importância à beleza nos homens. Eles não se vão queixar e evita usar o Estado para forçar as pessoas a terem todas os mesmos gostos que ela.

Além do abuso do poder do Estado, é fútil regular os vestidos na RTP para alterar estereótipos de género. Mesmo que Lenka vá para o Preço Certo de fato de treino e gola alta, continua a ter página no Instagram, as mulheres continuam a vestir-se como querem na rua e há partes da Internet cheias de estereótipos de género em várias posições, combinações e número de participantes. E nem adiantava censurar tudo isso, como evidencia a persistência de estereótipos de género em países onde as mulheres são obrigadas a disfarçarem-se de saco preto. A razão da futilidade está na origem destes estereótipos, que não são causados pelo vestido da Lenka mas sim por milhões de anos de evolução que criaram na nossa espécie dois sexos especializados em aspectos diferentes da reprodução, com estratégias diferentes, preferências diferentes na selecção de parceiros e comportamentos diferentes.

É por isso que não se consegue igualdade de género declarando que não há géneros. Seria o mais prático. Acabava de uma vez a desigualdade, a discriminação e essas coisas todas. Mas não funciona porque os géneros são consequência dos sexos. É algo tão profundo no ser humano, e tão resistente a propaganda e programas de televisão, que há pessoas que recorrem a tratamentos drásticos ou se suicidam por o seu corpo não corresponder ao que concebem ser o seu género. Na verdade, combater estereótipos de género é combater a identidade de género. Cada pessoa tem de conceber e generalizar características que distinguem os géneros para se categorizar e para se relacionar com os outros. Mas essa generalização conceptual exige estereótipos. O que é ser homem ou ser mulher, por exemplo. Câncio quer usar o Estado para alterar a forma como certas pessoas concebem os géneros, violando o dever de respeitar a autonomia de cada um nestas matérias.

Esta ideologia woke é inconsistente, demagógica e até hipócrita. Diz ser pela igualdade e justiça mas a igualdade que defende é injusta. Em vez de ser igualdade nas liberdades quer forçar igualdade de preferências e escolhas, que ninguém deve ser obrigado a ter igual aos outros. Combate estereótipos sem reconhecer que os estereótipos fazem parte de como cada um pensa acerca de si e dos outros. Daquela identidade que dizem defender e que não deve ser alvo de regulação governamental. Combate a sexualização das mulheres de forma claramente discriminatória (o tronco nu do homem não tem problema) e sem perceber que as competições desportivas sexualizam os homens como os vestidos curtos sexualizam as mulheres.

A "objectificação" é mais uma treta. Objectificar uma pessoa é tratá-la como um objecto, em oposição a tratá-la como um ser humano. Mas se bem que quando apreciamos a apresentadora de mini saia ou o futebolista de tronco nu não os estamos a valorizar em toda a sua dimensão humana, a atracção sexual é caracteristicamente humana. Muito mais objectificado é quem recolhe o lixo, desentope sarjetas ou desempenha tantas outras profissões nas quais as pessoas, principalmente homens, fazem papel de coisa e até seriam substituídas por máquinas se ficasse mais barato. E raramente conhecemos alguém com tal intimidade que o possamos apreciar plenamente como ser humano. É o vizinho, o colega, a professora, todos objectificados em maior ou menor grau em função da relação que tenhamos com cada um. O termo "objectificação" como Câncio o usa apenas disfarça o puritanismo desigual com que encara a sexualidade, segundo o qual ser sexualmente atraente é bom para homens mas desumanizador para as mulheres.

Esta esquerda evangélica abandonou a luta por liberdades e direitos e está fixada em usar o Estado para doutrinar. O combate aos estereótipos, a exigência de igualdade em valores e opções e a demagogia envolvente visam politizar opiniões pessoais para justificar restringi-las àquilo que declaram ser correcto. Quem preze a democracia, mesmo que se considere de esquerda, deve opor esta tentativa de eliminar a fronteira entre o Estado e a opinião de cada um.

1- DN, O preço (errado) de uma polémica

Os ciclos.

Publicado por Ludwig Krippal em 3 de Julho de 2021 às 13:06

Com o aumento de novos casos aumenta também a acusação de falsos positivos. Independentemente do aumento nos internamentos, cuidados intensivos ou óbitos, é tudo falsos positivos porque a um bom conspiracionista os factos não metem medo. Uma variante sofisticada desta alegação é a dos ciclos de amplificação nos testes de PCR. Reza a conspiração que "eles" aumentam o número de ciclos para dar falsos positivos porque o PCR não é fiável acima dos 35 ciclos. É uma boa conspiração porque não só tem os tais "eles" que nos andam a enganar como permite ao conspiracionista passar por entendido sem o trabalho de entender seja o que for. Noutras circunstâncias, esta coisa dos ciclos até teria graça.

O muco que vem na zaragatoa tem células da pessoa testada, bactérias diversas, vários vírus e sabe-se lá mais o quê. Uma forma de testar se a pessoa está infectada com SARS-CoV-2 é usar moléculas que se ligam especificamente a certas proteínas desse vírus. É o que se usa no teste rápido, que assim fica com uma marca visível apenas se a amostra contém essas proteínas do vírus. O problema dos testes rápidos é que a marca só é visível se houver proteína suficiente e, por isso, só são fiáveis num período curto após o início dos sintomas, quando a carga viral é mais alta. Para detectar infecções antes dos sintomas é preciso usar PCR, que aproveita as propriedades do ADN* para conseguir detectar o vírus mesmo em quantidades pequenas.

As moléculas de ADN são longas cadeias formadas pela ligação sequencial de quatro tipos de nucleótidos, as tais letras A, C, G, e T **. Na cadeia de ADN, estes nucleótidos ficam com umas caudas espetadas e há uma forte afinidade entre a cauda de A e a de T, e entre e a cauda de C e a de G. Uma consequência disto é a forte tendência das moléculas de ADN se colarem a moléculas complementares. Se uma tem ACTGAC... vai ficar presa a outra que tenha TGACTG... como um fecho de correr. A outra consequência desta afinidade específica é permitir que enzimas sintetizem uma cadeia de ADN a partir de uma cadeia mãe e um trecho inicial ligando pela ordem certa os nucleótidos complementares. Estas enzimas são as polimerases, o P em PCR.

A ligação entre duas moléculas complementares de ADN é forte mas não tão forte como as ligações covalentes dentro de cada molécula. Por isso, se as aquecermos conseguimos que a agitação as separe sem degradar as moléculas. E quando arrefecem ficam novamente coladas aos pares complementares, porque a afinidade das sequências de nucleótidos é muito específica. Ou seja, podemos separar e juntar os pares sempre que quisermos, bastando aquecer e arrefecer a solução com o ADN. E com isto temos praticamente tudo para o PCR. Faltam só os primers.

O propósito do processo PCR é amplificar trechos específicos de ADN se estes existirem. Dessa forma, mesmo que haja pouco vírus na amostra, com PCR conseguimos aumentar a concentração do ADN correspondente até ser detectável. Para isso vamos juntar à amostra uma quantidade grande de pedaços de ADN, sintetizados artificialmente, cuja sequência encaixa especificamente em partes do ADN viral. Quando aquecemos e arrefecemos a amostra, as moléculas de ADN vão emparelhar e, se houver lá ADN do vírus, vai ficar com esses primers agarrados. Pomos a polimerase a trabalhar e todos os primers que encontraram parceiro vão ser aumentados pela polimerase, sintetizando a partir deles cópias do ADN do vírus. Agora aquecemos e arrefecemos de novo e vamos repetindo o processo. A cada ciclo a quantidade de ADN do vírus duplica, aproximadamente, porque estamos a criar cópias, e cópias de cópias, e assim por diante.

Tipicamente, se a pessoa está infectada com este vírus, ao fim de uns vinte ciclos já há tanto ADN que se nota o sinal de fluorescência no aparelho. Nesse caso o teste deu positivo. Se não houver vírus na amostra, então não acontece nada e ao fim de quarenta ciclos acaba o teste, que nesse caso deu negativo. O tal problema dos 35 ciclos ocorre se o sinal de fluorescência surgir acima deste número de ciclos. Isso é um problema porque sugere que há vírus na amostra mas para o sinal só surgir depois dos 35 ciclos é porque alguma coisa correu mal ou a quantidade inicial de vírus era tão pequena que pode ter sido contaminação. A zaragatoa pode ter tocado onde não devia, alguém espirrou lá perto ou coisa do género. Por isso, nesses casos, manda a DGS que o teste seja repetido do início, incluindo meter outra vez a zaragatoa no nariz do desgraçado que, sem culpa nenhuma, teve um teste inconclusivo.

Em conclusão, se bem que seja verdade que acima de 35 ciclos o resultado positivo não é de fiar, é ridícula a ideia de que "eles" andam a aumentar os ciclos para dar falsos positivos. O número de ciclos depende da amostra. Se não tem nada o teste pára aos 40 ciclos e é negativo. Se houver sinal é no ciclo que calhar. Normalmente é abaixo de 35 e é positivo, mas se calhar acima repete-se. Não há um botão no aparelho para aumentar a taxa de falsos positivos.

* Em rigor, este vírus tem ARN em vez de ADN. Mas o que se faz é usar enzimas que copiam o ARN do vírus para o ADN correspondente, pelo que isto acaba por ser um detalhe irrelevante.
** Adenina, Citosina, Guanina e Timina, mas esta parte não vem para o teste.

Status update, June 2021

Publicado por Hugo Peixoto em 30 de Junho de 2021 às 01:00

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

F-Zero Central

After bringing the website back up, I worked on making it possible for existing players to reset their passwords, logging in, and submitting new times.

Time submission took a bit longer than I expected because we have a bunch of tables that cache several scores and rankings that needed to be recalculated. This used to be done as a cron job because it took too long to do inline, but I was able to make it fast enough. It took me a while to understand what each table represented and to figure out the formulas from the code and FAQ, but it seems to be working.

Part of the good news is, the code is now licensed under AGPL, and it’s available here:

https://github.com/fzerocentral/fzerocentral

Cyberscore

The most visible thing I did this month was to rewrite the login, registration and password recovery pages to be mobile friendly. Apart from that, I worked on improving the way we do html escaping. Some old translation strings were being stored in escaped form, and some functions like GetGameName was also returning an already escaped string. I normalized the database and moved all escaping to the html templates. These were all detected thanks to an effort to translate the website to japanese, given all the influx of New Pokémon Snap submissions from japanese players.

During fzero’s rewrite, I added a migration system. I want to port it to cyberscore, we’ve been doing some changes to the database and it’s kind of troublesome to keep everyone in sync without one.

Personal infrastructure

I have a bunch of HDDs in my desktop that are not encrypted. I started moving files around to get those disks empty so I can format them, but it’s taking forever. When emptying one disk, I decided to create at least two copies on different disks to improve my redundancy. This means I need to have a lot of free space available to move everything around, so this doubles the time it takes to empty a disk.

Every time I make a new copy, I double check that everything was copied successfully with a sha1sum:

1 2 3 4 5 find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha1sum | tee ../shasums.txt | sha1sum

I never detected any issues when copying files between two disks on the same machine, but one of my larger disks is in another computer in the network, so some of the files are being transfered via rsync. I transferred 2.4 TB in one go, and the checksums didn’t match. There were differences in three files, of sizes 8 GB, 93 GB, and 256 GB.

I was curious to know how many bits were corrupted, but I didn’t want to make a trivial diff over the network, since it would require copying all those gigabytes, so I ended up writing a rust cli tool that would find the differences between the two using a merkle tree to detect where the errors were. You can find the source code of netdiff here:

https://github.com/hugopeixoto/netdiff/

Using this tool, I found that the files had, respectively, 1 bit flip, 2 bit flips, 4 bit flips. I could just flip those bits to fix the issue, but I’ll rsync those files again instead.

Other stuff

I accidentally discovered Mindustry while browsing F-Droid for games. It looks great, it’s GPLv3, and I already played it for more hours than I should. I kind of want to play with setting up a multiplayer server and playing it on the desktop instead, but it’s probably better if it stays contained in my phone.

If you enjoy my work, consider sponsoring me so I can keep on doing this full time: https://github.com/sponsors/hugopeixoto

Subscreva Planeta ANSOL