Vanosilla - server-files

./server-translations contains all game-server configuration files.

Every .yaml file contains header with the comment.


⚠️ This section is mainly for developers with basic C# knowledge only ⚠️

Creating new config file - Single file

To create a config, you need to create a .yaml file (if you don't know what .yaml is, wiki is your friend).

Let's say we want to create a single file that contains some basic data:

  • Maximum upgrade of the Specialist Card is 15
  • Maximum upgrade of the equipment is 10
  • Maximum rarity of the equipment is 7

First, let's create maximum_eq_sp.yaml:

max_specialist_upgrade: 15
max_equipment_upgrade: 10
max_equipment_rarity: 7

Single file - Load file

Okay, config created and now it's time to load it to the server.

First of all, let's create a new .cs file in ./Configurations directory in WingsAPI.Game project and let's name it SpecialistEquipmentMaximum like above:

public class SpecialistEquipmentMaximum
{
}

with 3 properties:

  • MaxSpecialistUpgrade
  • MaxEquipmentUpgrade
  • MaxEquipmentRarity

All properties will be the type of byte:

public class SpecialistEquipmentMaximum
{
    public byte MaxSpecialistUpgrade { get; set; }
    public byte MaxEquipmentUpgrade { get; set; }
    public byte MaxEquipmentRarity { get; set; }
}

❗ Note

If you want to rename your properties in .yaml file to whatever you want (for example because the name is too long) just add YamlMember attribute above property:

public class SpecialistEquipmentMaximum
{
    [YamlMember(Alias = "max_sp_upgr")]
    public byte MaxSpecialistUpgrade { get; set; }
}

and change naming of the property in .yaml file:

max_sp_upgr: 15

Great! Now, it's time to load config into server. First, go to the GameManagersPluginCore.cs file in WingsEmu.Plugins.BasicImplementations project and at the end of the AddDependencies method add new line:

services.AddFileConfiguration<SpecialistEquipmentMaximum>("maximum_eq_sp");

Congratulations! You have successfully created a new config and now it's loaded in the server!

Single file - Server implementation

Now it's time for server implementation. Let's say that I want to check if the player's Specialist Card upgrade is is greater than or equal to our value in config while player wants to upgrade his Specialist Card.

The event that is responsible for upgrading the Specialist Card is called SpUpgradeEvent, so let's find the handler of this event in the solution... and we can find SpUpgradeEventHandler (if you don't know anything about events in the emulator, read Event and Event Handler section in ./server/README.md).

First, implement our config in the constructor of SpUpgradeEventHandler by using Depedency Injection:

public class SpUpgradeEventHandler : IAsyncEventProcessor<SpUpgradeEvent>
{
    private readonly SpecialistEquipmentMaximum _specialistEquipmentMaxConfig;

    public SpUpgradeEventHandler(SpecialistEquipmentMaximum specialistEquipmentMaxConfig)
    {
        _specialistEquipmentMaxConfig = specialistEquipmentMaxConfig;
    }
}

Now, let's move to the HandleAsync method at the very beginning (line ~55) and use our config:

GameItemInstance sp = e.InventoryItem.ItemInstance;

if (sp.GameItem.IsPartnerSpecialist)
{
    return;
}

if (sp.Rarity == -2)
{
    return;
}

// First, let's take current Specialist Card upgrade
byte specialistUpgrade = sp.Upgrade;

// Now, let's take maximum upgrade for Specialist Card
byte maxSpecialistUpgrade = _specialistEquipmentMaxConfig.MaxSpecialistUpgrade;

// Check if specialistUpgrade is greater than or equal to maxSpecialistUpgrade
if (specialistUpgrade >= maxSpecialistUpgrade)
{
    return;
}

and voilà, the server implementation is done by using new config, again - Congratulations!

Creating new config file - Multiple configurations in one file

Okay, let's say you want to create some lists of positions on individual maps that give you some amount of gold each time you enter that position.

First, let's create the .yaml file - I will call it give_gold_in_position.yaml and it will store the following data:

  • map_id - on which map id it will work
  • amount_of_gold - amount of gold that is given to the player
  • positions - list of the cells (X/Y)

So, let's build the .yaml:

- map_id: 1 # NosVille
  amount_of_gold: 1 # 1x Gold
  positions:
    - x: 1
      y: 1
    - x: 2
      y: 2
- map_id: 10000 # GM Room
  amount_of_gold: 5 # 5x Gold
  positions:
    - x: 10
      y: 15
    - x: 23
      y: 11

❗ Note

I recommend you to use several YAML validators to check that the .yaml file is processing correctly before starting the server:


Summing up what is above - if the player is in:

  • NosVille and steps on coordinates X: 1 | Y: 1 or X: 2 | Y: 2 he will receive 1 gold.
  • GM Room and steps on coordinates X: 10 | Y: 15 or X: 23 | Y: 11 he will receive 5 gold.

