-
Notifications
You must be signed in to change notification settings - Fork 37
RevScript System Complete Guide
Mateuzkl edited this page Apr 24, 2026
·
1 revision
Modern scripting system for TFS 1.8 Downgrade 8.60 — No XML registration required
RevScript is a modern alternative way to register scripts in TFS without needing XML files. Instead of editing actions.xml, movements.xml, talkactions.xml, etc., you simply place your Lua scripts inside data/scripts/ (or any subfolder), and they are automatically registered.
| Traditional System | RevScript System |
|---|---|
| Requires XML registration | Auto-registration |
| Separate files for each event type | Multiple event types in one file |
| Manual ID/word mapping | Programmatic registration |
| Harder to maintain | Cleaner and more organized |
data/
├── scripts/ ← Place your RevScripts here
│ ├── actions/
│ ├── creatureevents/
│ ├── globalevents/
│ ├── movements/
│ ├── spells/
│ ├── talkactions/
│ └── weapons/
└── monster/ ← Monster scripts go here
└── scripts/
💡 Tip: You can organize scripts in any subfolder structure you want inside
data/scripts/
RevScript supports the following event types:
| Metatable | Usage |
|---|---|
Action() |
Item usage (onUse) |
CreatureEvent("name") |
Player/creature events (login, logout, death, etc.) |
GlobalEvent("name") |
Server-wide events (startup, shutdown, timers) |
MonsterType("name") |
Monster definitions and behaviors |
MoveEvent() |
Tile stepping, item equip/deequip |
Spell("name") |
Spell casting |
TalkAction("words") |
Chat commands |
Weapon(WEAPON_TYPE) |
Weapon attacks |
Every RevScript follows this pattern:
-- 1. Header: Create the metatable
local scriptName = Metatable()
-- 2. Configuration (optional)
scriptName:method(value)
-- 3. Function: Define the behavior
function scriptName.onEvent(parameters)
-- Your code here
return true
end
-- 4. Registration: Register IDs, words, etc.
scriptName:id(1234)
-- 5. Footer: Register the script
scriptName:register()local action = Action()
function action.onUse(player, item, fromPosition, target, toPosition, isHotkey)
player:sendTextMessage(MESSAGE_STATUS_DEFAULT, "You used item: " .. item:getId())
return true
end
action:id(2550) -- Scythe
action:register()| Method | Description | Example |
|---|---|---|
id(ids) |
Register by item ID(s) |
action:id(2550) or action:id(2550, 2551, 2552)
|
aid(ids) |
Register by Action ID(s) | action:aid(1000) |
uid(ids) |
Register by Unique ID(s) | action:uid(5000) |
allowFarUse(bool) |
Allow use from distance | action:allowFarUse(true) |
blockWalls(bool) |
Block use through walls | action:blockWalls(false) |
checkFloor(bool) |
Require same floor level | action:checkFloor(false) |
local questChest = Action()
function questChest.onUse(player, item, fromPosition, target, toPosition, isHotkey)
local storage = 10001
if player:getStorageValue(storage) == 1 then
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The chest is empty.")
return true
end
player:addItem(2160, 10) -- 10 crystal coins
player:setStorageValue(storage, 1)
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have found 10 crystal coins!")
return true
end
questChest:uid(5001)
questChest:register()| Interface | Triggered When |
|---|---|
onLogin(player) |
Player logs in |
onLogout(player) |
Player logs out |
onThink(creature, interval) |
Creature thinks (periodic) |
onPrepareDeath(creature, killer) |
Before creature dies |
onDeath(creature, corpse, killer, ...) |
Creature dies |
onKill(creature, target) |
Creature kills another |
onAdvance(player, skill, oldLevel, newLevel) |
Player advances skill/level |
onModalWindow(player, modalWindowId, buttonId, choiceId) |
Modal window interaction |
onTextEdit(player, item, text) |
Player edits text (books, signs) |
onHealthChange(creature, attacker, ...) |
Health changes |
onManaChange(creature, attacker, ...) |
Mana changes |
onExtendedOpCode(player, opcode, buffer) |
Extended opcode received |
local loginEvent = CreatureEvent("WelcomeMessage")
function loginEvent.onLogin(player)
player:sendTextMessage(MESSAGE_STATUS_DEFAULT, "Welcome to the server, " .. player:getName() .. "!")
player:getPosition():sendMagicEffect(CONST_ME_FIREWORK_YELLOW)
return true
end
loginEvent:register()local levelReward = CreatureEvent("LevelReward")
function levelReward.onAdvance(player, skill, oldLevel, newLevel)
if skill ~= SKILL_LEVEL then
return true
end
if newLevel >= 100 and oldLevel < 100 then
player:addItem(2160, 50) -- 50 crystal coins
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations on reaching level 100! You received 50 crystal coins!")
end
return true
end
levelReward:register()| Interface | Triggered When |
|---|---|
onThink(interval) |
Periodic execution |
onTime(interval) |
Specific time of day |
onStartup() |
Server starts |
onShutdown() |
Server shuts down |
onRecord(current, old) |
New player record |
local startup = GlobalEvent("ServerStartup")
function startup.onStartup()
print(">> Server has started successfully!")
return true
end
startup:register()local restartWarning = GlobalEvent("DailyRestart")
function restartWarning.onTime(interval)
Game.broadcastMessage("Server will restart in 5 minutes!", MESSAGE_STATUS_WARNING)
return true
end
restartWarning:time("05:55") -- 5:55 AM daily
restartWarning:register()local periodicEvent = GlobalEvent("PeriodicCheck")
function periodicEvent.onThink(interval)
print("This runs every 30 seconds")
return true
end
periodicEvent:interval(30000) -- 30000ms = 30 seconds
periodicEvent:register()| Interface | Triggered When |
|---|---|
onEquip(player, item, slot, isCheck) |
Player equips item |
onDeEquip(player, item, slot, isCheck) |
Player unequips item |
onStepIn(creature, item, position, fromPosition) |
Creature steps on tile |
onStepOut(creature, item, position, fromPosition) |
Creature leaves tile |
onAddItem(moveitem, tileitem, position) |
Item added to tile |
onRemoveItem(moveitem, tileitem, position) |
Item removed from tile |
| Method | Description | Example |
|---|---|---|
id(ids) |
Register by item ID(s) | moveevent:id(2550) |
aid(ids) |
Register by Action ID(s) | moveevent:aid(1000) |
uid(ids) |
Register by Unique ID(s) | moveevent:uid(5000) |
position(positions) |
Register by position | moveevent:position({x=100, y=100, z=7}) |
level(lvl) |
Minimum level required | moveevent:level(50) |
magiclevel(lvl) |
Minimum magic level | moveevent:magiclevel(25) |
premium(bool) |
Premium required | moveevent:premium(true) |
vocation(name, show, last) |
Vocation restriction | moveevent:vocation("Sorcerer", true, false) |
local equipBonus = MoveEvent()
function equipBonus.onEquip(player, item, slot, isCheck)
if isCheck then
return true
end
player:addHealth(500)
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You feel healthier! (+500 HP)")
return true
end
function equipBonus.onDeEquip(player, item, slot, isCheck)
if isCheck then
return true
end
player:addHealth(-500)
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You feel weaker. (-500 HP)")
return true
end
equipBonus:id(2472) -- Magic plate armor
equipBonus:register()local teleport = MoveEvent()
function teleport.onStepIn(creature, item, position, fromPosition)
local player = creature:getPlayer()
if not player then
return true
end
local destination = Position(1000, 1000, 7)
player:teleportTo(destination)
destination:sendMagicEffect(CONST_ME_TELEPORT)
return true
end
teleport:aid(1234)
teleport:register()local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA)
combat:setArea(createCombatArea(AREA_CIRCLE3X3))
function onGetFormulaValues(player, level, magicLevel)
local min = (level / 5) + (magicLevel * 5) + 20
local max = (level / 5) + (magicLevel * 8) + 40
return -min, -max
end
combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")
local spell = Spell("instant")
function spell.onCastSpell(creature, variant)
return combat:execute(creature, variant)
end
spell:name("Fire Bomb")
spell:words("exevo flam hur")
spell:group("attack")
spell:vocation("sorcerer", "master sorcerer")
spell:id(50)
spell:cooldown(4000) -- 4 seconds
spell:groupCooldown(2000) -- 2 seconds
spell:level(30)
spell:mana(150)
spell:isSelfTarget(true)
spell:isAggressive(true)
spell:needLearn(false)
spell:register()| Method | Description |
|---|---|
name(name) |
Spell name |
words(words) |
Magic words |
group(group) |
Spell group (attack, healing, support) |
vocation(voc1, voc2, ...) |
Allowed vocations |
id(id) |
Spell ID |
cooldown(ms) |
Individual cooldown |
groupCooldown(ms) |
Group cooldown |
level(lvl) |
Minimum level |
mana(mana) |
Mana cost |
soul(soul) |
Soul cost |
isPremium(bool) |
Premium required |
isAggressive(bool) |
Aggressive spell |
needLearn(bool) |
Needs to be learned |
isSelfTarget(bool) |
Targets self |
local hello = TalkAction("!hello")
function hello.onSay(player, words, param, type)
player:say("Hello, " .. player:getName() .. "!", TALKTYPE_MONSTER_SAY)
return true
end
hello:separator(" ")
hello:register()local teleportCommand = TalkAction("/tp")
function teleportCommand.onSay(player, words, param, type)
if not player:getGroup():getAccess() then
return false
end
local split = param:split(",")
if #split < 3 then
player:sendCancelMessage("Usage: /tp x,y,z")
return false
end
local position = Position(tonumber(split[1]), tonumber(split[2]), tonumber(split[3]))
player:teleportTo(position)
position:sendMagicEffect(CONST_ME_TELEPORT)
return true
end
teleportCommand:separator(" ")
teleportCommand:register()WEAPON_SWORDWEAPON_AXEWEAPON_CLUBWEAPON_SHIELDWEAPON_DISTANCEWEAPON_WANDWEAPON_AMMO
local explosiveArrow = Weapon(WEAPON_AMMO)
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE)
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONAREA)
combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_BURSTARROW)
combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true)
combat:setFormula(COMBAT_FORMULA_SKILL, 0, 0, 1.5, 0)
combat:setArea(createCombatArea(AREA_CIRCLE2X2))
function explosiveArrow.onUseWeapon(player, variant)
return combat:execute(player, variant)
end
explosiveArrow:id(2547) -- Power bolt
explosiveArrow:register()local mType = Game.createMonsterType("Demon Lord")
local monster = {}
monster.description = "a demon lord"
monster.experience = 10000
monster.outfit = {
lookType = 35,
lookHead = 0,
lookBody = 0,
lookLegs = 0,
lookFeet = 0,
lookAddons = 0,
lookMount = 0
}
monster.health = 8200
monster.maxHealth = monster.health
monster.race = "fire"
monster.corpse = 5995
monster.speed = 280
monster.maxSummons = 2
monster.changeTarget = {
interval = 4000,
chance = 20
}
monster.flags = {
summonable = false,
attackable = true,
hostile = true,
convinceable = false,
pushable = false,
canPushItems = true,
canPushCreatures = true,
targetDistance = 1,
staticAttackChance = 90
}
monster.summons = {
{name = "Fire Elemental", chance = 15, interval = 2000, max = 2}
}
monster.voices = {
interval = 5000,
chance = 10,
{text = "BOW TO THE POWER OF THE RUTHLESS SEVEN!", yell = true},
{text = "SOULS FOR BELIAL!", yell = true}
}
monster.loot = {
{id = "gold coin", chance = 100000, maxCount = 100},
{id = "platinum coin", chance = 100000, maxCount = 50},
{id = "crystal coin", chance = 50000, maxCount = 10},
{id = "magic plate armor", chance = 1500},
{id = "demon shield", chance = 2000}
}
monster.attacks = {
{name = "melee", attack = 130, skill = 100, effect = CONST_ME_DRAWBLOOD, interval = 2000},
{name = "combat", type = COMBAT_FIREDAMAGE, chance = 20, interval = 2000, minDamage = -200, maxDamage = -400, range = 7, radius = 3, shootEffect = CONST_ANI_FIRE, effect = CONST_ME_FIREAREA, target = true}
}
monster.defenses = {
defense = 55,
armor = 55,
{name = "combat", type = COMBAT_HEALING, chance = 15, interval = 2000, minDamage = 200, maxDamage = 400, effect = CONST_ME_MAGIC_BLUE}
}
monster.elements = {
{type = COMBAT_PHYSICALDAMAGE, percent = 20},
{type = COMBAT_ENERGYDAMAGE, percent = 20},
{type = COMBAT_EARTHDAMAGE, percent = 20},
{type = COMBAT_FIREDAMAGE, percent = 100},
{type = COMBAT_LIFEDRAIN, percent = 0},
{type = COMBAT_MANADRAIN, percent = 0},
{type = COMBAT_DROWNDAMAGE, percent = 0},
{type = COMBAT_ICEDAMAGE, percent = -10},
{type = COMBAT_HOLYDAMAGE, percent = -10},
{type = COMBAT_DEATHDAMAGE, percent = 20}
}
monster.immunities = {
{type = "paralyze", condition = true},
{type = "outfit", condition = false},
{type = "invisible", condition = true},
{type = "bleed", condition = false}
}
mType:register(monster)data/scripts/
├── actions/
│ ├── quests/
│ ├── tools/
│ └── doors/
├── movements/
│ ├── teleports/
│ └── tiles/
└── talkactions/
├── player/
└── admin/
-- ❌ Bad
local a = Action()
-- ✅ Good
local questChestAction = Action()local action = Action()
function action.onUse(player, item, fromPosition, target, toPosition, isHotkey)
-- Check if player already completed the quest
if player:getStorageValue(10001) == 1 then
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The chest is empty.")
return true
end
-- Give reward and mark quest as completed
player:addItem(2160, 10)
player:setStorageValue(10001, 1)
return true
end
action:uid(5001)
action:register()-- Quest system with multiple event types
local questAction = Action()
local questMovement = MoveEvent()
function questAction.onUse(player, item, fromPosition, target, toPosition, isHotkey)
-- Action logic
return true
end
function questMovement.onStepIn(creature, item, position, fromPosition)
-- Movement logic
return true
end
questAction:uid(5001)
questAction:register()
questMovement:aid(5002)
questMovement:register()- Official TFS Wiki: github.com/otland/forgottenserver/wiki/Revscriptsys
- TFS Repository: github.com/otland/forgottenserver
- OTLand Forum: otland.net
Made with 💜 by Mateuzkl