#include "MySQLDatabase.h" #include #include namespace { IUgc::ProcessInfo ReadUgcProcessInfo(PreparedStmtResultSet& result, bool modular) { IUgc::ProcessInfo info; info.id = result->getInt64("id"); info.characterId = result->getInt64("character_id"); info.characterName = std::string(result->getString("character_name").c_str()); info.state = static_cast(result->getInt("is_optimized")); info.attempts = static_cast(result->getInt("process_attempts")); info.processedAt = result->getInt64("processed_at"); info.error = std::string(result->getString("process_error").c_str()); if (modular) info.details = std::string(result->getString("ldf_config").c_str()); else info.processAfter = result->getInt64("process_after"); if (!modular) info.bakeAo = result->getInt("bake_ao") != 0; return info; } int64_t UnixNow() { return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } } IUgc::Model ReadModel(PreparedStmtResultSet& result) { IUgc::Model model; // blob is owned by the query, so we need to do a deep copy :/ std::unique_ptr blob(result->getBlob("lxfml")); model.lxfmlData << blob->rdbuf(); model.id = result->getUInt64("ugcID"); model.modelID = result->getUInt64("modelID"); return model; } std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto result = ExecuteSelect( "SELECT lxfml, u.id as ugcID, pc.id as modelID FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", propertyId); std::vector toReturn; while (result->next()) { toReturn.push_back(ReadModel(result)); } return toReturn; } std::vector MySQLDatabase::GetAllUgcModels() { auto result = ExecuteSelect("SELECT u.id AS ugcID, lxfml, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE pc.lot = 14 AND pc.ugc_id IS NOT NULL;"); std::vector models; models.reserve(result->rowsCount()); while (result->next()) { models.push_back(ReadModel(result)); } return models; } void MySQLDatabase::RemoveUnreferencedUgcModels() { ExecuteDelete("DELETE FROM ugc WHERE id NOT IN (SELECT ugc_id FROM properties_contents WHERE ugc_id IS NOT NULL);"); } void MySQLDatabase::InsertNewUgcModel( std::stringstream& sd0Data, // cant be const sad const uint64_t blueprintId, const uint32_t accountId, const LWOOBJID characterId, const int64_t processAfter) { const std::istream stream(sd0Data.rdbuf()); ExecuteInsert( "INSERT INTO `ugc`(`id`, `account_id`, `character_id`, `is_optimized`, `lxfml`, `bake_ao`, `filename`, `process_after`) VALUES (?,?,?,?,?,?,?,?)", blueprintId, accountId, characterId, 0, &stream, false, "weedeater.lxfml", processAfter ); // The owner is still building: their other models waiting for their quiet period wait longer if (processAfter > 0) ExecuteUpdate("UPDATE ugc SET process_after = ? WHERE character_id = ? AND is_optimized = 0 AND process_after > 0 AND process_after < ?;", processAfter, characterId, processAfter); } void MySQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM ugc WHERE id = ?;", modelId); ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); } void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml, const int64_t processAfter) { const std::istream stream(lxfml.rdbuf()); // The UGC server makes the model's files again ExecuteUpdate("UPDATE ugc SET lxfml = ?, is_optimized = 0, process_attempts = 0, process_error = '', process_after = ? WHERE id = ?;", &stream, processAfter, modelId); } std::optional MySQLDatabase::GetUgcModel(const LWOOBJID ugcId) { // LEFT JOIN: a model that is in someone's inventory rather than placed on a property still exists auto result = ExecuteSelect("SELECT u.id AS ugcID, lxfml, pc.id AS modelID FROM ugc AS u LEFT JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE u.id = ? LIMIT 1;", ugcId); std::optional toReturn = std::nullopt; if (result->next()) { toReturn = ReadModel(result); } return toReturn; } std::vector MySQLDatabase::GetUgcModelsToProcess(const uint32_t limit, const bool priorityOnly) { auto result = ExecuteSelect("SELECT id, lxfml, process_attempts, priority, process_options FROM ugc WHERE is_optimized = 0 AND process_after <= ? AND (? = 0 OR priority = 1) " "ORDER BY priority DESC, process_attempts ASC, id DESC LIMIT ?;", UnixNow(), priorityOnly ? 1 : 0, limit); std::vector models; while (result->next()) { auto& model = models.emplace_back(); model.id = result->getInt64("id"); std::unique_ptr blob(result->getBlob("lxfml")); std::stringstream contents; contents << blob->rdbuf(); model.lxfml = contents.str(); model.attempts = static_cast(result->getInt("process_attempts")); model.priority = result->getInt("priority") != 0; model.options = std::string(result->getString("process_options").c_str()); } return models; } void MySQLDatabase::SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) { ExecuteUpdate("UPDATE ugc SET is_optimized = ?, process_attempts = ?, process_error = ?, bake_ao = ?, processed_at = ?, priority = CASE WHEN ? = 0 THEN priority ELSE 0 END, " "process_options = CASE WHEN ? = 0 THEN process_options ELSE '' END WHERE id = ?;", static_cast(state), attempts, error, bakeAo, UnixNow(), static_cast(state), static_cast(state), id); } std::optional MySQLDatabase::GetUgcProcessInfo(const LWOOBJID id) { auto result = ExecuteSelect( "SELECT u.id, u.character_id, c.name AS character_name, u.is_optimized, u.process_attempts, u.processed_at, u.process_error, u.bake_ao, u.process_after " "FROM ugc AS u LEFT JOIN charinfo AS c ON c.id = u.character_id WHERE u.id = ? LIMIT 1;", id); if (!result->next()) return std::nullopt; return ReadUgcProcessInfo(result, false); } uint64_t MySQLDatabase::ResetUgcModelProcessing(const std::optional id, const bool failedOnly, const std::string_view options) { if (id) return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0, process_options = ? WHERE id = ?;", options, *id); if (failedOnly) return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0, process_options = ? WHERE is_optimized = 2;", options); return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0, process_options = ?;", options); } IUgc::ProcessTotals MySQLDatabase::GetUgcProcessTotals(const bool modular) { const std::string sql = modular ? "SELECT COUNT(*) AS made, SUM(CASE WHEN process_ms > 0 THEN 1 ELSE 0 END) AS timed, COALESCE(SUM(process_ms), 0) AS ms, COALESCE(SUM(process_cpu_ms), 0) AS cpu, " "COALESCE(MAX(process_ms), 0) AS maxms, COALESCE(AVG(NULLIF(process_memory_kb, 0)), 0) AS memavg, COALESCE(MAX(process_memory_kb), 0) AS memmax, " "0 AS bricks, 0 AS tris, 0 AS before_tris, 0 AS after_tris FROM ugc_modular_build WHERE is_optimized = 1;" : "SELECT COUNT(*) AS made, SUM(CASE WHEN process_ms > 0 THEN 1 ELSE 0 END) AS timed, COALESCE(SUM(process_ms), 0) AS ms, COALESCE(SUM(process_cpu_ms), 0) AS cpu, " "COALESCE(MAX(process_ms), 0) AS maxms, COALESCE(AVG(NULLIF(process_memory_kb, 0)), 0) AS memavg, COALESCE(MAX(process_memory_kb), 0) AS memmax, " "COALESCE(SUM(brick_count), 0) AS bricks, COALESCE(SUM(triangle_count), 0) AS tris, " "COALESCE(SUM(CASE WHEN triangle_count_before > 0 THEN triangle_count_before ELSE 0 END), 0) AS before_tris, " "COALESCE(SUM(CASE WHEN triangle_count_before > 0 THEN triangle_count ELSE 0 END), 0) AS after_tris FROM ugc WHERE is_optimized = 1;"; auto result = ExecuteSelect(sql); IUgc::ProcessTotals totals; if (!result->next()) return totals; const auto field = [&](const char* name) { return static_cast(std::llround(result->getDouble(name))); }; totals = { field("made"), field("timed"), field("ms"), field("cpu"), field("maxms"), field("memavg"), field("memmax"), field("bricks"), field("tris"), field("before_tris"), field("after_tris") }; return totals; } uint64_t MySQLDatabase::ResetPropertyUgcModelProcessing(const LWOOBJID propertyId, const std::string_view options) { return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0, priority = 1, process_options = ? " "WHERE id IN (SELECT ugc_id FROM properties_contents WHERE property_id = ? AND ugc_id IS NOT NULL);", options, propertyId); } void MySQLDatabase::RecordUgcModelRun(const ProcessRun& run) { ExecuteInsert("INSERT INTO ugc_process_runs (ugc_id, options, made_at, process_ms, process_cpu_ms, hsr_ms, ao_ms, icon_ms, bricks, triangles_before, triangles) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", run.id, run.options, UnixNow(), run.milliseconds, run.cpuMilliseconds, run.hsrMilliseconds, run.aoMilliseconds, run.iconMilliseconds, run.bricks, run.trianglesBefore, run.triangles); ExecuteUpdate("UPDATE ugc SET made_options = ? WHERE id = ?;", run.options, run.id); } std::vector MySQLDatabase::GetUgcRunSummaries() { auto result = ExecuteSelect("SELECT options, COUNT(*) AS runs, COUNT(DISTINCT ugc_id) AS models, SUM(process_ms) AS ms, SUM(process_cpu_ms) AS cpu, " "SUM(hsr_ms) AS hsr, SUM(ao_ms) AS ao, SUM(icon_ms) AS icon, SUM(bricks) AS bricks, SUM(triangles_before) AS before_tris, SUM(triangles) AS tris " "FROM ugc_process_runs GROUP BY options ORDER BY runs DESC, options;"); std::vector summaries; while (result->next()) { const auto field = [&](const char* name) { return static_cast(std::llround(result->getDouble(name))); }; summaries.push_back({ std::string(result->getString("options").c_str()), field("runs"), field("models"), field("ms"), field("cpu"), field("hsr"), field("ao"), field("icon"), field("bricks"), field("before_tris"), field("tris") }); } return summaries; } std::vector MySQLDatabase::GetUgcProcessList(const std::optional state, const std::string_view search, const uint32_t offset, const uint32_t limit) { const std::string select = "SELECT u.id, u.character_id, c.name AS character_name, u.is_optimized, u.process_attempts, u.processed_at, u.process_error, u.bake_ao, u.process_after " "FROM ugc AS u LEFT JOIN charinfo AS c ON c.id = u.character_id "; std::vector list; // Every filter always bound, off when its first value says so const int32_t stateValue = state ? static_cast(*state) : -1; const std::string text(search); const std::string pattern = "%" + text + "%"; const std::string where = "WHERE (? < 0 OR u.is_optimized = ?) AND (? = '' OR CAST(u.id AS CHAR) = ? OR c.name LIKE ?) "; auto result = ExecuteSelect(select + where + "ORDER BY u.id DESC LIMIT ? OFFSET ?;", stateValue, stateValue, text, text, pattern, limit, offset); while (result->next()) { list.push_back(ReadUgcProcessInfo(result, false)); } return list; } std::vector> MySQLDatabase::GetUgcProcessCounts() { auto result = ExecuteSelect("SELECT is_optimized, COUNT(*) AS count FROM ugc GROUP BY is_optimized;"); std::vector> counts; while (result->next()) { counts.emplace_back(static_cast(result->getInt("is_optimized")), static_cast(result->getInt64("count"))); } return counts; } void MySQLDatabase::ExpediteUgcModel(const LWOOBJID id) { ExecuteUpdate("UPDATE ugc SET process_after = 0 WHERE id = ? AND is_optimized = 0 AND process_after > 0;", id); } void MySQLDatabase::ExpediteUgcModels(const LWOOBJID characterId) { ExecuteUpdate("UPDATE ugc SET process_after = 0 WHERE character_id = ? AND is_optimized = 0 AND process_after > 0;", characterId); } void MySQLDatabase::SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles, const uint32_t trianglesBefore) { ExecuteUpdate("UPDATE ugc SET brick_count = ?, triangle_count = ?, triangle_count_before = ? WHERE id = ?;", bricks, triangles, trianglesBefore, id); } void MySQLDatabase::SetUgcFileChecksum(const eFileOwner owner, const LWOOBJID storageId, const std::string_view file, const std::string_view md5, const uint32_t size) { ExecuteInsert("INSERT INTO ugc_file_checksums (kind, storage_id, file, md5, size) VALUES (?, ?, ?, ?, ?) " "ON DUPLICATE KEY UPDATE md5 = VALUES(md5), size = VALUES(size);", static_cast(owner), storageId, file, md5, size); } std::optional MySQLDatabase::GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) { auto result = ExecuteSelect("SELECT md5, size FROM ugc_file_checksums WHERE kind = 0 AND storage_id = ? AND file = ? " "UNION ALL SELECT f.md5, f.size FROM ugc_modular_build b JOIN ugc_file_checksums f ON f.kind = 1 AND f.storage_id = b.combination_id AND f.file = ? " "WHERE b.ugc_id = ? AND b.combination_id != 0 LIMIT 1;", blueprintId, file, file, blueprintId); if (!result->next()) return std::nullopt; return IUgc::FileChecksum{ std::string(result->getString("md5").c_str()), static_cast(result->getUInt("size")) }; } void MySQLDatabase::SetUgcModelProcessStats(const LWOOBJID id, const ProcessStats& stats) { ExecuteUpdate("UPDATE ugc SET process_ms = ?, process_cpu_ms = ?, process_memory_kb = ? WHERE id = ?;", stats.milliseconds, stats.cpuMilliseconds, stats.memoryKb, id); }