It is time to look over 2019 to see if or how I have grown, what or where I put my focus, and determine whether or not I need to re-align myself for the coming year.
Blog
I migrated from BitBucket Pages to GitLab Pages for various reasons. There were a few hiccups along the way but overall it seems to have been a pretty smooth transition.
Part of my goal after the migration was to consistently produce content. Perhaps I was too ambitious with my goals/expectations. I was able to produce consistently throughout October but fell off in November due to numerous issues and frustrations with my Raspberry Pi-Hole project.
Over this next year, I would like to keep the goal of producing content regularly and personalizing and/or creating a blog theme. At this time, I want to focus on the content to be more how-to’s, and notes, and improve my communication.
Books
One of my goals for 2019 was to read 12 books. I thought a book a month was reasonable and achievable. And for the first quarter of the year, it seemed like it was going to be. I was able to make my way through the following:
I am not sure what happened after that to cause me to get off track. I started reading Getting Things Done to re-emphasize some of the techniques he provides. The irony is not lost on me that this was the book to break my trend.
Although important, I am not sure I want to have a reading goal for 2020 - except for finishing Getting Things Done.
Pluralsight Courses
PluralSight may be part of the reason I was unable to complete my reading goal for 2019. Throughout 2019, I completed the following PluralSight Courses and started several others:
I planned on creating posts reviewing each one containing my notes to keep them in a central location. Unfortunately, I was not happy with the format/quality of the few courses that I did create reviews for. While I still want to create those entries, I need to figure out how to communicate the review effectively.
I also took the C# Skill IQ Evaluation and scored in the 95th percentile with a 248. I plan on improving this score in 2020, but also want to take courses to build other skills.
Programming Languages
I have worked with C# for a decade. While I love the language, I have started to feel as though the problem space has become stagnant and repetitive. It seems as though I am not the only one feeling this way either.
This is not to say that I think .NET is dying/dead. I still very much enjoy it and have much left to learn about it - particularly with the quality of life changes .NET Core is providing. I simply want to expand my thinking and skill set and learning a new programming language may be better suited to that.
Specifically, I am considering Go and Functional Programming. Begrudgingly Pragmatically, I am considering JavaScript/TypeScript and React/React Native.
Projects
I have no shortage of projects. My problem is in finishing them and/or making them public.
One of those projects this year was my Raspberry Pi-Hole, but have not circled back to it yet. Once that project is completed, I plan on making a Raspberry Pi Development Server. The idea is for it to contain a dockerize Jenkins, Redmine, SonarQube, and/or other software used in my development lifecycle. It is portable enough that it can be brought with me on the go or can be configured with a VPN and accessed remotely.
With the announcement that Google Chrome would be making it more difficult for ad-blockers, I looked at alternatives. I tried Brave and Vivaldi, but due to several issues, I am switching back to Firefox.
I looked at Fork and GitKraken as SourceTree replacements. GitKraken is my favorite, if I only had a single account it would probably be my daily driver. Overall, Fork looks like a good replacement for SourceTree but I have not spent enough time with it. I can say its merge tool is one of the best.
For productivity, I settled on TickTick for task management and Dynalist as my work journal.
I gave up on trying to get HyperJS to work the way I wanted. Initially, I tried Windows Terminal but ended up returning to Cmder.
I am starting to use Docker for Windows but still need more exposure to using it.
Tech Setup
I upgraded my wife’s computer so she could do her design schoolwork. Given the programs, she needs to run and that she does not game too much I opted for an AMD Ryzen 3700X and NVIDIA 2070 Super. She also got a Secret Lab Omega
My computer (~10 years old) and desk are due for an upgrade this year. Additionally, we are looking to soundproof my office to get a streaming setup started.
Work
I accepted a new opportunity working on a WPF Prism application (as evidenced by blog posts and Pluralsight history).
Lacking
Going over this year I realize three areas are lacking: family, relaxation, and exercise. These are areas I will need to make time to focus on in 2020.
I have been struggling to get ArchARM set up on my Raspberry Pi. I believe I have identified the root cause of my current issue; however, I could be wrong or still have more issues to troubleshoot and resolve. Without the Raspberry Pi, I am unable to configure my router to use Pi-Hole or AdGuard Home as a network-wide ad blocker.
Fortunately, AdGuard Home has a few servers that I can use to see whether or not it blocks ads until I can get my Raspberry Pi set up. Unfortunately, there will not be any back-end dashboard to view to administer block lists or see blocked traffic. Since ads are so prevalent these days I am not worried about wondering if it is blocking ads.
Baseline
To gauge the effectiveness, a few sites have to be checked before the settings are applied.
eBay
Forbes
CNN
Ads-Blocker
Setup
The next step is to configure the router’s DNS to use AdGuard Home’s DNS servers. AdGuard Home’s Knowledge Base provides the steps to accomplish this:
Open the preferences for your router Usually you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don’t remember it, you can often reset the password by pressing a button on the router itself. Some routers have a specific application which should be already installed on your computer in that case.
Find the DNS settings Look for the ’DNS’ letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three numbers.
Enter our DNS server addresses there 176.103.130.130176.103.130.131
I have a LinkSys WRT32 router and found this under Advanced Settings→Internet Connection Settings.
All I had to do was change this to Custom and provide the DNS server addresses.
Verification
All that is left to see is how it performs using the same sites as before - using a hard refresh to prevent cached ads from being served.
eBay Verification
Forbes Verification
CNN Verification
Ads-Blocker Verification
Conclusion
It seems as though some ads are being blocked but I am surprised at how many are still being served. To me, this looks like the ad-blocking lists need to be updated but cannot say for certain without installing it myself.
I still will be loading Pi-Hole or AdGuard Home on my Raspberry Pi as this keeps my data in-house. As much as I love what I have seen of AdGuard Home’s admin dashboard, this experience is not reassuring me of its effectiveness. In the end, the more effective product will be the one I use.
One of my brothers gifted Terraria to me on Steam. Needless to say, instead of doing the things that I ought to be doing; I have been instead playing it far more frequently. At least until my world save became corrupted.
While I was playing, Terraria suddenly crashed. I figure it was in the middle of a save/backup because when I tried to load the game back up Terraria informed me that my save was corrupted (I am not sure what caused it either). I shrugged it off thinking “No big deal, I will just use the backup file.” As it turned out, the backup was not as recent as I would have liked.
I had just gotten through a particularly nasty section of sand and had no desire to repeat it. Logically, the next thing to try was to open the world save in TEdit to see if there was anything salvageable. To my surprise, I opened the world save with TEdit and was informed that TEdit could try to recover it. Cool! After it loaded the map, nothing seemed out of place, so I saved it and went on with my business of exploring.
It was not until I had an inventory full of items that I realized what TEdit had not been able to recover - chest data. All the chests in my player home were now empty. Curious, I checked other chests using TEdit. All of the chests I checked were empty too! I would later find out that about 100 of the chests were now empty.
That would not do, that was about a third of the chests in my world. Half the fun of exploring and finding a chest is the goodies you get inside it. The only option I could think of at the time was to create a new world, maybe even using the same world seed to get the same map. However, I was not sure (still am not sure either) whether chest data is randomly generated, which could mean that even using the same world seed would not result in the same items. To top it off, it would have been easier to go through the section of sand that I was trying to avoid. So the only real option was to see if I could repair the world save to get the chest items back.
Fortunately, I had been backing my save files up to Google Drive. Without these backups, there would have been no way to restore the chest data. That does not mean it was a breeze though, there were a few hiccups along the way; most of them involving the sheer amount of data included in the world save. In the end, I was able to restore my game save to a state that I consider to be close enough to where it was that I do not miss anything. Probably, I am still missing a few items, but nothing I have noticed so I do not feel like I lost anything.
Researching the Terraria Source Code
The first step of the process required parsing world save data. I had to find the chest data in the good backup save to get the list of items that I was missing. Luckily, the Terraria client is fairly easy to decompile (NOTE: The decompiled source code will not run without modifications). There could be legal implications for decompiling the Terraria client - I do not know, I did not read the End User License Agreement. Instead, I used a repository that someone else had posted from dotPeek. Using the decompiled source code I could replicate how the game client reads the world save data and compare the chests from two of my world saves.
The code I was looking for is located in Terraria/IO/WorldFile.cs and begins in loadWorld. loadWorld does some date checking for special events, checks if the file exists, and then reads some data to determine how to parse the rest of the data. Depending on this value, the code is directed to either WorldFile.LoadWorld_Version1 or WorldFile.LoadWorld_Version2, since I know that my world file is fairly recent I immediately continued to WorldFile.LoadWorld_Version2.
publicstaticintLoadWorld_Version2(BinaryReader reader) { reader.BaseStream.Position = 0L; bool[] importance; int[] positions; if (!WorldFile .LoadFileFormatHeader(reader, out importance, out positions) || reader.BaseStream.Position != (long) positions[0]) return5; WorldFile.LoadHeader(reader); if (reader.BaseStream.Position != (long) positions[1]) return5; WorldFile.LoadWorldTiles(reader, importance); if (reader.BaseStream.Position != (long) positions[2]) return5; WorldFile.LoadChests(reader); if (reader.BaseStream.Position != (long) positions[3]) return5; WorldFile.LoadSigns(reader); if (reader.BaseStream.Position != (long) positions[4]) return5; WorldFile.LoadNPCs(reader); if (reader.BaseStream.Position != (long) positions[5]) return5; if (WorldFile.versionNumber >= 116) { if (WorldFile.versionNumber < 122) { WorldFile.LoadDummies(reader); if (reader.BaseStream.Position != (long) positions[6]) return5; } else { WorldFile.LoadTileEntities(reader); if (reader.BaseStream.Position != (long) positions[6]) return5; } } if (WorldFile.versionNumber >= 170) { WorldFile.LoadWeightedPressurePlates(reader); if (reader.BaseStream.Position != (long) positions[7]) return5; } if (WorldFile.versionNumber >= 189) { WorldFile.LoadTownManager(reader); if (reader.BaseStream.Position != (long) positions[8]) return5; } return WorldFile.LoadFooter(reader); }
The WorldFile.LoadWorld_Version2 function provided me with a layout of the different sections in a world save. The data appeared to be broken out into the following sections which are read sequentially from the world save:
File Format Header
Header
World Tiles
Chests
Signs
NPCs
Tile Entities
Weighted Pressure Plates
Town Manager
Footer
Wow! That is more data than I thought there would be. It appears as though after every section a check is done to verify that the current position in the data file matches a position that is read from the WorldFile.LoadFileFormatHeader. I should be able to use the position at the corresponding index to jump directly to the Chest data section.
The WorldFile.LoadFileFormatHeader is responsible for reading the WorldFileMetadata, the sections, and an array of items known as ‘importance’. Each section position is represented as a single integer value. I am familiar with this kind of data storage technique since HTTP packets do something similar:
Using the section list from the WorldFile.LoadWorld_Version2 and the position data from the WorldFile.LoadFileFormatHeader, I could read the Chest data immediately by jumping to that position in the save file. The next question was to determine how the Chest data was stored.
privatestaticvoidLoadChests(BinaryReader reader) { int num1 = (int) reader.ReadInt16(); int num2 = (int) reader.ReadInt16(); int num3; int num4; if (num2 < 40) { num3 = num2; num4 = 0; } else { num3 = 40; num4 = num2 - 40; } int index1; for (index1 = 0; index1 < num1; ++index1) { Chest chest = new Chest(false); chest.x = reader.ReadInt32(); chest.y = reader.ReadInt32(); chest.name = reader.ReadString(); for (int index2 = 0; index2 < num3; ++index2) { short num5 = reader.ReadInt16(); Item obj = new Item(); if ((int) num5 > 0) { obj.netDefaults(reader.ReadInt32()); obj.stack = (int) num5; obj.Prefix((int) reader.ReadByte()); } elseif ((int) num5 < 0) { obj.netDefaults(reader.ReadInt32()); obj.Prefix((int) reader.ReadByte()); obj.stack = 1; } chest.item[index2] = obj; } for (int index2 = 0; index2 < num4; ++index2) { if ((int) reader.ReadInt16() > 0) { reader.ReadInt32(); int num5 = (int) reader.ReadByte(); } } Main.chest[index1] = chest; } List<Point16> point16List = new List<Point16>(); for (int index2 = 0; index2 < index1; ++index2) { if (Main.chest[index2] != null) { Point16 point16 = new Point16( Main.chest[index2].x, Main.chest[index2].y); if (point16List.Contains(point16)) Main.chest[index2] = (Chest) null; else point16List.Add(point16); } } for (; index1 < 1000; ++index1) Main.chest[index1] = (Chest) null; if (WorldFile.versionNumber >= 115) return; WorldFile.FixDresserChests(); }
Not quite as straight-forward to decipher, but still doable:
num1 represents the number of chests stored in the world.
num2 represents the number of items stored in the chest.
num3 represents the items the chest is holding.
num4 represents the overflow of items if the chest had more items than the maximum.
Each chest then has the following properties:
x represents the x-coordinate of the chest in the world.
y represents the y-coordinate of the chest in the world.
name represents the name of the chest in the world.
I found it odd that the Chest data did not have an Id property that could be used to identify specific chests. Though the x and y properties are probably sufficient in most cases, it just meant that I would have to be more careful about identifying chests.
Depending on the next value a chest can have between 0 and 40 items that have the following properties:
id represents the item id
stack represents the quantity of that item in the single item slot.
prefix represents a prefix value that affects the stats on the item.
The code deviates a little from the normal layout with items. Instead of having an Int16 represent the number of items in a chest, each slot is read. If the slot is empty a zero will be placed in that Item data location and the code must account for that. This is caused by items in chests being located anywhere within the 40 slots. By storing the data this way, the game can reduce the size of the world save file.
After reviewing WorldFile.LoadChests, I had enough information to parse the chest data.
Applying the Research
In an effort NOT to confuse myself, I tried to break the sections out a little further into methods. The code was written using LINQPad to rapidly develop the prototype for reading the data. Main is the entry point for the ‘script’. The final code can be found in Appendix A.
1 2 3 4 5 6 7 8 9 10 11 12 13
voidMain() { World GoodWorld = GetWorld(@"201708310826.wld"); }
World GetWorld(string worldPath) { using (BinaryReader worldReader = new BinaryReader(File.OpenRead(worldPath))) { return GetWorld(worldReader); } }
GetWorld takes a string file path where the world file is located. This would allow me to add a single line to get the data from BadWorld once I was satisfied that the current implementation worked as intended. Once the file was opened for reading, the data needed to be read:
1 2 3 4 5 6 7 8 9 10 11 12 13
World GetWorld(BinaryReader worldReader) { World world = new World();
The File MetaData is read to get the BinaryReader to the correct position. This could be done with math - 2x Int32 (8 bytes), 2x Int64 (16 bytes) would yield 24 bytes. However, to maintain readability instead of jumping to an arbitrary location, reading the MetaData seemed more appropriate. Especially since there is an unknown value and the structure of this location in the file could be updated or changed between versions. An exception to this ‘rule’ is made once the section data has been read since it is more obvious why and where the jump is occurring:
for (int i = 0; i < world.TotalChests; i++) { world.ChestCollection[i] = GetChestData(worldReader, itemsPerChest, overflowItems); } }
Chest GetChestData( BinaryReader worldReader, int itemsPerChest, int overflowItems) { Chest chest = new Chest(itemsPerChest) { X = worldReader.ReadInt32(), Y = worldReader.ReadInt32(), Name = worldReader.ReadString(), };
for (int i = 0; i < itemsPerChest; i++) { chest.ItemCollection[i] = GetItemData(worldReader); }
for (int i = 0; i < overflowItems; i++) { GetItemData(worldReader); }
return chest; }
Each chest can contain one or more items up to the max item. For each chest, the items are parsed in a separate method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Item GetItemData(BinaryReader worldReader) { short stackSize = worldReader.ReadInt16();
voidMain() { World BadWorld = GetWorld(@"201709010835.wld"); Console.WriteLine( "{0} Empty Chests in Bad World out of {1}", BadWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), BadWorld.ChestCollection.Length);
13 Empty Chests in Good World out of 302 145 Empty Chests in Bad World out of 302
That seems to match what I was seeing. Although I was a little surprised there were empty chests in the good world. It turned out that the empty chests in the good world came from the Hell layer and one in my player home that I was not utilizing.
The next step is to try to compare the chests in to the two worlds to see if they can be matched. If they can, a repair is possible.
Comparing Chest Data
Unfortunately, there is no way to prevent having to iterate two lists in order to compare. However, the size of the lists can be reduced by retrieving the empty chests from the bad world. It can be reduced even further by excluding chests contained within player housing. I recommend doing this because these chests have been touched by player(s) which I was more comfortable doing by hand with TEdit. It probably would not have made any difference though:
voidMain() { World BadWorld = GetWorld(@"201709010835.wld"); Console.WriteLine( "{0} Empty Chests in Bad World out of {1}", BadWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), BadWorld.ChestCollection.Length);
Console.WriteLine(); Console.WriteLine("After Merge:"); Console.WriteLine( "{0} Empty Chests in Bad World out of {1}", BadWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), BadWorld.ChestCollection.Length); }
Then each Chest from the bad world is identified and the items it should contain are added from the matching Chest in the good world.
voidMergeEmptyChests( Chest[] sourceChests, Chest[] destinationChests) { foreach (Chest destinationChest in destinationChests) { foreach (Chest sourceChest in sourceChests) { if (destinationChest.X != sourceChest.X && destinationChest.Y != sourceChest.Y) { continue; }
int sourceChestItemLength = sourceChest.ItemCollection.Length;
for (int i = 0; i < sourceChestItemLength; i++) { destinationChest.ItemCollection[i] = sourceChest.ItemCollection[i]; } } } }
The only way to identify chests in the world is based on their location. Name possibly could be used - except named chests likely mean that the player has placed these chests intentionally. As previously stated, any chests that I had placed I wanted to do manually as I needed to compare with the chests I had placed items in after I noticed the save file corruption.
After running the application I get the following output:
13 Empty Chests in Good World out of 302 145 Empty Chests in Bad World out of 302
After Merge: 13 Empty Chests in Bad World out of 302
Perfect, I was able to restore all of the wild chests back to their generated state. The last step in the process is saving the changes.
Saving Chest Data
Everything up to this point had been relatively easy, so it was only a matter of time before I came across an issue. Unfortunately, the issue did not appear until I got to the final step of saving the merged Chest data.
The Issue
Remember how Chest data is read? Or more specifically, how the item data is read?
Chest GetChestData( BinaryReader worldReader, int itemsPerChest, int overflowItems) { Chest chest = new Chest(itemsPerChest) { X = worldReader.ReadInt32(), Y = worldReader.ReadInt32(), Name = worldReader.ReadString(), };
for (int i = 0; i < itemsPerChest; i++) { chest.ItemCollection[i] = GetItemData(worldReader); }
for (int i = 0; i < overflowItems; i++) { GetItemData(worldReader); }
return chest; }
Item GetItemData(BinaryReader worldReader) { short stackSize = worldReader.ReadInt16();
The issue is that the Chest data has grown after the merge since items have been added to a Chest that require data representations for a stack size, an id, and (potentially?) a prefix. This made my plan to just overwrite the data in the Chest data section impossible because it overwrites the data in the next section and consequentially causes the section locations to be incorrect. I was able to come up with only two ways to solve this:
The first solution was to add support for parsing the entire WorldFile save into the object that is being manipulated. This would prevent any data from getting overwritten but would still require the section data to be updated. Either through mathematically calculating the size of each section given the size of each representation of the data in those sections, or by updating the section positions after each section was written.
While this is probably a more robust approach in the long run, it would require a lot of effort on my part to build all of the representations for the data in each section and the corresponding code to read it.
Partition WorldFile
The second solution would segment the file into ~5 partitions. Three partitions for the data that is not changing, and two partitions for the data that needs updated (section locations and Chest data). In this way the data is accounted for without actually defining a representation for each piece of the data like the previous solution would have had to. Keep in mind that this solution is only worthwhile because the data changes that were made made were local to the Chest data section. If this were not the case, 2n+1 (where n is the number of sections) would need to be updated:
Partition Name
Partition Type
MetaData
Original
Section Location
Modified
2x Sections
Original
Chest Data
Modified
Remaining Sections
Original
Solution Implementation
I chose the second solution as it seemed like the easier of the two for what I was trying to accomplish. If this wasn’t a one-off project, I would suggest the first solution. To implement the second solution, all of the data from the file needs to be read. Fortunately, the data that I am not interested in can be read into a byte array. Then during the save process, the original data can be preserved by writing the contents of these byte arrays. The GetWorld function then gets updated to look like this:
Now that I had all of the file data in a mechanism that prevents data loss, I could write the function to save the world file and call it from Main after the Merge.
voidMain() { World GoodWorld = GetWorld(@"201708310826.wld"); Console.WriteLine( "{0} Empty Chests in Good World out of {1}", GoodWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), GoodWorld.ChestCollection.Length);
World BadWorld = GetWorld(@"201709010835.wld"); Console.WriteLine( "{0} Empty Chests in Bad World out of {1}", BadWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), BadWorld.ChestCollection.Length);
Console.WriteLine(); Console.WriteLine("After Merge:"); Console.WriteLine( "{0} Empty Chests in Bad World out of {1}", BadWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), BadWorld.ChestCollection.Length);
voidSaveWorld(World world, string worldFileSave) { using (BinaryWriter binaryWriter = new BinaryWriter( File.Create(worldFileSave))) { SaveWorld(binaryWriter, world); } }
Just like before, this is just a utility method around the SaveWorld method that actually does all the work. The worldFileSave property is the location to save the data, I recommend making it different than the original file to prevent further data loss!
This method is the exact opposite of the GetWorld method with a few extra modifications. The sectionPosition is used at the end to update the section offsets after all the data has been written since the size of the data section will be known at that point. The newChestDataSectionPosition is the local variable used to update the current section data to the new updated values. The writer then moves back to the section offset location stored in sectionPosition and writes the new section data. Each write method defined in this method is the inverse of the corresponding get method. The BinaryWriter will try to pad some of the values which is why the data is converted to the proper type before writing. This could probably be corrected by changing the underlying value in the corresponding object to the exact value needed:
1 2 3 4 5 6 7
voidWriteWorldMetaData(BinaryWriter worldWriter, World world) { worldWriter.Write((Int32)world.Version); worldWriter.Write((Int64)world.TypeCheck); worldWriter.Write((Int32)world.Revision); worldWriter.Write((Int64)world.UnknownMetaData); }
No scary code here.
1 2 3 4 5 6 7 8 9
voidWriteWorldSections(BinaryWriter worldWriter, World world) { worldWriter.Write((Int16)world.SectionCount);
The value of 0 needs to be explicitly cast down to Int16 as it is by default an Int32. This was causing my file to be larger than it needed to be at first and prevented it from being loaded.
Results
Once the file has been written to disk, it can be loaded in TEdit or Terraria to see if it can be parsed.
Alternate Solutions
Here are a couple alternative approaches to the one outlined above.
TEdit
It would have been faster to reference the TEdit executable and reuse the code they have written to parse the world file data for each instance of the save. This would have been like the first solution except I would be relying on TEdit’s implementation of it instead of rolling my own. The only code that would have needed to be written is MergeEmptyChests.
Terraria
Similar to the previous solution, it may be possible to reference the Terraria executable and reuse the code to load worlds. This would have made the first solution redundant and in hindsight is probably what I should have done in the first place. The save functionality probably does not have a Validate method check like TEdit does though.
Summary
In the end, was it worth it?
Probably not. Honestly, it would have been faster to go through the sand pit again than to research and code a solution like this. Granted, at the time I had no idea I had lost anything. At least I was able to recover from my mistake and learn something in the process.
voidMain() { World GoodWorld = GetWorld(@"201708310826.wld"); Console.WriteLine( "{0} Empty Chests in Good World out of {1}", GoodWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), GoodWorld.ChestCollection.Length);
World BadWorld = GetWorld(@"201709010835.wld"); Console.WriteLine( "{0} Empty Chests in Bad World out of {1}", BadWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), BadWorld.ChestCollection.Length);
Console.WriteLine(); Console.WriteLine("After Merge:"); Console.WriteLine( "{0} Empty Chests in Bad World out of {1}", BadWorld.ChestCollection .Where(chest => chest.ItemCollection .All(item => item == null)) .Count(), BadWorld.ChestCollection.Length);
for (int i = 0; i < world.TotalChests; i++) { world.ChestCollection[i] = GetChestData( worldReader, itemsPerChest, overflowItems); } }
Chest GetChestData( BinaryReader worldReader, int itemsPerChest, int overflowItems) { Chest chest = new Chest(itemsPerChest) { X = worldReader.ReadInt32(), Y = worldReader.ReadInt32(), Name = worldReader.ReadString(), };
for (int i = 0; i < itemsPerChest; i++) { chest.ItemCollection[i] = GetItemData(worldReader); }
for (int i = 0; i < overflowItems; i++) { GetItemData(worldReader); }
return chest; }
Item GetItemData(BinaryReader worldReader) { short stackSize = worldReader.ReadInt16();
For most small websites today, website speed and server security trump dynamic content. Once a small website has established a theme, it is unlikely to change. And even when dynamic content is needed; such as comments or contact forms, some services allow this dynamic content to be embedded using JavaScript. This is exactly why static site generators have become all the rage for personal and/or developer blogs. Enough where the number of options can be overwhelming, just take a look at the size of the scrollbar on StaticGen when no filters are applied.
Three Reasons Why I Chose NodeJS
WordPress has been the de-facto choice for years. They even know how much of the internet is built using WordPress. I used to use WordPress too, but only because I felt like I had no other choice. I was happy to finally have a way to get away from WordPress. Although their new Ghost blogging platform was a candidate I was considering it was eventually discarded due to some of the security concerns I had about it.
The first step to deciding which static site generator to use is to narrow it down by programming language. Conveniently, this is also a filter that StaticGen provides. The most popular ones that I had heard about were Jekyll (Ruby), Hugo (Go), Hexo (JavaScript), Pelican (Python), and DocPad (CoffeeScript). I could eliminate a few already based on my previous experience with the languages they used: namely Ruby and Go.
This left me with three potential language choices. Since I had some experience with Python from my college days I thought I would try something new. And it would probably be a good thing for me to try to get over my blatant hate for all things JavaScript. This led me to the decision that the blogging framework had to be built with NodeJS. Unfortunately, JavaScript has several different flavors and StaticGen does not allow you to select multiple languages at a time. At least not by default.
Getting A List of Frameworks
I dug through the source code for StaticGen and found that I could use Developer Tools to select multiple languages at once. The edit turns the option value from this:
A separate option could also have been added, but I only wanted JavaScript-based frameworks. The resulting list was still fairly large, but it was much better than before. To reduce the list further I made the rule that I would only consider projects that had more than 500 stars - which would allow me to test a few that were under DocPad. DocPad was the deciding factor because it was the lowest of the frameworks that I had heard about.
I had a pretty thorough plan that I was going to try each framework and rate it in its ease of installation, ease of use, theme support, and upgradeability. Honestly, there were still too many frameworks for this kind of analysis. However, I did go through them all and gave each framework an attempt to win me over. I may make a post with the results of this analysis.
Six Reasons Why I Chose Hexo
Hexo turned out to be the ideal blogging framework for me, and here are the reasons why:
1. Hexo Knows What It Is and What It Is Not
Hexo sells itself as:
A fast, simple, and powerful blog framework
Which is exactly what I was looking for. At this time, I only need a blogging platform for publishing blog posts. Should I ever need additional functionality, I may consider an alternate framework.
A lot of the other top frameworks on StaticGen are build tools that are better for sites that are not specifically designed to be blogging platforms. This is great for the amount of flexibility and control but is more than I am looking for at this time.
2. Hexo Keeps It Super Simple
Everything about Hexo was easy.
[x] Installation
[x] Setup
[x] Installing Themes
[x] Installing Plugins
[x] Custom Theming
And the documentation is thorough enough that should any questions arise, the answer can likely be found in it. The only time I became lost was when I tried to go through the source code to understand how it was passing variables from the configuration files to the plugins while trying to set up another framework to do something similar.
3. Hexo Is Still Actively Maintained
Hexo is still being actively developed/maintained. Both in the forms of themes, and plugins; but also the core repository.
4. Extensible
Hexo comes with sane default values that will work out of the box, but these can also be changed using the plugin library. Alternatively, entire themes can be applied that have been built by other people if you do not want to do any of the work yourself.
5. Cross-Platform Support and Baked In Package Manager
Not specific to Hexo, but to NodeJS - but it still is a benefit to using Hexo as opposed to creating a custom static site generator. But even if Hexo does not have a plugin for a particular technology, a plugin can be created and added to npm.
6. Pluralsight Author Endorsed
By far the weakest reason on this list, but I still included it here because it provides more resources to learn about Hexo.
In the video course the course author advocates for DocPad, but on Twitter, the course author has switched to Hexo.
Conclusion
Hexo might not be for everybody, but it met (and exceeded) the criteria that I was looking to fill at this time. Other frameworks would have been ideal in other situations - but for getting a blogging platform out quickly Hexo just made sense.