tibia-rme/source/lua/lua_api_item.cpp
Humberto Gontijo c136a75263
fix: make macOS build with modern sol2 and wxGL (#201)
Bug Fixes:
- Improved Lua scripting: Lua-visible nil is now consistent for missing/empty values across storage, JSON parsing, item/map/tile APIs, dialogs, app properties, and overlay hover.
- Improved macOS graphics reliability (OpenGL context/linking) and safer background/thread handling for streaming and SQLite bootstrap.
- Fixed client asset loading paths and improved SQLite migration input paths.

UI Improvements:
- Replaced “Missing …” modal dialogs with inline text boxes.
- Made “Client directory” editable; added macOS app-picker browse flow.

CI:
- Added compile-scoped macOS builds via reusable workflow and a new build-macos job.
2026-07-29 19:44:25 -03:00

232 lines
8.1 KiB
C++

//////////////////////////////////////////////////////////////////////
// This file is part of Remere's Map Editor
//////////////////////////////////////////////////////////////////////
// Remere's Map Editor is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Remere's Map Editor is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//////////////////////////////////////////////////////////////////////
#include "main.h"
#include "lua_api_item.h"
#include "../item.h"
#include "../items.h"
#include <algorithm>
#include <cctype>
namespace LuaAPI {
// Helper: convert string to lowercase
static std::string toLower(const std::string &str) {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::tolower(c); });
return result;
}
// Helper: check if string contains another (case-insensitive)
static bool containsIgnoreCase(const std::string &haystack, const std::string &needle) {
std::string lowerHaystack = toLower(haystack);
std::string lowerNeedle = toLower(needle);
return lowerHaystack.contains(lowerNeedle);
}
void registerItem(sol::state &lua) {
// Helper to validate uint16_t range
auto require_u16 = [](int value, const char* field) {
if (value < 0 || value > 65535) {
throw sol::error(std::string(field) + " must be between 0 and 65535");
}
return static_cast<uint16_t>(value);
};
// Register Item usertype
lua.new_usertype<Item>(
"Item",
// No public constructor - items are created via Tile:addItem() or obtained from tiles
sol::no_constructor,
// Read-only properties
"id", sol::property(&Item::getID),
"clientId", sol::property([](const Item &item) { return g_items[item.getID()].clientID; }),
"name", sol::property(&Item::getName),
"fullName", sol::property(&Item::getFullName),
// Read/write properties
"count", sol::property(&Item::getCount, [require_u16](Item &item, int count) {
item.setSubtype(require_u16(count, "count"));
}),
"subtype", sol::property([](const Item &item) -> int { return item.getSubtype(); }, [require_u16](Item &item, int subtype) { item.setSubtype(require_u16(subtype, "subtype")); }), "actionId", sol::property([](const Item &item) -> int { return item.getActionID(); }, [require_u16](Item &item, int aid) { item.setActionID(require_u16(aid, "actionId")); }), "uniqueId", sol::property([](const Item &item) -> int { return item.getUniqueID(); }, [require_u16](Item &item, int uid) { item.setUniqueID(require_u16(uid, "uniqueId")); }), "text", sol::property(&Item::getText, &Item::setText), "description", sol::property(&Item::getDescription, &Item::setDescription),
// Selection
"isSelected", sol::property(&Item::isSelected), "select", &Item::select, "deselect", &Item::deselect,
// Type checks (read-only)
"isStackable", sol::property(&Item::isStackable), "isMoveable", sol::property(&Item::isMoveable), "isPickupable", sol::property(&Item::isPickupable), "isBlocking", sol::property(&Item::isBlocking), "isGroundTile", sol::property(&Item::isGroundTile), "isBorder", sol::property(&Item::isBorder), "isWall", sol::property(&Item::isWall), "isDoor", sol::property(&Item::isDoor), "isTable", sol::property(&Item::isTable), "isCarpet", sol::property(&Item::isCarpet), "isHangable", sol::property(&Item::isHangable), "isRoteable", sol::property(&Item::isRoteable), "isFluidContainer", sol::property(&Item::isFluidContainer), "isSplash", sol::property(&Item::isSplash), "hasCharges", sol::property(&Item::hasCharges), "hasElevation", sol::property([](const Item &item) {
return g_items[item.getID()].hasElevation;
}),
"zOrder", sol::property(&Item::getTopOrder),
// Methods
"clone", [](const Item &item) { return item.deepCopy(); }, "rotate", &Item::doRotate,
"getName", [](int id) -> std::string {
if (g_items[id].id != 0) {
return g_items[id].name;
}
return ""; }, "getDescription", [](int id) -> std::string {
if (g_items[id].id != 0) {
return g_items[id].description;
}
return ""; },
// String representation
sol::meta_function::to_string, [](const Item &item) { return "Item(id=" + std::to_string(item.getID()) + ", name=\"" + item.getName() + "\")"; }
);
// Register Items namespace for item lookup
sol::table items = lua.create_named_table("Items");
// Get item type info by ID
items["get"] = [](sol::this_state ts, int id) -> sol::object {
sol::state_view lua(ts);
if (g_items[id].id == 0) {
return sol::lua_nil;
}
ItemType &it = g_items[id];
sol::table info = lua.create_table();
info["id"] = it.id;
info["clientId"] = it.clientID;
info["name"] = it.name;
info["description"] = it.description;
return info;
};
// Get item info by ID - returns a table with item properties
items["getInfo"] = [](sol::this_state ts, int id) -> sol::object {
sol::state_view lua(ts);
if (g_items[id].id == 0) {
return sol::lua_nil;
}
ItemType &it = g_items[id];
sol::table info = lua.create_table();
info["id"] = it.id;
info["clientId"] = it.clientID;
info["name"] = it.name;
info["description"] = it.description;
info["isStackable"] = it.stackable;
info["isMoveable"] = it.moveable;
info["isPickupable"] = it.pickupable;
info["isGroundTile"] = it.isGroundTile();
info["isBorder"] = it.isBorder;
info["isWall"] = it.isWall;
info["isDoor"] = it.isDoor();
info["isTable"] = it.isTable;
info["isCarpet"] = it.isCarpet;
info["hasElevation"] = it.hasElevation;
return info;
};
// Check if item ID exists
items["exists"] = [](int id) {
return g_items[id].id != 0;
};
// Get max item ID
items["getMaxId"] = []() -> int {
return g_items.getMaxID();
};
// Find items by name (returns array of {id, name} tables)
// Searches for items whose name contains the search string (case-insensitive)
items["findByName"] = [](sol::this_state ts, const std::string &searchName, sol::optional<int> maxResults) -> sol::table {
sol::state_view lua(ts);
sol::table results = lua.create_table();
int limit = maxResults.value_or(50); // Default max 50 results
int count = 0;
int maxId = g_items.getMaxID();
std::string searchLower = toLower(searchName);
for (int id = 1; id <= maxId && count < limit; ++id) {
if (g_items[id].id == 0) {
continue;
}
ItemType &it = g_items[id];
if (it.name.empty()) {
continue;
}
// Check if name contains search string
if (containsIgnoreCase(it.name, searchName)) {
sol::table item = lua.create_table();
item["id"] = it.id;
item["name"] = it.name;
results[++count] = item;
}
}
return results;
};
// Find first item matching name exactly (case-insensitive)
// Returns item ID or nil
items["findIdByName"] = [](const std::string &searchName) -> sol::optional<int> {
std::string searchLower = toLower(searchName);
int maxId = g_items.getMaxID();
// First pass: exact match
for (int id = 1; id <= maxId; ++id) {
if (g_items[id].id == 0) {
continue;
}
ItemType &it = g_items[id];
if (it.name.empty()) {
continue;
}
if (toLower(it.name) == searchLower) {
return id;
}
}
// Second pass: contains match (return first)
for (int id = 1; id <= maxId; ++id) {
if (g_items[id].id == 0) {
continue;
}
ItemType &it = g_items[id];
if (it.name.empty()) {
continue;
}
if (containsIgnoreCase(it.name, searchName)) {
return id;
}
}
return sol::nullopt;
};
// Get item name by ID
items["getName"] = [](int id) -> std::string {
if (g_items[id].id != 0) {
return g_items[id].name;
}
return "";
};
}
} // namespace LuaAPI