Okay, we have raw file - now it's time to create proper C# class:

public class GiveGoldInPosition
{
    public int MapId { get; set; }
    public int AmountOfGold { get; set; }
    public List<GoldPosition> Positions { get; set; }
}

public class GoldPosition
{
    public short X { get; set; }
    public short Y { get; set; }
}

❗ Note

I couldn't use Position struct in this case, because Position struct doesn't have setters in X and Y properties, so instead I created a new class GoldPosition.


Great, now it's time to load our give_gold_in_position.yaml file into GiveGoldInPosition class. This time, instead of using AddFileConfiguration method, we have to use AddMultipleConfigurationOneFile method to create a list of GiveGoldInPosition class.

services.AddMultipleConfigurationOneFile<GiveGoldInPosition>("give_gold_in_position");

Multiple configurations in one file - Server implementation

Now when our config is loaded in the memory of the server, it's time to use it. Let's enter the WalkPacketHandler class in WingsEmu.Plugins.PacketHandling project, when player is moving.

Like in the previous section of server implementation, let's implement our config to the class by using Depedency Injection:

public class WalkPacketHandler : GenericGamePacketHandlerBase<WalkPacket>
{
    private readonly List<GiveGoldInPosition> _giveGoldInPosition;

    public WalkPacketHandler(List<GiveGoldInPosition> giveGoldInPosition)
    {
        _giveGoldInPosition = giveGoldInPosition;
    }
}

After some checks inside HandlePacketAsync method, we should add our new config below session.PlayerEntity.ChangePosition method. First, let's create a new method:

public async Task CheckForGoldAsync(IClientSession session, short x, short y)
{
    // Let's take current map id from the player
    int mapId = session.PlayerEntity.MapInstance.MapId;

    // Find given map id from config
    GiveGoldInPosition goldInPosition = _giveGoldInPosition.FirstOrDefault(config => config.MapId == mapId);
    
    // Couldn't find config in giving map
    if (goldInPosition == null)
    {
        return;
    }

    // Let's find GoldPosition in our config, but the player isn't in any given cell
    if (!goldInPosition.Positions.Any(coords => coords.X == x && coords.Y == y))
    {
        return;
    }

    //Execute GenerateGoldEvent event to give player gold
    int amountOfGold = goldInPosition.AmountOfGold;
    await session.EmitEventAsync(new GenerateGoldEvent(amountOfGold));
}

and at the end, add our CheckForGoldAsync method in HandlePacketAsync method somewhere under ChangePosition method:

session.PlayerEntity.ChangePosition(new Position(walkPacket.XCoordinate, walkPacket.YCoordinate));
await CheckForGoldAsync(session, walkPacket.XCoordinate, walkPacket.YCoordinate);

Done... well, kind of. Currently the main problem of this solution is the FirstOrDefault(config => config.MapId == mapId) method - just imagine how many times we have to use this method for every player movement even if he isn't on the map given in config, let alone with the proper coordinates... the performance will cost us a lot of doing that and we want to avoid that.

The solution is... Dictionary - let's implement it.


Multiple configurations in one file - Config Manager

First we need to create a manager for our config - the interface of the config and the class to implement methods from our interface.

I will create the manager inside GiveGoldInPosition namespace with interface IGiveGoldConfig and the class GiveGoldConfig that inherits our interface:

public interface IGiveGoldConfig
{

}

public class GiveGoldConfig : IGiveGoldConfig
{

}

Great - now let's create a method that will return the GiveGoldInPosition class by giving map id:

public interface IGiveGoldConfig
{
    GiveGoldInPosition FindConfigByMapId(int mapId);
}

public class GiveGoldConfig : IGiveGoldConfig
{
    public GiveGoldInPosition FindConfigByMapId(int mapId)
    {
        return null;
    }
}

For now I will return nothing, because I didn't store any data inside this manager - to do it I need to parse our list of configs inside the constructor of the GiveGoldConfig class using Depedency Injection again. As I said previously, we're gonna use Dictionary that will store map id as key and GiveGoldInPosition class as value:

public interface IGiveGoldConfig
{
    GiveGoldInPosition FindConfigByMapId(int mapId);
}

public class GiveGoldConfig : IGiveGoldConfig
{
    private readonly IReadOnlyDictionary<int, GiveGoldInPosition> _configs = new Dictionary<int, GiveGoldInPosition>();

    public GiveGoldConfig(IEnumerable<GiveGoldInPosition> configs)
    {

    }

    public GiveGoldInPosition FindConfigByMapId(int mapId)
    {
        return null;
    }
}

As you can see I used IReadOnlyDictionary, because we're not gonna add new data while the server is running, but at startup using the GiveGoldConfig's constructor. Now, let's use ToDictionary method which will create a dictionary for us:

