Lua Conversion Guide

From PMDOWiki

While writing your own custom Lua scripts, there could be several instances where you might replicate the engine source code and modify it from there. However, it can be tricky translating C# into Lua, especially with C# methods and classes involving generics.

Examples

Below are some C# to Lua conversion cases you might encounter:

Calling C# Enum

Dir8

C#:

Dir8 dir = Dir8.DownLeft;

Lua:

local dir = RogueElements.Dir8.DownLeft
  • Notice how RogueElements is prepended in the Lua implementation. This is because it is located under the RogueElements namespace. This is essential to keep notice when working with the engine's C# classes.

Creating C# Class

Loc

C#:

Loc origin = new Loc(10, 10);
Loc origin2 = new Loc(25);
Console.WriteLine(origin.X);
Console.WriteLine(Loc.Dot(origin, origin2));
Console.WriteLine(origin2.ToString());

Lua:

local origin = RogueElements.Loc(10, 10)
local origin2 = RogueElements.Loc(25)
PrintInfo(tostring(origin.X))
PrintInfo(tostring(RogueElements.Loc.Dot(origin, origin2)))
PrintInfo(origin:ToString())
  • Note that you can call any of the class's constructor
  • In Lua, you can access a class member variable through the "." operator
  • You also use the "." operator for static methods
  • To call a class method (not static) is different. You use the ":" operator in Lua instead


Calling C# Generic Method

StateCollections::GetWithDefault<K>

C#:

AbilityLearnContext learn = context.ContextStates.GetWithDefault<AbilityLearnContext>();

Lua:

AbilityLearnContextType = luanet.import_type('PMDC.Dungeon.AbilityLearnContext')
local learn = context.ContextStates:GetWithDefault(luanet.ctype(AbilityLearnContextType))
  • Notice that in Lua, you have to import the type directly using luanet.import_type.
  • It is also important to note that PMDC.Dungeon is prepended because AbilityLearnContext is located at under the namespace here.

GameContext::GetContextStateInt<T>(int)

C#

int contextVal = context.GetContextStateInt<TargetAtkBoost>(0);

Lua

TargetAtkBoostType = luanet.import_type('PMDC.Dungeon.TargetAtkBoost')
local context_val = context:GetContextStateInt(luanet.ctype(TargetAtkBoostType), 0)

Create a Generic C# Class

List<T>

C#:

List<MobSpawn> allSpawns = new List<MobSpawn>();

Lua:

ListType = luanet.import_type('System.Collections.Generic.List`1')
MobSpawnType = luanet.import_type('RogueEssence.LevelGen.MobSpawn')
local all_spawns = LUA_ENGINE:MakeGenericType( ListType, { MobSpawnType }, { })

PlaceEntranceMobsStep<T, TEntrance>

C#:

SpecificTeamSpawner specificTeam = SpecificTeamSpawner();
MobSpawn postMob = MobSpawn();
postMob.BaseForm = MonsterID("oshawott", 0, "normal", Gender.Unknown);
postMob.Tactic = "boss";
mostMob.Level = RandRange(50);
specificTeam.Spawns.Add(postMob);
PresetMultiTeamSpawner picker = new PresetMultiTeamSpawner<MapGenContext>();
picker.Spawns.Add(specificTeam);
PlaceEntranceMobsStep mobPlacement = new PlaceEntranceMobsStep<MapGenContext, MapGenEntrance>(picker);

Lua:

PresetMultiTeamSpawnerType = luanet.import_type('RogueEssence.LevelGen.PresetMultiTeamSpawner`1')
EntranceType = luanet.import_type('RogueEssence.LevelGen.MapGenEntrance')
PlaceEntranceMobsStepType = luanet.import_type('RogueEssence.LevelGen.PlaceEntranceMobsStep`2')
MapGenContextType = luanet.import_type('RogueEssence.LevelGen.ListMapGenContext')

local specific_team = RogueEssence.LevelGen.SpecificTeamSpawner()
local post_mob = RogueEssence.LevelGen.MobSpawn()
post_mob.BaseForm = RogueEssence.Dungeon.MonsterID("oshawott", 0, "normal", Gender.Unknown)
post_mob.Tactic = "boss"
post_mob.Level = RogueElements.RandRange(50)
specific_team.Spawns:Add(post_mob)
local picker = LUA_ENGINE:MakeGenericType(PresetMultiTeamSpawnerType, { MapGenContextType }, { })
picker.Spawns:Add(specific_team)
local mob_placement = LUA_ENGINE:MakeGenericType(PlaceEntranceMobsStepType, { MapGenContextType, EntranceType }, { picker })
  • Notice the `1 and `2 appended at the end of the import_type. These represent how many generics the class requires.
  • The LUA_ENGINE:MakeGenericType method takes in three parameters:
    • The 1st parameter is what generic type to create
    • The 2nd parameter is a table of types necessary to create the generic type
    • The 3rd parameter is a table of values that is called with the constructor of the generic type

Source Code Conversion Example

