A mod that, when triggered, displays a popup on the screen with an image, text, and a sound. The sound is optional — you can choose to use only the image and text if you prefer.
💻Client Side
Create a folder inside the client's 'mods' folder named popup_mod. Inside it, create the following subfolders: imgfor images and soundsfor sound effects. In the root of the popup_mod folder, create the following files: popup_mod.lua, popup_mod.otmod, and popup_mod.otui
popup_mod.lua
-- Configurações
local config = {
displayTime = 3000,
interactive = false,
fadeOut = { duration = 500, interval = 50, enabled = true },
position = { top = 10, right = 200 },
spacing = 5,
maxPopups = 5,
maxPopupsBehavior = 'remove_oldest', -- 'ignore' ou 'remove_oldest'
size = { width = 600, height = 80 },
iconSize = { width = 64, height = 64 },
image = { opacity = 1.0, smooth = true },
text = {
fontSize = 16,
color = '#FFFFFF',
align = 'left',
margin = { left = 10, right = 10, top = 5, bottom = 5 },
position = { relativeTo = 'right', distance = 10 },
wrap = true,
maxHeight = 0
},
backgroundColor = '#000000b0'
}
local activePopups = {}
local popupQueue = {}
local nextPopupId = 1
local function createPopupData(imageName, text)
return {
id = nextPopupId,
imageName = imageName,
text = text,
window = nil,
hideEvent = nil,
fadeEvent = nil,
fadeStep = 0,
isDestroying = false
}
end
local function reorganizePopups()
local currentTop = config.position.top
for i, popupData in ipairs(activePopups) do
if popupData.window then
local newTop = currentTop + ((i - 1) * (config.size.height + config.spacing))
popupData.window:setMarginTop(newTop)
end
end
end
local function processQueue()
while #popupQueue > 0 and #activePopups < config.maxPopups do
local nextPopup = table.remove(popupQueue, 1)
showPopup(nextPopup.imageName, nextPopup.text)
end
end
local function startFadeOut(popupData)
if not popupData.window then return end
popupData.fadeStep = 0
local totalSteps = config.fadeOut.duration / config.fadeOut.interval
local alphaStep = 1.0 / totalSteps
local function fadeOutStep()
if not popupData.window then return end
popupData.fadeStep = popupData.fadeStep + 1
local currentAlpha = 1.0 - (popupData.fadeStep * alphaStep)
if currentAlpha <= 0 then
if popupData.window then
popupData.window:destroy()
popupData.window = nil
end
if popupData.fadeEvent then
removeEvent(popupData.fadeEvent)
popupData.fadeEvent = nil
end
for i, popup in ipairs(activePopups) do
if popup.id == popupData.id then
table.remove(activePopups, i)
break
end
end
reorganizePopups()
processQueue()
return
end
local alpha = math.floor(currentAlpha * 255)
local bgColor = config.backgroundColor:sub(1, 7) .. string.format("%02X", alpha)
if popupData.window then
popupData.window:setBackgroundColor(bgColor)
end
popupData.fadeEvent = scheduleEvent(fadeOutStep, config.fadeOut.interval)
end
popupData.fadeEvent = scheduleEvent(fadeOutStep, config.fadeOut.interval)
end
function destroyPopup(popupData)
if popupData.isDestroying or not popupData.window then return end
popupData.isDestroying = true
if popupData.hideEvent then
removeEvent(popupData.hideEvent)
popupData.hideEvent = nil
end
if not config.fadeOut.enabled then
if popupData.window then
popupData.window:destroy()
popupData.window = nil
end
for i, popup in ipairs(activePopups) do
if popup.id == popupData.id then
table.remove(activePopups, i)
break
end
end
reorganizePopups()
processQueue()
return
end
startFadeOut(popupData)
end
function showPopup(imageName, text)
if #activePopups >= config.maxPopups then
if config.maxPopupsBehavior == 'remove_oldest' then
local oldestPopup = activePopups[1]
if oldestPopup then
destroyPopup(oldestPopup)
end
else
return
end
end
local popupData = createPopupData(imageName, text)
nextPopupId = nextPopupId + 1
table.insert(activePopups, popupData)
popupData.window = g_ui.createWidget('PopupModWindow', g_ui.getRootWidget())
if not popupData.window then
return
end
popupData.window:setFocusable(config.interactive)
popupData.window:setEnabled(config.interactive)
popupData.window:setSize({ width = config.size.width, height = config.size.height })
popupData.window:setMarginRight(config.position.right)
popupData.window:setBackgroundColor(config.backgroundColor)
local icon = popupData.window:getChildById('popupIcon')
if icon then
icon:setImageSource('/mods/popup_mod/img/' .. imageName .. '.png')
icon:setSize({ width = config.iconSize.width, height = config.iconSize.height })
icon:setImageSmooth(config.image.smooth)
icon:setOpacity(config.image.opacity)
end
local label = popupData.window:getChildById('popupText')
if label then
label:setText(text)
label:setColor(config.text.color)
end
reorganizePopups()
popupData.window:show()
popupData.window:raise()
popupData.hideEvent = scheduleEvent(function()
destroyPopup(popupData)
end, config.displayTime)
end
function addPopupRequest(imageName, text)
if #activePopups >= config.maxPopups then
if config.maxPopupsBehavior == 'remove_oldest' then
local oldestPopup = activePopups[1]
if oldestPopup then
destroyPopup(oldestPopup)
end
table.insert(popupQueue, { imageName = imageName, text = text })
else
return
end
else
showPopup(imageName, text)
end
end
function clearAllPopups()
for i = #activePopups, 1, -1 do
local popupData = activePopups[i]
if popupData.window then
popupData.window:destroy()
end
if popupData.hideEvent then
removeEvent(popupData.hideEvent)
end
if popupData.fadeEvent then
removeEvent(popupData.fadeEvent)
end
end
activePopups = {}
popupQueue = {}
end
function init()
g_ui.importStyle('popup_mod.otui')
pcall(ProtocolGame.unregisterExtendedOpcode, 55)
ProtocolGame.registerExtendedOpcode(55, onPopupMessage)
scheduleEvent(function()
if g_ui.console then
g_ui.console.registerCommand({
name = 'testpopup',
help = 'Testa popup',
callback = function()
addPopupRequest('balde', 'Testando popup!')
end
})
g_ui.console.registerCommand({
name = 'clearpopup',
help = 'Limpa popups',
callback = function()
clearAllPopups()
end
})
end
end, 500)
end
function terminate()
pcall(ProtocolGame.unregisterExtendedOpcode, 55)
clearAllPopups()
if g_ui.console then
g_ui.console.unregisterCommand('testpopup')
g_ui.console.unregisterCommand('clearpopup')
end
end
function onPopupMessage(protocol, opcode, buffer)
if opcode ~= 55 then return end
local parts = buffer:split(',')
local soundName
local imageName
local text
-- Verifica se o primeiro parâmetro é um som
if parts[1] and parts[1]:match("^som=") then
soundName = parts[1]:sub(5) -- Pega "erro" de "som=erro"
table.remove(parts, 1) -- Remove o parâmetro de som da lista
end
-- Após tratar o som, o que resta deve ser a imagem e o texto
if #parts < 2 then
g_logger.error("[PopupMod] Mensagem malformada (imagem ou texto ausente): " .. buffer)
return
end
imageName = table.remove(parts, 1) -- O próximo item é sempre a imagem
text = table.concat(parts, ',') -- Todo o resto é o texto
-- Toca o som, se um nome válido foi encontrado
if soundName and soundName ~= "" then
local soundPath = '/mods/popup_mod/sounds/' .. soundName .. '.ogg'
if g_resources.fileExists(soundPath) then
g_sounds.play(soundPath)
else
g_logger.error("[PopupMod] Arquivo de som não encontrado: " .. soundPath)
end
end
-- Exibe o popup com a imagem e texto corretos
addPopupRequest(imageName, text)
end
popup_mod.otmod
Module
name: popup_mod
description: Exibe um popup com icone e texto
author: Farm Team
version: 1.0
autoload: true
sandboxed: true
scripts: [ popup_mod ]
@onLoad: init()
@onUnload: terminate()
Inside the 'img' folder, place the images with a size of 32x32pixels in .png format.
By default, the mod will display them at 64x64 pixels.
You can change this on line 28 of the Lua file by modifying:
iconSize = { width = 64, height = 64 },to adjust the size or zoom as needed.
Inside the 'sounds' folder, you can add sound effects in .ogg format.
For example: 'error sound.ogg'. The system supports spaces in file names.
🔧 About the Configurations:
The popup behavior, appearance, and layout are fully customizable in the config section of the popup_mod.lua file. Here’s what each option does:
displayTime – Duration the popup stays on screen (in milliseconds). Example: 3000 = 3 seconds.
interactive – If set to true, the popup can be clicked or interacted with. Default is false (just displays).
fadeOut – Controls the fade-out effect:
enabled – Turn fade-out on/off (true or false).
duration – Total fade-out time in milliseconds (500 = half a second).
interval – Speed of fade steps (50 milliseconds).
position – Defines where the popup appears on the screen:
top – Distance from the top (pixels).
right – Distance from the right (pixels).
spacing – Space between multiple popups (in pixels).
maxPopups – Maximum number of popups shown at the same time.
maxPopupsBehavior – What happens when the limit is reached:
'remove_oldest' – Removes the oldest popup.
'ignore' – Ignores new popups until space is available.
size – Popup window size:
width – Width in pixels (600).
height – Height in pixels (80).
iconSize – Size of the image/icon:
width and height – Default 64x64 (can be adjusted for zoom or fit).
The sound is located in the sounds folder with the .ogg extension.
The image is located in the img folder with the .png extension.
✅ Example 2 – With image and text only (no sound):
player:sendPopupMessage("bucket", "You don't have enough water.")
🖼️ Image: bucket
📝 Text:"You don't have enough water."
The image is located in the img folder with the .png extension.
🔥 RevScript Example – Check Money and Show Popup if Not Enough:
local popupItem = 26453 -- Item ID to trigger the popup
local requiredMoney = 10000 -- Money required (in gold coins)
local popupAction = Action()
function popupAction.onUse(player, item, fromPosition, target, toPosition, isHotkey)
if player:getMoney() < requiredMoney then
player:sendPopupMessage("sound=no money", "money", "You don't have enough money.")
return true
end
-- If the player has enough money, you can do something here.
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have enough money!")
-- Or remove money, give an item, etc.
return true
end
I hope this helps anyone who needs it! 😄
If you'd like to support me, here are my details: