header
← All posts

Reverse engineering Argentum maps

by Saúl Bensach

Tagged as hello

img-description

Argentum Online is an MMORPG (Massively Multiplayer Online Role-Playing Game) developed in Argentina. It was first released in 1999 and is considered one of the first MMORPGs developed in Latin America. The game is open-source and free to play, which has led to the creation of numerous private servers and modifications by the community.

Argentum Online is set in a medieval fantasy world, and the game features 2D isometric graphics. Players can choose from various character classes, such as knights, mages, and assassins, and embark on adventures, complete quests, and battle monsters. The game also features a player-driven economy, with crafting, trading, and resource gathering playing a significant role.

Throughout the years, Argentum Online has maintained a dedicated player base, and the game has been praised for its simplicity, community-driven development, and nostalgic appeal. (GPT4 generated)

Goals

After learning to code, I always aimed to revive Argentum Online. Many have tried refactoring the codebase in various languages, and the current maintainers created Finisterra, a new AO in Java using LibGDX. However, each rewrite had unique map editors, map saving methods, and configuration files, which made it more complex. The original VB code is still maintained on GitHub

The goal is to recreate the current AO client using Unity. The choice is simple: Unity is widely used and has numerous tools, making it easier than building from scratch. While significant coding is still needed, low-level rendering code and tons of other algorithms is not required as Unity already provides all of that.

Maps

The tech that AO uses for map is what one would expect when building tiled maps, with layers, tiles, objects, etc. But everything tailored for their own needs. So here is the first problem. We have to reverse engineer everything, read the little documentation available, code, and try to understand how the map is encoded/decoded/rendered.

And then the first goal is born, try to render any map. The rabbit hole biggins…

Reverse enginering

The original maps are encoded in binary format, but the latest Finisterra project, they ported them to JSON. I will be using the JSON ones. Each map is a matrix of 100x100 tiles, if that cell does not contain any tile null is placed, otherwise we can find the folloing:

{
    "graphic": [
        6009,
        0,
        0,
        0
    ],
    "blocked": true
}

Others have a bit more data

{
    "graphic": [
        6010,
        0,
        7001,
        0
    ],
    "objIndex": 147,
    "objCount": 1,
    "blocked": true,
    "trigger": 1
}

At this moment we are just going to focus on the “graphic” field, as this provides all of the info that we need in order to render stuff Without reading any code I will make the asumption that all maps have 4 layers, and each value is an index to a TextureAtlas, and without having to look a lot we can find the following folder on GitHub Gráficos BOOM! that’s the Graphics with what appears an index as a filename.

It is a bit crazy that this was not packed on sorted in any shape or form other than just having this files like that but this was 1999 so having to load huge textures to the Graphics cards of that time, might had some issues

Now, in order to continue we need to load all graphics in unity but I will not use the ones provided in the Gráficos link as they use black as the transparency. Under the Finisterra repo everything has been converted to alpha channel. Gráficos2x

Now we need to check that our assumption was correct and the number there corresponds to the file name, but after seeing the file 8123.png another piece of the puzzle needs to be solved as this texture contains at leat 4 sprites. Now we need to split this in regions so we need to find a file that specifies all of that…

img-description 8123.png

GrhIndex

After some vscode search bar I found out many instances of something called GrhIndex, and after some more search… images.json

[
  {
    "x": 128,
    "fileNum": 1,
    "id": 1,
    "width": 64,
    "height": 64
  },
  {
    "x": 64,
    "fileNum": 1,
    "id": 2,
    "width": 64,
    "height": 64
  }
]

Second BOOM! This is what we needed for understanding everything, not we can se that fileNum corresponds to the filename and the “ID” is the index, or at least is what I believe. Having this information we can start to write some c# code to test our findings :D

Decoding

Sprite Atlas

Before working with the textures in Unity, make sure they are loaded and configured as 64x64 sprites as multiple and the good compression settings.

Unity’s JsonUtility doesn’t support JSON arrays at the root level, so we need to modify the images.json file like this:

{
    "sprites": [...]
}

Now, create the following classes:

[System.Serializable]
public class AOSprite
{
    public int x;
    public int y;
    public int fileNum;
    public int id;
    public int width;
    public int height;
}

[System.Serializable]
public class AOAtlas
{
    public AOSprite[] sprites;
}

Finally in our script added to a game object in my case I created a file called MapLoader at the moment it will be just dirty like this but it works for what I want to archieve at this moment

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class MapLoader : MonoBehaviour
{
    void Start()
    {
        string filePath = "Assets/Resources/AOAssets/images.json";
        StreamReader reader = new StreamReader(filePath);
        string jsonString = reader.ReadToEnd();
        AOAtlas atlas = JsonUtility.FromJson<AOAtlas>(jsonString);

        Debug.Log(atlas.sprites[0].x);
    }

    [...]
}