LeechSeedEvent

C#:

public override IEnumerator<YieldInstruction> Apply(GameEventOwner owner, Character ownerChar, SingleCharContext context)
{
  if (context.User.CharStates.Contains<MagicGuardState>())
      yield break;
          
  //check for someone within 4 tiles away; if there's no one, then remove the status
  List<Character> targets = AreaAction.GetTargetsInArea(context.User, context.User.CharLoc, Alignment.Foe, 4);
  int lowestDist = Int32.MaxValue;
  Character target = null;
  for (int ii = 0; ii < targets.Count; ii++)
  {
    int newDist = (targets[ii].CharLoc - context.User.CharLoc).DistSquared();
    if (newDist < lowestDist)
    {
        target = targets[ii];
        lowestDist = newDist;
    }
  }

  if (target == null)
    yield return CoroutineManager.Instance.StartCoroutine(context.User.RemoveStatusEffect(((StatusEffect)owner).ID));
  else
  {
    int seeddmg = Math.Max(1, context.User.MaxHP / 12);

    DungeonScene.Instance.LogMsg(Text.FormatGrammar(new StringKey("MSG_LEECH_SEED").ToLocal(), context.User.GetDisplayName(false)));
    
    GameManager.Instance.BattleSE("DUN_Hit_Neutral");
    if (!context.User.Unidentifiable)
    {
        SingleEmitter endEmitter = new SingleEmitter(new AnimData("Hit_Neutral", 3));
        endEmitter.SetupEmit(context.User.MapLoc, context.User.MapLoc, context.User.CharDir);
        DungeonScene.Instance.CreateAnim(endEmitter, DrawLayer.NoDraw);
    }

    yield return CoroutineManager.Instance.StartCoroutine(context.User.InflictDamage(seeddmg, false));

    if (context.User.CharStates.Contains<DrainDamageState>())
    {
        GameManager.Instance.BattleSE("DUN_Toxic");
        DungeonScene.Instance.LogMsg(Text.FormatGrammar(new StringKey("MSG_LIQUID_OOZE").ToLocal(), target.GetDisplayName(false)));
        yield return CoroutineManager.Instance.StartCoroutine(target.InflictDamage(seeddmg * 4, false));
    }
    else if (target.HP < target.MaxHP)
    {
        yield return CoroutineManager.Instance.StartCoroutine(target.RestoreHP(seeddmg, false));
    }
  }
}

Lua:

SINGLE_CHAR_SCRIPT = {}
DrainDamageStateType = luanet.import_type('PMDC.Dungeon.DrainDamageState')
MagicGuardStateType = luanet.import_type('PMDC.Dungeon.MagicGuardState')

function SINGLE_CHAR_SCRIPT.LeechSeedEvent(owner, ownerChar, context, args)
  if context.User.CharStates:Contains(luanet.ctype(MagicGuardStateType)) then
    return
  end

  local targets = RogueEssence.Dungeon.AreaAction.GetTargetsInArea(context.User, context.User.CharLoc, RogueEssence.Dungeon.Alignment.Foe, 4)
  local lowestDist = 10000
  local target = nil
  for ii = 0, targets.Count - 1, 1 do
    local newDist = (targets[ii].CharLoc - context.User.CharLoc):DistSquared()
    if newDist < lowestDist then
      target = targets[ii]
      lowestDist = newDist
    end
  end

  if target == nil then
    TASK:WaitTask(context.User:RemoveStatusEffect(owner:GetID()))
    return
  else
    local seeddmg = math.max(1, context.User.MaxHP / 12)
    _DUNGEON:LogMsg(STRINGS:Format(RogueEssence.StringKey("MSG_LEECH_SEED"):ToLocal(), context.User:GetDisplayName(false)))
    SOUND:PlayBattleSE('DUN_Hit_Neutral')
    if not context.User.Unidentifiable then
      local endEmitter = RogueEssence.Content.SingleEmitter(RogueEssence.Content.AnimData("Hit_Neutral", 3))
      endEmitter:SetupEmit(context.User.MapLoc, context.User.MapLoc, context.User.CharDir)
      _DUNGEON:CreateAnim(endEmitter, RogueEssence.Content.DrawLayer.NoDraw)
    end

    TASK:WaitTask(context.User:InflictDamage(seeddmg, false))
    if context.User.CharStates:Contains(luanet.ctype(MagicGuardStateType)) then
      SOUND:PlayBattleSE('DUN_Toxic')
      _DUNGEON:LogMsg(STRINGS:Format(RogueEssence.StringKey("MSG_LIQUID_OOZE"):ToLocal(), target:GetDisplayName(false)))
      TASK:WaitTask(target:InflictDamage(seeddmg * 4, false))
    elseif target.HP < target.MaxHP then
      TASK:WaitTask(target:RestoreHP(seeddmg, false))
    end
  end
end

This concludes converting Lua to C# code! Knowing how and when to call and use the source engine's classes, methods, and enums allows you write more complex battle events beyond the Dev menu!