public interface IGiveGoldConfig
{
    GiveGoldInPosition FindConfigByMapId(int mapId);
}

public class GiveGoldConfig : IGiveGoldConfig
{
    private readonly IReadOnlyDictionary<int, GiveGoldInPosition> _configs = new();

    public GiveGoldConfig(IEnumerable<GiveGoldInPosition> configs)
    {
        // ToDictionary() will create a Dictionary from each element in configs list and map it MapI as key
        _configs = configs.ToDictionary(x => x.MapId);
    }
}

Now, we can easy implement our method:

public GiveGoldInPosition FindConfigByMapId(int mapId)
{
    return _configs.TryGetValue(mapId, out GiveGoldInPosition config) ? config : null;;
}

Now, when everything is done, let's add our config into Depedency Injection to use it later in WalkPacketHandler. To do that, just add TryAddSingleton method after AddMultipleConfigurationOneFile of our new config - the final result should look like that:

services.AddMultipleConfigurationOneFile<GiveGoldInPosition>("give_gold_in_position");
services.TryAddSingleton<IGiveGoldConfig, GiveGoldConfig>();

Now, let's move back to the WalkPacketHandler and our CheckForGoldAsync... and instead of FirstOrDefault method we will use FindConfigByMapId method - but first, replace our old config in the constructor of the config with our new config manager:

public class WalkPacketHandler : GenericGamePacketHandlerBase<WalkPacket>
{
    private readonly IGiveGoldConfig _giveGoldConfig;

    public WalkPacketHandler(IGiveGoldConfig giveGoldConfig)
    {
        _giveGoldConfig = giveGoldConfig;
    }
}

... remove FirstOrDefault method and replace with FindConfigByMapId method from our config manager:

public async Task CheckForGoldAsync(IClientSession session, short x, short y)
{
    // Let's take current map id from the player
    int mapId = session.PlayerEntity.MapInstance.MapId;

    // Find given map id from config
    GiveGoldInPosition goldInPosition = _giveGoldConfig.FindConfigByMapId(mapId);

    // Couldn't find config in giving map
    if (goldInPosition == null)
    {
        return;
    }

    // Let's find GoldPosition in our config, but the player isn't in any given cell
    if (!goldInPosition.Positions.Any(coords => coords.X == x && coords.Y == y))
    {
        return;
    }

    //Execute GenerateGoldEvent event to give player gold
    int amountOfGold = goldInPosition.AmountOfGold;
    await session.EmitEventAsync(new GenerateGoldEvent(amountOfGold));
}

Well, much better... but it's just an example. Please remember that storing a lot of elements in the list and trying to return one of them have a very high performance cost - the better solution to this is Dictionary as I showed above.


❗ Note

Remember that everything you return from methods and change in properties of the config while server is running will be saved! Example:

GiveGoldInPosition goldInPosition = _giveGoldConfig.FindConfigByMapId(mapId);

if (goldInPosition == null)
{
    return;
}

goldInPosition.AmountOfGold = 100;

The goldInPosition.AmountOfGold = 100 will be saved inside the memory of the server and each time someone returns the same config from the dictionary it will return 100 instead of 1 just like it was at the beginning of the config.

There are two ways to fix it:

  • First one is changing all properties setters { set; } into { init; } so you can't change the value of the property while the sever is running, but only during initialization of the config.
  • Second one is returning the config using Adapt method:
public GiveGoldInPosition FindConfigByMapId(int mapId)
{
    return _configs.TryGetValue(mapId, out GiveGoldInPosition config) ? config.Adapt<GiveGoldInPosition>() : null;;
}

Pssst... do you remember why you couldn't receive the rewards from mini-games in Miniland? Yeah, that's why... We didn't use Adapt<>() method.


The final result of the .cs file:

public interface IGiveGoldConfig
{
    GiveGoldInPosition FindConfigByMapId(int mapId);
}

public class GiveGoldConfig : IGiveGoldConfig
{
    private readonly IReadOnlyDictionary<int, GiveGoldInPosition> _configs = new Dictionary<int, GiveGoldInPosition>();

    public GiveGoldConfig(IEnumerable<GiveGoldInPosition> configs)
    {
        // ToDictionary() will create a Dictionary from each element in configs list and map MapId as key
        _configs = configs.ToDictionary(x => x.MapId);
    }

    public GiveGoldInPosition FindConfigByMapId(int mapId)
    {
        return _configs.TryGetValue(mapId, out GiveGoldInPosition config) ? config : null;
    }
}

public class GiveGoldInPosition
{
    public int MapId { get; set; }
    public int AmountOfGold { get; set; }
    public List<GoldPosition> Positions { get; set; }
}

public class GoldPosition
{
    public short X { get; set; }
    public short Y { get; set; }
}