Aaaand in the console we can just see the number 128 corresponding to the first object that x has a value of 128. One thing done.

Tilemap

Next, create the following classes for the maps:

AOTileExit, unsure about what this does atm

[System.Serializable]
public class AOTileExit
{
    public int map;
    public int x;
    public int y;
}

[System.Serializable]
public class AOTile
{
    int[] graphic;
    int objIndex;
    int objCount;
    bool blocked;
    bool trigger;
    int npcIndex;
}

[System.Serializable]
public class AOMap
{
    public AOTile tiles;
}

With all of this then we can just decode from Json as before but we have one small issue… We need to read a List of Lists something that Unity does not support when the root is not an object so we need to find another way of decoding JSON, the Newtonsoft.Json library now costs 20€, so WTF, instead of that is easier to write some code and “fix” the json files.

For that I will use Elixir as it is the language that I’m most familiar with.

After creating a new Elixir project and importing the Json library. I came up with the following code

defmodule AoMapFix do
  def fix do
    path = "C:/Users/saul-/Aurum/Assets/Resources/AOassets/Maps/Map1.json"

    string =
      path
      |> File.read!()
      |> Jason.decode!()
      |> update_in(["tiles", Access.all()], &%{"tiles" => &1})
      |> Jason.encode!()

    File.write(path <> ".tmp", string)
  end
end

Is very simple the only modification that we are doing is for each list inside “tiles” we add that list inside a new field called “tiles” So the json ends up looking like this.

{
    "tiles": [
        {
            "tiles": [
                {
                    "blocked": true,
                    "graphic": [6005, 0, 0, 0 ]
                }
            ]
        }
    ]
}

It is a dirty hack but I will modifiy the whole encoding thing once the structure is well understood so at the moment we will manage with this and change a bit the code along the way.

Now we need to add a new class that will wrap the new JsonObject

[System.Serializable]
public class AOTileRow
{
    public AOTile[] tiles;
}

Finally we can execute the following:

filePath = "Assets/Resources/AOAssets/Maps/Map1.json";
reader = new StreamReader(filePath);
jsonString = reader.ReadToEnd();
AOMap map = JsonUtility.FromJson<AOMap>(jsonString);
Debug.Log(map.tiles[1].tiles[2].graphic[0]);

And on the debug console we see: 6009! Corresponding the the correct tile if we check the JSON

At this poing of time with the Atlas and the Map loaded we can render it!

Rendering

This will be the hardest part, for the moment it was just finding stuff, now we need to seriously warm up, as we need to find how AO renders the maps, the layout ordering, how to properly cut textures and finally use the Unity built in tilemap in order to handle everything else for us.

To begin with we will write first a very simple testing thingy to print the textures specified in the Atlas

For that we will modify our AOAtlas file like this:

using System.Collections.Generic;
using System.IO;
using UnityEngine;

[System.Serializable]
public class AOAtlas
{
    public AOSprite[] sprites;
    public Dictionary<int, AOSprite> dict;

    public void Load()
    {
        string filePath = "Assets/Resources/AOAssets/images.json";
        StreamReader reader = new StreamReader(filePath);
        string jsonString = reader.ReadToEnd();
        AOAtlas AOatlas = JsonUtility.FromJson<AOAtlas>(jsonString);
        dict = new Dictionary<int, AOSprite>();

        foreach (AOSprite sprite in AOatlas.sprites){
            dict.Add(sprite.id, sprite);
        }
    }

    public Sprite LoadSprite(int index)
    {
        AOSprite aoSprite = dict[index];

        Texture2D text = Resources.Load("AOassets/graficos2x/"+aoSprite.fileNum) as Texture2D;

        return Sprite.Create(text, new Rect(aoSprite.x, aoSprite.y, aoSprite.width, aoSprite.height), new Vector2(0.5f, 0.5f));
    }
}

Notice that now the “atlas” is stored in a Dictionary where the key is the ID, and the value the whole AOSprite From some place we will call the Load() function and we will have everything ready to start calling `LoadSprite(n) Now for testing to our gameobject we will add an SpriteRenderer and with the following code csharp atlas = new AOAtlas(); atlas.Load(); GetComponent<SpriteRenderer>().sprite = atlas.LoadSprite(1); We can see all of the sprites available, add that to a variable and change that on runtime Image from Gyazo This is also a great way of checking that all of the steps that we did before work as expected