ace/Source/ACE.Server/Command/Handlers/DeveloperContentCommands.cs
Ty Conner b9b5c23220
Update Event Importing (#4402)
Adjusted code for event to be properly registered in EventManager for hot-loading event without restarting server
2025-11-28 14:21:18 -05:00

3357 lines
129 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using ACE.Adapter.GDLE;
using ACE.Adapter.Lifestoned;
using ACE.Common.Extensions;
using ACE.Database;
using ACE.Database.Models.World;
using ACE.Database.SQLFormatters.World;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Server.Entity;
using ACE.Server.Factories;
using ACE.Server.Managers;
using ACE.Server.Network;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Physics.Extensions;
using ACE.Server.WorldObjects;
namespace ACE.Server.Command.Handlers.Processors
{
public class DeveloperContentCommands
{
public enum FileType
{
Undefined,
Encounter,
LandblockInstance,
Quest,
Recipe,
Spell,
Weenie,
Event,
}
public static FileType GetContentType(string[] parameters, ref string param)
{
for (var i = 0; i < parameters.Length; i++)
{
var otherIdx = i == 0 ? 1 : 0;
param = parameters[otherIdx];
var fileType = parameters[i].ToLower();
if (fileType.StartsWith("landblock"))
return FileType.LandblockInstance;
else if (fileType.StartsWith("encounter"))
return FileType.Encounter;
else if (fileType.StartsWith("event"))
return FileType.Event;
else if (fileType.StartsWith("quest"))
return FileType.Quest;
else if (fileType.StartsWith("recipe"))
return FileType.Recipe;
else if (fileType.StartsWith("weenie"))
return FileType.Weenie;
else if (fileType.StartsWith("spell"))
return FileType.Spell;
}
return FileType.Undefined;
}
[CommandHandler("import-json", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Imports json data from the Content folder", "<type> <wcid>\n<type> - landblock, quest, recipe, spell, weenie (default if not specified)\n<wcid> - filename prefix to search for. can be 'all' to import all files for this content type")]
public static void HandleImportJson(Session session, params string[] parameters)
{
var param = parameters[0];
var contentType = FileType.Weenie;
if (parameters.Length > 1)
{
contentType = GetContentType(parameters, ref param);
if (contentType == FileType.Undefined)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unknown content type '{parameters[1]}'");
return;
}
}
switch (contentType)
{
case FileType.LandblockInstance:
ImportJsonLandblock(session, param);
break;
case FileType.Quest:
ImportJsonQuest(session, param);
break;
case FileType.Recipe:
ImportJsonRecipe(session, param);
break;
case FileType.Weenie:
ImportJsonWeenie(session, param);
break;
}
}
public static void ImportJsonWeenie(Session session, string wcid)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var json_folder = $"{di.FullName}{sep}json{sep}weenies{sep}";
var prefix = wcid + " - ";
if (wcid.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(json_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.json") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {json_folder}{prefix}*.json");
return;
}
foreach (var file in files)
ImportJsonWeenie(session, json_folder, file.Name);
}
public static void ImportJsonRecipe(Session session, string recipeId)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var json_folder = $"{di.FullName}{sep}json{sep}recipes{sep}";
var prefix = recipeId.PadLeft(5, '0') + " - ";
if (recipeId.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(json_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.json") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {json_folder}{prefix}*.json");
return;
}
foreach (var file in files)
ImportJsonRecipe(session, json_folder, file.Name);
}
public static void ImportJsonLandblock(Session session, string landblockId)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var json_folder = $"{di.FullName}{sep}json{sep}landblocks{sep}";
var prefix = landblockId;
if (landblockId.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(json_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.json") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {json_folder}{prefix}*.json");
return;
}
foreach (var file in files)
ImportJsonLandblock(session, json_folder, file.Name);
}
public static void ImportJsonQuest(Session session, string questName)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var json_folder = $"{di.FullName}{sep}json{sep}quests{sep}";
var prefix = questName;
if (questName.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(json_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.json") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {json_folder}{prefix}*.json");
return;
}
foreach (var file in files)
ImportJsonQuest(session, json_folder, file.Name);
}
[CommandHandler("import-sql-folders", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Imports all weenie sql data from the Content folder and all sub-folders", "<wcid>\n<wcid> - wcid prefix to search for. can be 'all' to import everything")]
public static void HandleImportSQLFolders(Session session, params string[] parameters)
{
var param = parameters[0];
ImportSQLWeenie(session, param, true);
}
[CommandHandler("import-sql", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Imports sql data from the Content folder", "<type> <wcid>\n<type> - landblock, encounter, quest, recipe, spell, weenie (default if not specified)\n<wcid> - filename prefix to search for. can be 'all' to import all files for this content type")]
public static void HandleImportSQL(Session session, params string[] parameters)
{
var param = parameters[0];
var contentType = FileType.Weenie;
if (parameters.Length > 1)
{
contentType = GetContentType(parameters, ref param);
if (contentType == FileType.Undefined)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unknown content type '{parameters[1]}'");
return;
}
}
try
{
switch (contentType)
{
case FileType.LandblockInstance:
ImportSQLLandblock(session, param);
break;
case FileType.Encounter:
ImportSQLEncounter(session, param);
break;
case FileType.Event:
ImportSQLEvent(session, param);
break;
case FileType.Quest:
ImportSQLQuest(session, param);
break;
case FileType.Recipe:
ImportSQLRecipe(session, param);
break;
case FileType.Spell:
ImportSQLSpell(session, param);
break;
case FileType.Weenie:
ImportSQLWeenie(session, param);
break;
}
}
catch(Exception e)
{
CommandHandlerHelper.WriteOutputError(session, $"There was an error importing the SQL:\n\n{e.Message}");
}
}
public static void ImportSQLWeenie(Session session, string wcid, bool withFolders = false)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var sql_folder = $"{di.FullName}{sep}sql{sep}weenies{sep}";
var prefix = wcid.PadLeft(5, '0') + " ";
if (wcid.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(sql_folder);
EnumerationOptions options = new EnumerationOptions();
options.RecurseSubdirectories = withFolders;
var files = di.Exists ? di.GetFiles($"{prefix}*.sql", options) : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {sql_folder}{prefix}*.sql");
return;
}
foreach (var file in files)
ImportSQLWeenie(session, file.DirectoryName + sep, file.Name);
}
public static void ImportSQLRecipe(Session session, string recipeId)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var sql_folder = $"{di.FullName}{sep}sql{sep}recipes{sep}";
var prefix = recipeId.PadLeft(5, '0') + " ";
if (recipeId.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(sql_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.sql") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {sql_folder}{prefix}*.sql");
return;
}
foreach (var file in files)
ImportSQLRecipe(session, sql_folder, file.Name);
}
public static void ImportSQLLandblock(Session session, string landblockId)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var sql_folder = $"{di.FullName}{sep}sql{sep}landblocks{sep}";
var prefix = landblockId;
if (landblockId.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(sql_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.sql") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {sql_folder}{prefix}*.sql");
return;
}
foreach (var file in files)
ImportSQLLandblock(session, sql_folder, file.Name);
}
public static void ImportSQLEncounter(Session session, string landblockId)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var sql_folder = $"{di.FullName}{sep}sql{sep}encounters{sep}";
var prefix = landblockId;
if (landblockId.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(sql_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.sql") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {sql_folder}{prefix}*.sql");
return;
}
foreach (var file in files)
ImportSQLEncounter(session, sql_folder, file.Name);
}
public static void ImportSQLEvent(Session session, string eventName)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var sql_folder = $"{di.FullName}{sep}sql{sep}events{sep}";
var prefix = eventName;
if (eventName.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(sql_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.sql") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {sql_folder}{prefix}*.sql");
return;
}
foreach (var file in files)
ImportSQLEvent(session, sql_folder, file.Name);
}
public static void ImportSQLQuest(Session session, string questName)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var sql_folder = $"{di.FullName}{sep}sql{sep}quests{sep}";
var prefix = questName;
if (questName.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(sql_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.sql") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {sql_folder}{prefix}*.sql");
return;
}
foreach (var file in files)
ImportSQLQuest(session, sql_folder, file.Name);
}
public static void ImportSQLSpell(Session session, string spellId)
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var sql_folder = $"{di.FullName}{sep}sql{sep}spells{sep}";
var prefix = spellId.PadLeft(5, '0') + " ";
if (spellId.Equals("all", StringComparison.OrdinalIgnoreCase))
prefix = "";
di = new DirectoryInfo(sql_folder);
var files = di.Exists ? di.GetFiles($"{prefix}*.sql") : null;
if (files == null || files.Length == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find {sql_folder}{prefix}*.sql");
return;
}
foreach (var file in files)
ImportSQLSpell(session, sql_folder, file.Name);
}
/// <summary>
/// Returns the absolute content folder path, and verifies it exists
/// </summary>
private static DirectoryInfo VerifyContentFolder(Session session, bool showError = true)
{
var content_folder = PropertyManager.GetString("content_folder").Item;
var sep = Path.DirectorySeparatorChar;
// handle relative path
if (content_folder.StartsWith("."))
{
var cwd = Directory.GetCurrentDirectory() + sep;
content_folder = cwd + content_folder;
}
var di = new DirectoryInfo(content_folder);
if (!di.Exists && showError)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find content folder: {di.FullName}");
CommandHandlerHelper.WriteOutputInfo(session, "To set your content folder, /modifystring content_folder <path>");
}
return di;
}
/// <summary>
/// Converts JSON to SQL, imports to database, and clears the weenie cache
/// </summary>
private static void ImportJsonWeenie(Session session, string json_folder, string json_file)
{
if (!uint.TryParse(Regex.Match(json_file, @"\d+").Value, out var wcid))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find wcid from {json_file}");
return;
}
// convert json -> sql
var sqlFile = json2sql_weenie(session, json_folder, json_file);
if (sqlFile == null) return;
// import sql to db
var sql_folder = json_folder.Replace("json", "sql");
ImportSQL(sql_folder + sqlFile);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sqlFile}");
// clear this weenie out of the cache
DatabaseManager.World.ClearCachedWeenie(wcid);
}
private static void ImportJsonRecipe(Session session, string json_folder, string json_file)
{
if (!uint.TryParse(Regex.Match(json_file, @"\d+").Value, out var recipeId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find recipe id from {json_file}");
return;
}
// convert json -> sql
var sqlFile = json2sql_recipe(session, json_folder, json_file);
if (sqlFile == null) return;
// import sql to db
var sql_folder = json_folder.Replace("json", "sql");
ImportSQL(sql_folder + sqlFile);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sqlFile}");
// clear recipe cache
DatabaseManager.World.ClearCookbookCache();
}
private static void ImportJsonLandblock(Session session, string json_folder, string json_file)
{
if (!ushort.TryParse(Regex.Match(json_file, @"[0-9A-F]{4}", RegexOptions.IgnoreCase).Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var landblockId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find landblock id from {json_file}");
return;
}
// convert json -> sql
var sqlFile = json2sql_landblock(session, json_folder, json_file);
if (sqlFile == null) return;
// import sql to db
var sql_folder = json_folder.Replace("json", "sql");
ImportSQL(sql_folder + sqlFile);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sqlFile}");
// clear any cached instances for this landblock
DatabaseManager.World.ClearCachedInstancesByLandblock(landblockId);
}
private static void ImportJsonQuest(Session session, string json_folder, string json_file)
{
var questName = json_file.TrimEnd(".json");
// convert json -> sql
var sqlFile = json2sql_quest(session, json_folder, json_file);
if (sqlFile == null) return;
// import sql to db
var sql_folder = json_folder.Replace("json", "sql");
ImportSQL(sql_folder + sqlFile);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sqlFile}");
// clear cached quest
DatabaseManager.World.ClearCachedQuest(questName);
}
public static WeenieSQLWriter WeenieSQLWriter;
/// <summary>
/// Converts a json file to sql file
/// </summary>
public static string json2sql_weenie(Session session, string folder, string json_filename)
{
var json_file = folder + json_filename;
// read json into lsd weenie
var success = LifestonedLoader.TryLoadWeenie(json_file, out var weenie);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to load {json_file}");
return null;
}
// convert to ace weenie
success = LifestonedConverter.TryConvert(weenie, out var output);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
// output to sql
var sqlFolder = folder.Replace("json", "sql");
var di = new DirectoryInfo(sqlFolder);
if (!di.Exists)
di.Create();
var sqlFilename = "";
try
{
if (WeenieSQLWriter == null)
{
WeenieSQLWriter = new WeenieSQLWriter();
WeenieSQLWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
WeenieSQLWriter.SpellNames = DatabaseManager.World.GetAllSpellNames();
WeenieSQLWriter.TreasureDeath = DatabaseManager.World.GetAllTreasureDeath();
WeenieSQLWriter.TreasureWielded = DatabaseManager.World.GetAllTreasureWielded();
}
if (output.LastModified == DateTime.MinValue)
output.LastModified = DateTime.UtcNow;
sqlFilename = WeenieSQLWriter.GetDefaultFileName(output);
using (StreamWriter sqlFile = new StreamWriter(sqlFolder + sqlFilename))
{
WeenieSQLWriter.CreateSQLDELETEStatement(output, sqlFile);
sqlFile.WriteLine();
WeenieSQLWriter.CreateSQLINSERTStatement(output, sqlFile);
var metadata = new Adapter.GDLE.Models.Metadata(weenie);
if (metadata.HasInfo)
{
var jsonEx = JsonSerializer.Serialize(metadata, LifestonedConverter.SerializerSettings);
sqlFile.WriteLine($"\n/* Lifestoned Changelog:\n{jsonEx}\n*/");
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {json_filename} to {sqlFilename}");
return sqlFilename;
}
public static CookBookSQLWriter CookBookSQLWriter;
public static RecipeSQLWriter RecipeSQLWriter;
public static SpellSQLWriter SpellSQLWriter;
public static string json2sql_recipe(Session session, string folder, string json_filename)
{
var json_file = folder + json_filename;
// read json into lsd recipe
var success = GDLELoader.TryLoadRecipeCombined(json_file, out var result);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to load {json_file}");
return null;
}
// convert to ace cookbooks + recipe
success = GDLEConverter.TryConvert(result, out var cookbooks, out var recipe);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
// output to sql
var sqlFolder = folder.Replace("json", "sql");
var di = new DirectoryInfo(sqlFolder);
if (!di.Exists)
di.Create();
var sqlFilename = "";
try
{
if (RecipeSQLWriter == null)
{
RecipeSQLWriter = new RecipeSQLWriter();
RecipeSQLWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
if (CookBookSQLWriter == null)
{
CookBookSQLWriter = new CookBookSQLWriter();
CookBookSQLWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
if (recipe.LastModified == DateTime.MinValue)
recipe.LastModified = DateTime.UtcNow;
foreach (var cookbook in cookbooks)
{
if (cookbook.LastModified == DateTime.MinValue)
cookbook.LastModified = DateTime.UtcNow;
}
sqlFilename = RecipeSQLWriter.GetDefaultFileName(recipe, cookbooks);
using (StreamWriter sqlFile = new StreamWriter(sqlFolder + sqlFilename)) {
RecipeSQLWriter.CreateSQLDELETEStatement(recipe, sqlFile);
sqlFile.WriteLine();
RecipeSQLWriter.CreateSQLINSERTStatement(recipe, sqlFile);
sqlFile.WriteLine();
CookBookSQLWriter.CreateSQLDELETEStatement(cookbooks, sqlFile);
sqlFile.WriteLine();
CookBookSQLWriter.CreateSQLINSERTStatement(cookbooks, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {json_filename} to {sqlFilename}");
return sqlFilename;
}
public static string json2sql_landblock(Session session, string folder, string json_filename)
{
var json_file = folder + json_filename;
// read json into gdle spawnmap
var success = GDLELoader.TryLoadLandblock(json_file, out var result);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to load {json_file}");
return null;
}
// convert to ace landblock_instances
success = GDLEConverter.TryConvert(result, out var landblockInstances, out var landblockInstanceLinks);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
// link up instances
// TODO: move this to TryConvert
foreach (var link in landblockInstanceLinks)
{
var parent = landblockInstances.FirstOrDefault(i => i.Guid == link.ParentGuid);
if (parent == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find parent guid for {link.ParentGuid:X8}");
continue;
}
parent.LandblockInstanceLink.Add(link);
var child = landblockInstances.FirstOrDefault(i => i.Guid == link.ChildGuid);
if (child == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find child guid for {link.ChildGuid:X8}");
continue;
}
child.IsLinkChild = true;
}
// output to sql
var sqlFolder = folder.Replace("spawnmaps", "landblock_instances").Replace("json", "sql");
var di = new DirectoryInfo(sqlFolder);
if (!di.Exists)
di.Create();
var sqlFilename = "";
try
{
if (LandblockInstanceWriter == null)
{
LandblockInstanceWriter = new LandblockInstanceWriter();
LandblockInstanceWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
foreach (var landblockInstance in landblockInstances)
{
if (landblockInstance.LastModified == DateTime.MinValue)
landblockInstance.LastModified = DateTime.UtcNow;
}
foreach (var landblockInstanceLink in landblockInstanceLinks)
{
if (landblockInstanceLink.LastModified == DateTime.MinValue)
landblockInstanceLink.LastModified = DateTime.UtcNow;
}
sqlFilename = LandblockInstanceWriter.GetDefaultFileName(landblockInstances[0]);
using (StreamWriter sqlFile = new StreamWriter(sqlFolder + sqlFilename))
{
LandblockInstanceWriter.CreateSQLDELETEStatement(landblockInstances, sqlFile);
sqlFile.WriteLine();
LandblockInstanceWriter.CreateSQLINSERTStatement(landblockInstances, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {json_filename} to {sqlFilename}");
return sqlFilename;
}
public static QuestSQLWriter QuestSQLWriter;
public static string json2sql_quest(Session session, string folder, string json_filename)
{
var json_file = folder + json_filename;
// read json quest
var success = GDLELoader.TryLoadQuest(json_file, out var result);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to load {json_file}");
return null;
}
// convert to sql quest
success = GDLEConverter.TryConvert(result, out var quest);
if (!success)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
// output to sql
var sqlFolder = folder.Replace("json", "sql");
var sqlFilename = json_filename.Replace(".json", ".sql");
var di = new DirectoryInfo(sqlFolder);
if (!di.Exists)
di.Create();
try
{
if (QuestSQLWriter == null)
QuestSQLWriter = new QuestSQLWriter();
if (quest.LastModified == DateTime.MinValue)
quest.LastModified = DateTime.UtcNow;
using (StreamWriter sqlFile = new StreamWriter(sqlFolder + sqlFilename))
{
QuestSQLWriter.CreateSQLDELETEStatement(quest, sqlFile);
sqlFile.WriteLine();
QuestSQLWriter.CreateSQLINSERTStatement(quest, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {json_file}");
return null;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {json_filename} to {sqlFilename}");
return sqlFilename;
}
/// <summary>
/// Converts SQL to JSON, imports to database, clears the weenie cache
/// </summary>
private static void ImportSQLWeenie(Session session, string sql_folder, string sql_file)
{
if (!uint.TryParse(Regex.Match(sql_file, @"\d+").Value, out var wcid))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find wcid from {sql_file}");
return;
}
// import sql to db
ImportSQL(sql_folder + sql_file);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sql_file}");
// clear this weenie out of the cache
DatabaseManager.World.ClearCachedWeenie(wcid);
// load weenie from database
var weenie = DatabaseManager.World.GetWeenie(wcid);
if (weenie == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't load weenie {wcid} from db");
return;
}
sql2json_weenie(session, weenie, sql_folder, sql_file);
}
private static void ImportSQLRecipe(Session session, string sql_folder, string sql_file)
{
if (!uint.TryParse(Regex.Match(sql_file, @"\d+").Value, out var recipeId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find recipe id from {sql_file}");
return;
}
// import sql to db
ImportSQL(sql_folder + sql_file);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sql_file}");
// clear this recipe out of the cache
DatabaseManager.World.ClearCookbookCache();
// load cookbooks + recipe from database
var cookbooks = DatabaseManager.World.GetCookbooksByRecipeId(recipeId);
if (cookbooks == null || cookbooks.Count == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't load recipe {recipeId} from db");
return;
}
sql2json_recipe(session, cookbooks, sql_folder, sql_file);
}
private static void ImportSQLLandblock(Session session, string sql_folder, string sql_file)
{
if (!ushort.TryParse(Regex.Match(sql_file, @"[0-9A-F]{4}", RegexOptions.IgnoreCase).Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var landblockId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find landblock id from {sql_file}");
return;
}
// import sql to db
ImportSQL(sql_folder + sql_file);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sql_file}");
// clear any cached instances for this landblock
DatabaseManager.World.ClearCachedInstancesByLandblock(landblockId);
// load landblock instances from database
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblockId);
// convert to json file
sql2json_landblock(session, instances, sql_folder, sql_file);
}
private static void ImportSQLEncounter(Session session, string sql_folder, string sql_file)
{
if (!ushort.TryParse(Regex.Match(sql_file, @"[0-9A-F]{4}", RegexOptions.IgnoreCase).Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var landblockId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find landblock id from {sql_file}");
return;
}
// import sql to db
ImportSQL(sql_folder + sql_file);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sql_file}");
// clear any cached encounters for this landblock
DatabaseManager.World.ClearCachedEncountersByLandblock(landblockId);
}
private static void ImportSQLEvent(Session session, string sql_folder, string sql_file)
{
// import sql to db
ImportSQL(sql_folder + sql_file);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sql_file}");
// clear cached event
var eventName = sql_file.TrimEnd(".sql");
DatabaseManager.World.ClearCachedEvent(eventName);
if (EventManager.IsEventAvailable(eventName))
{
if (EventManager.IsEventStarted(eventName, null, null))
{
EventManager.StopEvent(eventName, null, null);
CommandHandlerHelper.WriteOutputInfo(session, $"-- Event {eventName} has been stopped.");
}
if (EventManager.Events.Remove(eventName))
{
CommandHandlerHelper.WriteOutputInfo(session, $"-- Event {eventName} has been removed from EventManager.");
}
}
// load event from db
var evt = DatabaseManager.World.GetCachedEvent(eventName);
if (EventManager.Events.TryAdd(evt.Name, evt))
{
CommandHandlerHelper.WriteOutputInfo(session, $"-- Event {eventName} has been added to EventManager.");
}
// Start the event if it needs to be
if (evt.State == (int)GameEventState.On)
{
if (EventManager.StartEvent(evt.Name, null, null))
{
CommandHandlerHelper.WriteOutputInfo(session, $"-- Event {eventName} has been started.");
}
}
}
private static void ImportSQLQuest(Session session, string sql_folder, string sql_file)
{
// import sql to db
ImportSQL(sql_folder + sql_file);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sql_file}");
// clear cached quest
var questName = sql_file.TrimEnd(".sql");
DatabaseManager.World.ClearCachedQuest(questName);
// load quest from db
var quest = DatabaseManager.World.GetCachedQuest(questName);
// convert to json file
sql2json_quest(session, quest, sql_folder, sql_file);
}
private static void ImportSQLSpell(Session session, string sql_folder, string sql_file)
{
if (!uint.TryParse(Regex.Match(sql_file, @"\d+").Value, out var spellId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find spell id from {sql_file}");
return;
}
// import sql to db
ImportSQL(sql_folder + sql_file);
CommandHandlerHelper.WriteOutputInfo(session, $"Imported {sql_file}");
// clear this spell out of the cache (and everything else)
DatabaseManager.World.ClearSpellCache();
WorldObject.ClearSpellCache();
// load spell from db
var spell = DatabaseManager.World.GetCachedSpell(spellId);
}
/// <summary>
/// Converts a sql file to json file
/// </summary>
public static bool sql2json_weenie(Session session, Weenie weenie, string sql_folder, string sql_filename)
{
if (!LifestonedConverter.TryConvertACEWeenieToLSDJSON(weenie, out var json, out var json_weenie))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {sql_filename} to json");
return false;
}
var json_folder = sql_folder.Replace("sql", "json");
var json_filename = sql_filename.Replace(".sql", ".json");
var match = Regex.Match(json_filename, @"^(\d+)");
if (match.Success)
{
var wcid = match.Groups[1].Value;
if (!json_filename.StartsWith(wcid + " -"))
json_filename = wcid + " -" + json_filename.Substring(wcid.Length);
}
var di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
if (File.Exists(json_folder + json_filename) && LifestonedLoader.AppendMetadata(json_folder + json_filename, json_weenie))
{
json = JsonSerializer.Serialize(json_weenie, LifestonedConverter.SerializerSettings);
}
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {sql_filename} to {json_filename}");
return true;
}
public static bool sql2json_recipe(Session session, List<CookBook> cookbooks, string sql_folder, string sql_filename)
{
if (!GDLEConverter.TryConvert(cookbooks, out var result))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {sql_filename} to json");
return false;
}
var json_folder = sql_folder.Replace("sql", "json");
var json_filename = sql_filename.Replace(".sql", ".json");
var match = Regex.Match(json_filename, @"^(\d+)");
if (match.Success)
{
var wcid = match.Groups[1].Value;
if (!json_filename.StartsWith(wcid + " -"))
json_filename = wcid + " -" + json_filename.Substring(wcid.Length);
}
var di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
var json = JsonSerializer.Serialize(result, LifestonedConverter.SerializerSettings);
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {sql_filename} to {json_filename}");
return true;
}
public static bool sql2json_landblock(Session session, List<LandblockInstance> instances, string sql_folder, string sql_filename)
{
if (GDLEConverter.WeenieNames == null)
GDLEConverter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
if (GDLEConverter.WeenieClassNames == null)
GDLEConverter.WeenieClassNames = DatabaseManager.World.GetAllWeenieClassNames();
if (!GDLEConverter.TryConvert(instances, out var result))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {sql_filename} to json");
return false;
}
var json_folder = sql_folder.Replace("sql", "json");
var json_filename = sql_filename.Replace(".sql", ".json");
var di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
var json = JsonSerializer.Serialize(result, LifestonedConverter.SerializerSettings);
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {sql_filename} to {json_filename}");
return true;
}
public static bool sql2json_quest(Session session, Quest quest, string sql_folder, string sql_filename)
{
if (!GDLEConverter.TryConvert(quest, out var result))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {sql_filename} to json");
return false;
}
var json_folder = sql_folder.Replace("sql", "json");
var json_filename = sql_filename.Replace(".sql", ".json");
var di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
var json = JsonSerializer.Serialize(result, LifestonedConverter.SerializerSettings);
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Converted {sql_filename} to {json_filename}");
return true;
}
/// <summary>
/// Imports an SQL file into the database
/// </summary>
public static void ImportSQL(string sqlFile)
{
var sqlCommands = File.ReadAllText(sqlFile);
sqlCommands = sqlCommands.Replace("\r\n", "\n");
// not sure why ExecuteSqlCommand doesn't parse this correctly..
var idx = sqlCommands.IndexOf($"/* Lifestoned Changelog:");
if (idx != -1)
sqlCommands = sqlCommands.Substring(0, idx);
using (var ctx = new WorldDbContext())
ctx.Database.ExecuteSqlRaw(sqlCommands);
}
public static LandblockInstanceWriter LandblockInstanceWriter;
[CommandHandler("createinst", AccessLevel.Developer, CommandHandlerFlag.RequiresWorld, 1, "Spawns a new wcid or classname as a landblock instance", "<wcid or classname>\n\nTo create a parent/child relationship: /createinst -p <parent guid> -c <wcid or classname>\nTo automatically get the parent guid from the last appraised object: /createinst -p -c <wcid or classname>\n\nTo manually specify a start guid: /createinst <wcid or classname> <start guid>\nStart guids can be in the range 0x000-0xFFF, or they can be prefixed with 0x7<landblock id>")]
public static void HandleCreateInst(Session session, params string[] parameters)
{
var loc = new Position(session.Player.Location);
var param = parameters[0];
Weenie weenie = null;
uint? parentGuid = null;
var landblock = session.Player.CurrentLandblock.Id.Landblock;
var firstStaticGuid = 0x70000000 | (uint)landblock << 12;
if (parameters.Length > 1)
{
var allParams = string.Join(" ", parameters);
var match = Regex.Match(allParams, @"-p ([\S]+) -c ([\S]+)", RegexOptions.IgnoreCase);
if (match.Success)
{
var parentGuidStr = match.Groups[1].Value;
param = match.Groups[2].Value;
if (parentGuidStr.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
parentGuidStr = parentGuidStr.Substring(2);
if (!uint.TryParse(parentGuidStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var _parentGuid))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't parse parent guid {match.Groups[1].Value}", ChatMessageType.Broadcast));
return;
}
parentGuid = _parentGuid;
if (parentGuid <= 0xFFF)
parentGuid = firstStaticGuid | parentGuid;
}
else if (parameters[1].StartsWith("-c", StringComparison.OrdinalIgnoreCase))
{
// get parent from last appraised object
var parent = CommandHandlerHelper.GetLastAppraisedObject(session);
if (parent == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find parent object", ChatMessageType.Broadcast));
return;
}
parentGuid = parent.Guid.Full;
}
}
if (uint.TryParse(param, out var wcid))
weenie = DatabaseManager.World.GetWeenie(wcid); // wcid
else
weenie = DatabaseManager.World.GetWeenie(param); // classname
if (weenie == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find weenie {param}", ChatMessageType.Broadcast));
return;
}
// clear any cached instances for this landblock
DatabaseManager.World.ClearCachedInstancesByLandblock(landblock);
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblock);
// for link mode, ensure parent guid instance exists
WorldObject parentObj = null;
LandblockInstance parentInstance = null;
if (parentGuid != null)
{
parentInstance = instances.FirstOrDefault(i => i.Guid == parentGuid);
if (parentInstance == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find landblock instance for parent guid 0x{parentGuid:X8}", ChatMessageType.Broadcast));
return;
}
parentObj = session.Player.CurrentLandblock.GetObject(parentGuid.Value);
if (parentObj == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find parent object 0x{parentGuid:X8}", ChatMessageType.Broadcast));
return;
}
}
var nextStaticGuid = GetNextStaticGuid(landblock, instances);
var maxStaticGuid = firstStaticGuid | 0xFFF;
// manually specify a start guid?
if (parameters.Length == 2)
{
if (uint.TryParse(parameters[1].Replace("0x", ""), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var startGuid))
{
if (startGuid <= 0xFFF)
startGuid = firstStaticGuid | startGuid;
if (startGuid < firstStaticGuid || startGuid > maxStaticGuid)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Landblock instance guid {startGuid:X8} must be between {firstStaticGuid:X8} and {maxStaticGuid:X8}", ChatMessageType.Broadcast));
return;
}
var existing = instances.FirstOrDefault(i => i.Guid == startGuid);
if (existing != null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Landblock instance guid {startGuid:X8} already exists", ChatMessageType.Broadcast));
return;
}
nextStaticGuid = startGuid;
}
}
if (nextStaticGuid > maxStaticGuid)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Landblock {landblock:X4} has reached the maximum # of static guids", ChatMessageType.Broadcast));
return;
}
// create and spawn object
var entityWeenie = Database.Adapter.WeenieConverter.ConvertToEntityWeenie(weenie);
var wo = WorldObjectFactory.CreateWorldObject(entityWeenie, new ObjectGuid(nextStaticGuid));
if (wo == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to create new object for {weenie.ClassId} - {weenie.ClassName}", ChatMessageType.Broadcast));
return;
}
var isLinkChild = parentInstance != null;
if (!wo.Stuck && !isLinkChild)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{weenie.ClassId} - {weenie.ClassName} is missing PropertyBool.Stuck, cannot spawn as landblock instance unless it is a child object", ChatMessageType.Broadcast));
return;
}
// spawn as ethereal temporarily, to spawn directly on player position
wo.Ethereal = true;
wo.Location = new Position(loc);
// even on flat ground, objects can sometimes fail to spawn at the player's current Z
// Position.Z has some weird thresholds when moving around, but i guess the same logic doesn't apply when trying to spawn in...
wo.Location.PositionZ += 0.05f;
session.Network.EnqueueSend(new GameMessageSystemChat($"Creating new landblock instance {(isLinkChild ? "child object " : "")}@ {loc.ToLOCString()}\n{wo.WeenieClassId} - {wo.Name} ({nextStaticGuid:X8})", ChatMessageType.Broadcast));
if (!wo.EnterWorld())
{
session.Network.EnqueueSend(new GameMessageSystemChat("Failed to spawn new object at this location", ChatMessageType.Broadcast));
return;
}
// create new landblock instance
var instance = CreateLandblockInstance(wo, isLinkChild);
instances.Add(instance);
if (isLinkChild)
{
var link = new LandblockInstanceLink();
link.ParentGuid = parentGuid.Value;
link.ChildGuid = wo.Guid.Full;
link.LastModified = DateTime.Now;
parentInstance.LandblockInstanceLink.Add(link);
parentObj.LinkedInstances.Add(instance);
// ActivateLinks?
parentObj.SetLinkProperties(wo);
parentObj.ChildLinks.Add(wo);
wo.ParentLink = parentObj;
}
SyncInstances(session, landblock, instances);
}
/// <summary>
/// Serializes landblock instances to XXYY.sql file,
/// import into database, and clears the cached landblock instances
/// </summary>
public static void SyncInstances(Session session, ushort landblock, List<LandblockInstance> instances)
{
// serialize to .sql file
var contentFolder = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
var folder = new DirectoryInfo($"{contentFolder.FullName}{sep}sql{sep}landblocks{sep}");
if (!folder.Exists)
folder.Create();
var sqlFilename = $"{folder.FullName}{sep}{landblock:X4}.sql";
if (instances.Count > 0)
{
var fileWriter = new StreamWriter(sqlFilename);
if (LandblockInstanceWriter == null)
{
LandblockInstanceWriter = new LandblockInstanceWriter();
LandblockInstanceWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
LandblockInstanceWriter.CreateSQLDELETEStatement(instances, fileWriter);
fileWriter.WriteLine();
LandblockInstanceWriter.CreateSQLINSERTStatement(instances, fileWriter);
fileWriter.Close();
// import into db
ImportSQL(sqlFilename);
}
else
{
// handle special case: deleting the last instance from landblock
File.Delete(sqlFilename);
using (var ctx = new WorldDbContext())
ctx.Database.ExecuteSqlInterpolated($"DELETE FROM landblock_instance WHERE landblock={landblock};");
}
// clear landblock instances for this landblock (again)
DatabaseManager.World.ClearCachedInstancesByLandblock(landblock);
}
public static LandblockInstance CreateLandblockInstance(WorldObject wo, bool isLinkChild = false)
{
var instance = new LandblockInstance();
instance.Guid = wo.Guid.Full;
instance.Landblock = (int)wo.Location.Landblock;
instance.WeenieClassId = wo.WeenieClassId;
instance.ObjCellId = wo.Location.Cell;
instance.OriginX = wo.Location.PositionX;
instance.OriginY = wo.Location.PositionY;
instance.OriginZ = wo.Location.PositionZ;
instance.AnglesW = wo.Location.RotationW;
instance.AnglesX = wo.Location.RotationX;
instance.AnglesY = wo.Location.RotationY;
instance.AnglesZ = wo.Location.RotationZ;
instance.IsLinkChild = isLinkChild;
instance.LastModified = DateTime.Now;
return instance;
}
public static uint GetNextStaticGuid(ushort landblock, List<LandblockInstance> instances)
{
var firstGuid = 0x70000000 | ((uint)landblock << 12);
var lastGuid = firstGuid | 0xFFF;
var highestLandblockInst = instances.Where(i => i.Landblock == landblock).OrderByDescending(i => i.Guid).FirstOrDefault();
if (highestLandblockInst == null)
return firstGuid;
var nextGuid = highestLandblockInst.Guid + 1;
if (nextGuid <= lastGuid)
return nextGuid;
// try more exhaustive search
return GetNextStaticGuid_GapFinder(landblock, instances) ?? nextGuid;
}
public static uint? GetNextStaticGuid_GapFinder(ushort landblock, List<LandblockInstance> instances)
{
var landblockGuids = instances.Where(i => i.Landblock == landblock).Select(i => i.Guid).ToHashSet();
var firstGuid = 0x70000000 | ((uint)landblock << 12);
var lastGuid = firstGuid | 0xFFF;
for (var guid = firstGuid; guid <= lastGuid; guid++)
{
if (!landblockGuids.Contains(guid))
return guid;
}
return null;
}
[CommandHandler("removeinst", AccessLevel.Developer, CommandHandlerFlag.RequiresWorld, "Removes the last appraised object from the current landblock instances")]
public static void HandleRemoveInst(Session session, params string[] parameters)
{
RemoveInstance(session);
}
public static void RemoveInstance(Session session, bool confirmed = false)
{
var wo = CommandHandlerHelper.GetLastAppraisedObject(session);
if (wo?.Location == null) return;
var landblock = (ushort)wo.Location.Landblock;
// if generator child, try getting the "real" guid
var guid = wo.Guid.Full;
if (wo.Generator != null)
{
var staticGuid = wo.Generator.GetStaticGuid(guid);
if (staticGuid != null)
guid = staticGuid.Value;
}
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblock);
var instance = instances.FirstOrDefault(i => i.Guid == guid);
if (instance == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find landblock_instance for {wo.WeenieClassId} - {wo.Name} (0x{guid:X8})", ChatMessageType.Broadcast));
return;
}
var numChilds = instance.LandblockInstanceLink.Count;
if (numChilds > 0 && !confirmed)
{
// get total numChilds iteratively
numChilds = 0;
foreach (var link in instance.LandblockInstanceLink)
numChilds += GetNumChilds(session, link, instances);
// require confirmation for parent objects
var msg = $"Are you sure you want to delete this parent object, and {numChilds} child object{(numChilds != 1 ? "s" : "")}?";
if (!session.Player.ConfirmationManager.EnqueueSend(new Confirmation_Custom(session.Player.Guid, () => RemoveInstance(session, true)), msg))
session.Player.SendWeenieError(WeenieError.ConfirmationInProgress);
return;
}
if (instance.IsLinkChild)
{
LandblockInstanceLink link = null;
foreach (var parent in instances.Where(i => i.LandblockInstanceLink.Count > 0))
{
link = parent.LandblockInstanceLink.FirstOrDefault(i => i.ChildGuid == instance.Guid);
if (link != null)
{
parent.LandblockInstanceLink.Remove(link);
break;
}
}
if (link == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find parent link for child {wo.WeenieClassId} - {wo.Name} (0x{guid:X8})", ChatMessageType.Broadcast));
return;
}
}
wo.DeleteObject();
foreach (var link in instance.LandblockInstanceLink)
RemoveChild(session, link, instances);
instances.Remove(instance);
SyncInstances(session, landblock, instances);
session.Network.EnqueueSend(new GameMessageSystemChat($"Removed {(instance.IsLinkChild ? "child " : "")}{wo.WeenieClassId} - {wo.Name} (0x{guid:X8}) from landblock instances", ChatMessageType.Broadcast));
}
public static int GetNumChilds(Session session, LandblockInstanceLink link, List<LandblockInstance> instances)
{
var child = instances.FirstOrDefault(i => i.Guid == link.ChildGuid);
if (child == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find child instance 0x{link.ChildGuid:X8}", ChatMessageType.Broadcast));
return 0;
}
var numChilds = 1;
foreach (var subLink in child.LandblockInstanceLink)
numChilds += GetNumChilds(session, subLink, instances);
return numChilds;
}
public static void RemoveChild(Session session, LandblockInstanceLink link, List<LandblockInstance> instances)
{
var child = instances.FirstOrDefault(i => i.Guid == link.ChildGuid);
if (child == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find child instance 0x{link.ChildGuid:X8}", ChatMessageType.Broadcast));
return;
}
instances.Remove(child);
var wo = session.Player.CurrentLandblock.GetObject(child.Guid);
if (wo != null)
{
wo.DeleteObject();
session.Network.EnqueueSend(new GameMessageSystemChat($"Removed child {wo.WeenieClassId} - {wo.Name} (0x{wo.Guid}) from landblock instances", ChatMessageType.Broadcast));
}
else
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find child object for 0x{link.ChildGuid:X8}", ChatMessageType.Broadcast));
foreach (var subLink in child.LandblockInstanceLink)
RemoveChild(session, subLink, instances);
}
public static EncounterSQLWriter LandblockEncounterWriter;
[CommandHandler("addenc", AccessLevel.Developer, CommandHandlerFlag.RequiresWorld, 1, "Spawns a new wcid or classname in the current outdoor cell as an encounter", "<wcid or classname>")]
public static void HandleAddEncounter(Session session, params string[] parameters)
{
var param = parameters[0];
Weenie weenie = null;
if (uint.TryParse(param, out var wcid))
weenie = DatabaseManager.World.GetWeenie(wcid); // wcid
else
weenie = DatabaseManager.World.GetWeenie(param); // classname
if (weenie == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find weenie {param}", ChatMessageType.Broadcast));
return;
}
var pos = session.Player.Location;
if ((pos.Cell & 0xFFFF) >= 0x100)
{
session.Network.EnqueueSend(new GameMessageSystemChat("You must be outdoors to create an encounter!", ChatMessageType.Broadcast));
return;
}
var cellX = (int)pos.PositionX / 24;
var cellY = (int)pos.PositionY / 24;
var landblock = (ushort)pos.Landblock;
// clear any cached encounters for this landblock
DatabaseManager.World.ClearCachedEncountersByLandblock(landblock);
// get existing encounters for this landblock
var encounters = DatabaseManager.World.GetCachedEncountersByLandblock(landblock);
// check for existing encounter
if (encounters.Any(i => i.CellX == cellX && i.CellY == cellY))
{
session.Network.EnqueueSend(new GameMessageSystemChat("This cell already contains an encounter!", ChatMessageType.Broadcast));
return;
}
// spawn encounter
var wo = SpawnEncounter(weenie, cellX, cellY, pos, session);
if (wo == null) return;
session.Network.EnqueueSend(new GameMessageSystemChat($"Creating new encounter @ landblock {pos.Landblock:X4}, cellX={cellX}, cellY={cellY}\n{wo.WeenieClassId} - {wo.Name}", ChatMessageType.Broadcast));
// add a new encounter (verifications?)
var encounter = new Encounter();
encounter.Landblock = (int)pos.Landblock;
encounter.CellX = cellX;
encounter.CellY = cellY;
encounter.WeenieClassId = weenie.ClassId;
encounter.LastModified = DateTime.Now;
encounters.Add(encounter);
// write encounters to sql file / load into db
SyncEncounters(session, landblock, encounters);
}
public static WorldObject SpawnEncounter(Weenie weenie, int cellX, int cellY, Position pos, Session session)
{
var wo = WorldObjectFactory.CreateNewWorldObject(weenie.ClassId);
if (wo == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to create encounter weenie", ChatMessageType.Broadcast));
return null;
}
if (!wo.IsGenerator)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Encounter must be a Generator", ChatMessageType.Broadcast));
return null;
}
var xPos = Math.Clamp(cellX * 24.0f, 0.5f, 191.5f);
var yPos = Math.Clamp(cellY * 24.0f, 0.5f, 191.5f);
var newPos = new Physics.Common.Position();
newPos.ObjCellID = pos.Cell;
newPos.Frame = new Physics.Animation.AFrame(new Vector3(xPos, yPos, 0), Quaternion.Identity);
newPos.adjust_to_outside();
newPos.Frame.Origin.Z = session.Player.CurrentLandblock.PhysicsLandblock.GetZ(newPos.Frame.Origin);
wo.Location = new Position(newPos.ObjCellID, newPos.Frame.Origin, newPos.Frame.Orientation);
var sortCell = Physics.Common.LScape.get_landcell(newPos.ObjCellID) as Physics.Common.SortCell;
if (sortCell != null && sortCell.has_building())
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to create encounter near building cell", ChatMessageType.Broadcast));
return null;
}
if (PropertyManager.GetBool("override_encounter_spawn_rates").Item)
{
wo.RegenerationInterval = PropertyManager.GetDouble("encounter_regen_interval").Item;
wo.ReinitializeHeartbeats();
if (wo.Biota.PropertiesGenerator != null)
{
// While this may be ugly, it's done for performance reasons.
// Common weenie properties are not cloned into the bota on creation. Instead, the biota references simply point to the weenie collections.
// The problem here is that we want to update one of those common collection properties. If the biota is referencing the weenie collection,
// then we'll end up updating the global weenie (from the cache), instead of just this specific biota.
if (wo.Biota.PropertiesGenerator == wo.Weenie.PropertiesGenerator)
{
wo.Biota.PropertiesGenerator = new List<ACE.Entity.Models.PropertiesGenerator>(wo.Weenie.PropertiesGenerator.Count);
foreach (var record in wo.Weenie.PropertiesGenerator)
wo.Biota.PropertiesGenerator.Add(record.Clone());
}
}
}
var success = wo.EnterWorld();
if (!success)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to spawn encounter", ChatMessageType.Broadcast));
return null;
}
return wo;
}
/// <summary>
/// Serializes encounters to XXYY.sql file,
/// import into database, and clears the cached encounters
/// </summary>
public static void SyncEncounters(Session session, ushort landblock, List<Encounter> encounters)
{
// serialize to .sql file
var contentFolder = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
var folder = new DirectoryInfo($"{contentFolder.FullName}{sep}sql{sep}encounters{sep}");
if (!folder.Exists)
folder.Create();
var sqlFilename = $"{folder.FullName}{sep}{landblock:X4}.sql";
if (encounters.Count > 0)
{
var fileWriter = new StreamWriter(sqlFilename);
if (LandblockEncounterWriter == null)
{
LandblockEncounterWriter = new EncounterSQLWriter();
LandblockEncounterWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
LandblockEncounterWriter.CreateSQLDELETEStatement(encounters, fileWriter);
fileWriter.WriteLine();
LandblockEncounterWriter.CreateSQLINSERTStatement(encounters, fileWriter);
fileWriter.Close();
// import into db
ImportSQL(sqlFilename);
}
else
{
// handle special case: deleting the last encounter from landblock
File.Delete(sqlFilename);
using (var ctx = new WorldDbContext())
ctx.Database.ExecuteSqlInterpolated($"DELETE FROM encounter WHERE landblock={landblock};");
}
// clear the encounters for this landblock (again)
DatabaseManager.World.ClearCachedEncountersByLandblock(landblock);
}
[CommandHandler("removeenc", AccessLevel.Developer, CommandHandlerFlag.RequiresWorld, "Removes the last appraised object from the encounters table")]
public static void HandleRemoveEnc(Session session, params string[] parameters)
{
var obj = CommandHandlerHelper.GetLastAppraisedObject(session);
if (obj == null)
return;
// find root generator
while (obj.Generator != null)
obj = obj.Generator;
var cellX = (int)obj.Location.PositionX / 24;
var cellY = (int)obj.Location.PositionY / 24;
var landblock = (ushort)obj.Location.Landblock;
// clear any cached encounters for this landblock
DatabaseManager.World.ClearCachedEncountersByLandblock(landblock);
// get existing encounters for this landblock
var encounters = DatabaseManager.World.GetCachedEncountersByLandblock(landblock);
// check for existing encounter
var encounter = encounters.FirstOrDefault(i => i.CellX == cellX && i.CellY == cellY && i.WeenieClassId == obj.WeenieClassId);
if (encounter == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find encounter for {obj.WeenieClassId} - {obj.Name}", ChatMessageType.Broadcast));
return;
}
session.Network.EnqueueSend(new GameMessageSystemChat($"Removing encounter @ landblock {obj.Location.Landblock:X4}, cellX={cellX}, cellY={cellY}\n{obj.WeenieClassId} - {obj.Name}", ChatMessageType.Broadcast));
encounters.Remove(encounter);
SyncEncounters(session, landblock, encounters);
// this is needed for any generators that don't have GeneratorDestructionType
DestroyAll(obj);
}
/// <summary>
/// Destroys a parent generator, and all of its child objects
/// </summary>
private static void DestroyAll(WorldObject wo)
{
wo.Destroy();
if (wo.GeneratorProfiles == null)
return;
foreach (var profile in wo.GeneratorProfiles)
{
foreach (var kvp in profile.Spawned)
{
var child = kvp.Value.TryGetWorldObject();
if (child != null)
DestroyAll(child);
}
}
}
[CommandHandler("export-json-folders", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Exports content from database to JSON file in a WeenieType/ItemType folder structure", "<wcid>")]
public static void HandleExportJsonFolder(Session session, params string[] parameters)
{
var param = parameters[0];
ExportJsonWeenie(session, param, true);
}
[CommandHandler("export-json", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Exports content from database to JSON file", "<optional type> <id>\n<optional type> - landblock, quest, recipe, spell, weenie (default if not specified)\n<id> - wcid or content id to export")]
public static void HandleExportJson(Session session, params string[] parameters)
{
var param = parameters[0];
var contentType = FileType.Weenie;
if (parameters.Length > 1)
{
contentType = GetContentType(parameters, ref param);
if (contentType == FileType.Undefined)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unknown content type '{parameters[1]}'");
return;
}
}
switch (contentType)
{
case FileType.LandblockInstance:
ExportJsonLandblock(session, param);
break;
case FileType.Quest:
ExportJsonQuest(session, param);
break;
case FileType.Recipe:
ExportJsonRecipe(session, param);
break;
case FileType.Weenie:
ExportJsonWeenie(session, param);
break;
}
}
public static void ExportJsonWeenie(Session session, string param, bool withFolders = false)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
Weenie weenie = null;
if (uint.TryParse(param, out var wcid))
weenie = DatabaseManager.World.GetWeenie(wcid);
else
weenie = DatabaseManager.World.GetWeenie(param);
if (weenie == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find weenie {param}");
return;
}
if (!LifestonedConverter.TryConvertACEWeenieToLSDJSON(weenie, out var json, out var json_weenie))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert {weenie.ClassId} - {weenie.ClassName} to json");
return;
}
string json_folder = null;
if (withFolders)
{
var weenieType = (WeenieType)weenie.Type;
switch (weenieType)
{
case WeenieType.Creature: // Export to the "CreatureType" folder
WeeniePropertiesInt cType = (from x in weenie.WeeniePropertiesInt where x.Type == 2 select x).FirstOrDefault();
if (cType == null)
json_folder = $"{di.FullName}{sep}json{sep}weenies{sep}{weenieType}{sep}";
else
{
CreatureType creatureType = (CreatureType)cType.Value;
json_folder = $"{di.FullName}{sep}json{sep}weenies{sep}{weenieType}{sep}{creatureType}{sep}";
}
break;
default: // Otherwise goes to "ItemType" folder
WeeniePropertiesInt iType = (from x in weenie.WeeniePropertiesInt where x.Type == 1 select x).FirstOrDefault();
if (iType == null)
json_folder = $"{di.FullName}{sep}json{sep}weenies{sep}{weenieType}{sep}";
else
{
ItemType itemType = (ItemType)iType.Value;
json_folder = $"{di.FullName}{sep}json{sep}weenies{sep}{weenieType}{sep}{itemType}{sep}";
}
break;
}
}
else
{
json_folder = $"{di.FullName}{sep}json{sep}weenies{sep}";
}
di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
var json_filename = $"{weenie.ClassId} - {weenie.WeeniePropertiesString.FirstOrDefault(i => i.Type == (int)PropertyString.Name)?.Value}.json";
if (File.Exists(json_folder + json_filename) && LifestonedLoader.AppendMetadata(json_folder + json_filename, json_weenie))
{
json = JsonSerializer.Serialize(json_weenie, LifestonedConverter.SerializerSettings);
}
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {json_folder}{json_filename}");
}
public static void ExportJsonRecipe(Session session, string param)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
if (!uint.TryParse(param, out var recipeId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"{param} not a valid recipe id");
return;
}
var cookbooks = DatabaseManager.World.GetCookbooksByRecipeId(recipeId);
if (cookbooks == null || cookbooks.Count == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find recipe id {recipeId}");
return;
}
if (!GDLEConverter.TryConvert(cookbooks, out var result))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert recipe id {recipeId} to json");
return;
}
var json_folder = $"{di.FullName}{sep}json{sep}recipes{sep}";
di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
if (RecipeSQLWriter == null)
{
RecipeSQLWriter = new RecipeSQLWriter();
RecipeSQLWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
var desc = RecipeSQLWriter.GetDefaultFileName(cookbooks[0].Recipe, cookbooks, true);
var json_filename = $"{recipeId.ToString("00000")} - {desc}.json";
var json = JsonSerializer.Serialize(result, LifestonedConverter.SerializerSettings);
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {json_folder}{json_filename}");
}
public static void ExportJsonLandblock(Session session, string param)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
if (!ushort.TryParse(Regex.Match(param, @"[0-9A-F]{4}", RegexOptions.IgnoreCase).Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var landblockId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"{param} not a valid landblock");
return;
}
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblockId);
if (instances == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find landblock {landblockId:X4}");
return;
}
if (GDLEConverter.WeenieNames == null)
GDLEConverter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
if (GDLEConverter.WeenieClassNames == null)
GDLEConverter.WeenieClassNames = DatabaseManager.World.GetAllWeenieClassNames();
if (!GDLEConverter.TryConvert(instances, out var result))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert landblock {landblockId:X4} to json");
return;
}
var json_folder = $"{di.FullName}{sep}json{sep}landblocks{sep}";
di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
var json_filename = $"{landblockId:X4}.json";
var json = JsonSerializer.Serialize(result, LifestonedConverter.SerializerSettings);
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {json_folder}{json_filename}");
}
public static void ExportJsonQuest(Session session, string questName)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
var quest = DatabaseManager.World.GetCachedQuest(questName);
if (quest == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find quest {questName}");
return;
}
if (!GDLEConverter.TryConvert(quest, out var result))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to convert quest {questName} to json");
return;
}
var json_folder = $"{di.FullName}{sep}json{sep}quests{sep}";
di = new DirectoryInfo(json_folder);
if (!di.Exists)
di.Create();
var json_filename = $"{questName}.json";
var json = JsonSerializer.Serialize(result, LifestonedConverter.SerializerSettings);
File.WriteAllText(json_folder + json_filename, json);
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {json_folder}{json_filename}");
}
[CommandHandler("export-sql-folders", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Exports weenie content from database to an SQL file in a WeenieType/ItemType folder structure", "<wcid>")]
public static void HandleExportSqlFolder(Session session, params string[] parameters)
{
var param = parameters[0];
ExportSQLWeenie(session, param, true);
}
[CommandHandler("export-sql", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Exports content from database to SQL file", "<optional type> <id>\n<optional type> - landblock, encounter, event, quest, recipe, spell, weenie (default if not specified)\n<id> - wcid or content id to export")]
public static void HandleExportSql(Session session, params string[] parameters)
{
var param = parameters[0];
var contentType = FileType.Weenie;
if (parameters.Length > 1)
{
contentType = GetContentType(parameters, ref param);
if (contentType == FileType.Undefined)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unknown content type '{parameters[1]}'");
return;
}
}
switch (contentType)
{
case FileType.LandblockInstance:
ExportSQLLandblock(session, param);
break;
case FileType.Encounter:
ExportSQLEncounter(session, param);
break;
case FileType.Event:
ExportSQLEvent(session, param);
break;
case FileType.Quest:
ExportSQLQuest(session, param);
break;
case FileType.Recipe:
ExportSQLRecipe(session, param);
break;
case FileType.Spell:
ExportSQLSpell(session, param);
break;
case FileType.Weenie:
ExportSQLWeenie(session, param);
break;
}
}
public static void ExportSQLWeenie(Session session, string param, bool withFolders = false)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
Weenie weenie = null;
if (uint.TryParse(param, out var wcid))
weenie = DatabaseManager.World.GetWeenie(wcid);
else
weenie = DatabaseManager.World.GetWeenie(param);
if (weenie == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find weenie {param}");
return;
}
string sql_folder = null;
if (withFolders)
{
var weenieType = (WeenieType)weenie.Type;
switch (weenieType)
{
case WeenieType.Creature: // Export to the "CreatureType" folder
WeeniePropertiesInt cType = (from x in weenie.WeeniePropertiesInt where x.Type == 2 select x).FirstOrDefault();
if (cType == null)
sql_folder = $"{di.FullName}{sep}sql{sep}weenies{sep}{weenieType}{sep}";
else
{
CreatureType creatureType = (CreatureType)cType.Value;
sql_folder = $"{di.FullName}{sep}sql{sep}weenies{sep}{weenieType}{sep}{creatureType}{sep}";
}
break;
default: // Otherwise goes to "ItemType" folder
WeeniePropertiesInt iType = (from x in weenie.WeeniePropertiesInt where x.Type == 1 select x).FirstOrDefault();
if (iType == null)
sql_folder = $"{di.FullName}{sep}sql{sep}weenies{sep}{weenieType}{sep}";
else
{
ItemType itemType = (ItemType)iType.Value;
sql_folder = $"{di.FullName}{sep}sql{sep}weenies{sep}{weenieType}{sep}{itemType}{sep}";
}
break;
}
}
else
{
sql_folder = $"{di.FullName}{sep}sql{sep}weenies{sep}";
}
di = new DirectoryInfo(sql_folder);
if (!di.Exists)
di.Create();
if (WeenieSQLWriter == null)
{
WeenieSQLWriter = new WeenieSQLWriter();
WeenieSQLWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
WeenieSQLWriter.SpellNames = DatabaseManager.World.GetAllSpellNames();
WeenieSQLWriter.TreasureDeath = DatabaseManager.World.GetAllTreasureDeath();
WeenieSQLWriter.TreasureWielded = DatabaseManager.World.GetAllTreasureWielded();
WeenieSQLWriter.PacketOpCodes = PacketOpCodeNames.Values;
}
var sql_filename = WeenieSQLWriter.GetDefaultFileName(weenie);
var writer = new StreamWriter(sql_folder + sql_filename);
try
{
WeenieSQLWriter.CreateSQLDELETEStatement(weenie, writer);
writer.WriteLine();
WeenieSQLWriter.CreateSQLINSERTStatement(weenie, writer);
writer.Close();
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to export {sql_folder}{sql_filename}");
return;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {sql_folder}{sql_filename}");
}
public static void ExportSQLRecipe(Session session, string param)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
if (!uint.TryParse(param, out var recipeId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"{param} not a valid recipe id");
return;
}
var cookbooks = DatabaseManager.World.GetCookbooksByRecipeId(recipeId);
if (cookbooks == null || cookbooks.Count == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find recipe id {recipeId}");
return;
}
var sql_folder = $"{di.FullName}{sep}sql{sep}recipes{sep}";
di = new DirectoryInfo(sql_folder);
if (!di.Exists)
di.Create();
if (RecipeSQLWriter == null)
{
RecipeSQLWriter = new RecipeSQLWriter();
RecipeSQLWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
if (CookBookSQLWriter == null)
{
CookBookSQLWriter = new CookBookSQLWriter();
CookBookSQLWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
// same recipe for all cookbooks
var recipe = cookbooks[0].Recipe;
var sql_filename = RecipeSQLWriter.GetDefaultFileName(recipe, cookbooks);
try
{
using (StreamWriter sqlFile = new StreamWriter(sql_folder + sql_filename))
{
RecipeSQLWriter.CreateSQLDELETEStatement(recipe, sqlFile);
sqlFile.WriteLine();
RecipeSQLWriter.CreateSQLINSERTStatement(recipe, sqlFile);
sqlFile.WriteLine();
CookBookSQLWriter.CreateSQLDELETEStatement(cookbooks, sqlFile);
sqlFile.WriteLine();
CookBookSQLWriter.CreateSQLINSERTStatement(cookbooks, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to export {sql_folder}{sql_filename}");
return;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {sql_folder}{sql_filename}");
}
public static void ExportSQLLandblock(Session session, string param)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
if (!ushort.TryParse(Regex.Match(param, @"[0-9A-F]{4}", RegexOptions.IgnoreCase).Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var landblockId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"{param} not a valid landblock");
return;
}
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblockId);
if (instances == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find landblock {landblockId:X4}");
return;
}
var sql_folder = $"{di.FullName}{sep}sql{sep}landblocks{sep}";
di = new DirectoryInfo(sql_folder);
if (!di.Exists)
di.Create();
var sql_filename = $"{landblockId:X4}.sql";
try
{
if (LandblockInstanceWriter == null)
{
LandblockInstanceWriter = new LandblockInstanceWriter();
LandblockInstanceWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
using (StreamWriter sqlFile = new StreamWriter(sql_folder + sql_filename))
{
// Check if the Landblock is empty
if(instances.Count > 0)
LandblockInstanceWriter.CreateSQLDELETEStatement(instances, sqlFile);
else
{
// We'll just create a dummy list with a fake instance in our landblock so we don't anger CreateSQLDeleteStatement()
CommandHandlerHelper.WriteOutputInfo(session, $"Landblock {landblockId:X4} is empty.");
List<LandblockInstance> dummyList = new List<LandblockInstance> ();
LandblockInstance dummyInstance = new LandblockInstance();
dummyInstance.ObjCellId = (uint)(landblockId << 16);
dummyList.Add(dummyInstance);
LandblockInstanceWriter.CreateSQLDELETEStatement(dummyList, sqlFile);
}
sqlFile.WriteLine();
LandblockInstanceWriter.CreateSQLINSERTStatement(instances, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to export {sql_folder}{sql_filename}");
return;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {sql_folder}{sql_filename}");
}
public static void ExportSQLEncounter(Session session, string param)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
if (!ushort.TryParse(Regex.Match(param, @"[0-9A-F]{4}", RegexOptions.IgnoreCase).Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var landblockId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"{param} not a valid landblock");
return;
}
var encounters = DatabaseManager.World.GetCachedEncountersByLandblock(landblockId);
if (encounters == null || encounters.Count == 0)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find encounters for landblock {landblockId:X4}");
return;
}
var sql_folder = $"{di.FullName}{sep}sql{sep}encounters{sep}";
di = new DirectoryInfo(sql_folder);
if (!di.Exists)
di.Create();
var sql_filename = $"{landblockId:X4}.sql";
try
{
if (LandblockEncounterWriter == null)
{
LandblockEncounterWriter = new EncounterSQLWriter();
LandblockEncounterWriter.WeenieNames = DatabaseManager.World.GetAllWeenieNames();
}
using (var sqlFile = new StreamWriter(sql_folder + sql_filename))
{
LandblockEncounterWriter.CreateSQLDELETEStatement(encounters, sqlFile);
sqlFile.WriteLine();
LandblockEncounterWriter.CreateSQLINSERTStatement(encounters, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to export {sql_folder}{sql_filename}");
return;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {sql_folder}{sql_filename}");
}
public static void ExportSQLEvent(Session session, string eventName)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
var evt = DatabaseManager.World.GetCachedEvent(eventName);
if (evt == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find event `{eventName}`");
return;
}
var sql_folder = $"{di.FullName}{sep}sql{sep}events{sep}";
di = new DirectoryInfo(sql_folder);
if (!di.Exists)
di.Create();
var sql_filename = $"{eventName}.sql";
EventSQLWriter eventSQLWriter = new EventSQLWriter();
try
{
using (var sqlFile = new StreamWriter(sql_folder + sql_filename))
{
eventSQLWriter.CreateSQLDELETEStatement(evt, sqlFile);
sqlFile.WriteLine();
eventSQLWriter.CreateSQLINSERTStatement(evt, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to export {sql_folder}{sql_filename}");
return;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {sql_folder}{sql_filename}");
}
public static void ExportSQLQuest(Session session, string questName)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
var quest = DatabaseManager.World.GetCachedQuest(questName);
if (quest == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find quest {questName}");
return;
}
var sql_folder = $"{di.FullName}{sep}sql{sep}quests{sep}";
di = new DirectoryInfo(sql_folder);
if (!di.Exists)
di.Create();
if (QuestSQLWriter == null)
QuestSQLWriter = new QuestSQLWriter();
var sql_filename = questName + ".sql";
try
{
using (StreamWriter sqlFile = new StreamWriter(sql_folder + sql_filename))
{
QuestSQLWriter.CreateSQLDELETEStatement(quest, sqlFile);
sqlFile.WriteLine();
QuestSQLWriter.CreateSQLINSERTStatement(quest, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to export {sql_folder}{sql_filename}");
return;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {sql_folder}{sql_filename}");
}
public static void ExportSQLSpell(Session session, string param)
{
DirectoryInfo di = VerifyContentFolder(session, false);
var sep = Path.DirectorySeparatorChar;
if (!uint.TryParse(param, out var spellId))
{
CommandHandlerHelper.WriteOutputInfo(session, $"{param} not a valid spell id");
return;
}
var spell = DatabaseManager.World.GetCachedSpell(spellId);
if (spell == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Couldn't find spell id {spellId}");
return;
}
var sql_folder = $"{di.FullName}{sep}sql{sep}spells{sep}";
di = new DirectoryInfo(sql_folder);
if (!di.Exists)
di.Create();
if (SpellSQLWriter == null)
SpellSQLWriter = new SpellSQLWriter();
var sql_filename = SpellSQLWriter.GetDefaultFileName(spell);
try
{
using (StreamWriter sqlFile = new StreamWriter(sql_folder + sql_filename))
{
SpellSQLWriter.CreateSQLDELETEStatement(spell, sqlFile);
sqlFile.WriteLine();
SpellSQLWriter.CreateSQLINSERTStatement(spell, sqlFile);
}
}
catch (Exception e)
{
Console.WriteLine(e);
CommandHandlerHelper.WriteOutputInfo(session, $"Failed to export {sql_folder}{sql_filename}");
return;
}
CommandHandlerHelper.WriteOutputInfo(session, $"Exported {sql_folder}{sql_filename}");
}
[CommandHandler("clearcache", AccessLevel.Developer, CommandHandlerFlag.None, "Clears the various database caches. This enables live editing of the database information")]
public static void HandleClearCache(Session session, params string[] parameters)
{
var mode = CacheType.All;
if (parameters.Length > 0)
{
if (parameters[0].Contains("landblock", StringComparison.OrdinalIgnoreCase))
mode = CacheType.Landblock;
if (parameters[0].Contains("recipe", StringComparison.OrdinalIgnoreCase))
mode = CacheType.Recipe;
if (parameters[0].Contains("spell", StringComparison.OrdinalIgnoreCase))
mode = CacheType.Spell;
if (parameters[0].Contains("weenie", StringComparison.OrdinalIgnoreCase))
mode = CacheType.Weenie;
if (parameters[0].Contains("wield", StringComparison.OrdinalIgnoreCase))
mode = CacheType.WieldedTreasure;
}
if (mode.HasFlag(CacheType.Landblock))
{
CommandHandlerHelper.WriteOutputInfo(session, "Clearing landblock instance cache");
DatabaseManager.World.ClearCachedLandblockInstances();
}
if (mode.HasFlag(CacheType.Recipe))
{
CommandHandlerHelper.WriteOutputInfo(session, "Clearing recipe cache");
DatabaseManager.World.ClearCookbookCache();
}
if (mode.HasFlag(CacheType.Spell))
{
CommandHandlerHelper.WriteOutputInfo(session, "Clearing spell cache");
DatabaseManager.World.ClearSpellCache();
WorldObject.ClearSpellCache();
}
if (mode.HasFlag(CacheType.Weenie))
{
CommandHandlerHelper.WriteOutputInfo(session, "Clearing weenie cache");
DatabaseManager.World.ClearWeenieCache();
}
if (mode.HasFlag(CacheType.WieldedTreasure))
{
CommandHandlerHelper.WriteOutputInfo(session, "Clearing wielded treasure cache");
DatabaseManager.World.ClearWieldedTreasureCache();
}
}
[Flags]
public enum CacheType
{
None = 0x0,
Landblock = 0x1,
Recipe = 0x2,
Spell = 0x4,
Weenie = 0x8,
WieldedTreasure = 0x10,
All = 0xFFFF
};
public static FileType GetFileType(string filename)
{
if (filename.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
return GetJsonFileType(filename);
}
else if (filename.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
{
return GetSQLFileType(filename);
}
return FileType.Undefined;
}
public static FileType GetJsonFileType(string filename)
{
if (!File.Exists(filename))
return FileType.Undefined;
// can possibly be indented format
var json = File.ReadAllText(filename);
if (json.Contains("\"wcid\":"))
return FileType.Weenie;
else if (json.Contains("\"recipe\":"))
return FileType.Recipe;
else
return FileType.Undefined;
}
public static FileType GetSQLFileType(string filename)
{
if (!File.Exists(filename))
return FileType.Undefined;
using (var streamReader = new StreamReader(filename))
{
var line = streamReader.ReadLine();
while (line != null)
{
if (line.Trim().Length == 0)
continue;
if (line.Contains("`encounter`"))
return FileType.Encounter;
else if (line.Contains("`landblock_instance`"))
return FileType.LandblockInstance;
else if (line.Contains("`quest`"))
return FileType.Quest;
else if (line.Contains("`recipe`"))
return FileType.Recipe;
else if (line.Contains("`spell`"))
return FileType.Spell;
else if (line.Contains("`weenie`"))
return FileType.Weenie;
else
break;
}
return FileType.Undefined;
}
}
[CommandHandler("nudge", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Adjusts the spawn position of a landblock instance", "<dir> <amount>\nDirections: x, y, z, north, south, west, east, northwest, northeast, southwest, southeast, n, s, w, e, nw, ne, sw, se, up, down, here")]
public static void HandleNudge(Session session, params string[] parameters)
{
WorldObject obj = null;
var curParam = 0;
if (parameters.Length == 3)
{
if (!uint.TryParse(parameters[curParam++].TrimStart("0x"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var guid))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Invalid guid: {parameters[0]}", ChatMessageType.Broadcast));
return;
}
obj = session.Player.FindObject(guid, Player.SearchLocations.Landblock);
if (obj == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find {parameters[0]}", ChatMessageType.Broadcast));
return;
}
}
else
obj = CommandHandlerHelper.GetLastAppraisedObject(session);
if (obj == null) return;
// ensure landblock instance
if (!obj.Guid.IsStatic())
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) is not landblock instance", ChatMessageType.Broadcast));
return;
}
if (obj.PhysicsObj == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) is not a physics object", ChatMessageType.Broadcast));
return;
}
// get direction
var dirname = parameters[curParam++].ToLower();
var dir = GetNudgeDir(dirname);
bool curPos = false;
if (dir == null)
{
if (dirname.Equals("here") || dirname.Equals("to me"))
{
dir = Vector3.Zero;
curPos = true;
}
else
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Invalid direction: {dirname}", ChatMessageType.Broadcast));
return;
}
}
// get distance / amount
var amount = 1.0f;
if (curParam < parameters.Length)
{
if (!float.TryParse(parameters[curParam++], out amount))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Invalid amount: {amount}", ChatMessageType.Broadcast));
return;
}
}
var nudge = dir * amount;
// get landblock for static guid
var landblock_id = (ushort)(obj.Guid.Full >> 12);
// get instances for landblock
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblock_id);
// find instance
var instance = instances.FirstOrDefault(i => i.Guid == obj.Guid.Full);
if (instance == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find instance for {obj.Name} ({obj.Guid})", ChatMessageType.Broadcast));
return;
}
if (curPos)
{
// ensure same landblock
if ((instance.ObjCellId >> 16) != (session.Player.Location.Cell >> 16))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to move {obj.Name} ({obj.Guid}) to current location -- different landblock", ChatMessageType.Broadcast));
return;
}
obj.Ethereal = true;
obj.EnqueueBroadcastPhysicsState();
var newLoc = new Position(session.Player.Location);
// slide?
var setPos = new Physics.Common.SetPosition(newLoc.PhysPosition(), Physics.Common.SetPositionFlags.Teleport /* | Physics.Common.SetPositionFlags.Slide */);
var result = obj.PhysicsObj.SetPosition(setPos);
if (result != Physics.Common.SetPositionError.OK)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to move {obj.Name} ({obj.Guid}) to current location: {result}", ChatMessageType.Broadcast));
return;
}
instance.AnglesX = obj.Location.RotationX;
instance.AnglesY = obj.Location.RotationY;
instance.AnglesZ = obj.Location.RotationZ;
instance.AnglesW = obj.Location.RotationW;
}
else
{
// compare current position with home position
// the nudge should be performed as an offset from home position
if (instance.OriginX != obj.Location.PositionX || instance.OriginY != obj.Location.PositionY || instance.OriginZ != obj.Location.PositionZ)
{
//session.Network.EnqueueSend(new GameMessageSystemChat($"Moving {obj.Name} ({obj.Guid}) to home position: {obj.Location} to {instance.ObjCellId:X8} [{instance.OriginX} {instance.OriginY} {instance.OriginZ}]", ChatMessageType.Broadcast));
var homePos = new Position(instance.ObjCellId, instance.OriginX, instance.OriginY, instance.OriginZ, instance.AnglesX, instance.AnglesY, instance.AnglesZ, instance.AnglesW);
// slide?
var setPos = new Physics.Common.SetPosition(homePos.PhysPosition(), Physics.Common.SetPositionFlags.Teleport /* | Physics.Common.SetPositionFlags.Slide*/);
var result = obj.PhysicsObj.SetPosition(setPos);
if (result != Physics.Common.SetPositionError.OK)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to move {obj.Name} ({obj.Guid}) to home position {homePos.ToLOCString()}", ChatMessageType.Broadcast));
return;
}
}
// perform physics transition
var newPos = new Physics.Common.Position(obj.PhysicsObj.Position);
newPos.add_offset(nudge.Value);
var transit = obj.PhysicsObj.transition(obj.PhysicsObj.Position, newPos, true);
var errorMsg = $"{obj.Name} ({obj.Guid}) failed to move from {obj.PhysicsObj.Position.ACEPosition()} to {newPos.ACEPosition()}";
if (transit == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat(errorMsg, ChatMessageType.Broadcast));
return;
}
// ensure same landblock
if ((transit.SpherePath.CurPos.ObjCellID >> 16) != (obj.PhysicsObj.Position.ObjCellID >> 16))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{errorMsg} - cannot change landblock", ChatMessageType.Broadcast));
return;
}
obj.PhysicsObj.SetPositionInternal(transit);
}
// update ace location
var prevLoc = new Position(obj.Location);
obj.Location = obj.PhysicsObj.Position.ACEPosition();
if (prevLoc.Landblock != obj.Location.Landblock)
LandblockManager.RelocateObjectForPhysics(obj, true);
// broadcast new position
obj.SendUpdatePosition(true);
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) - moved from {prevLoc} to {obj.Location}", ChatMessageType.Broadcast));
// update sql
instance.ObjCellId = obj.Location.Cell;
instance.OriginX = obj.Location.PositionX;
instance.OriginY = obj.Location.PositionY;
instance.OriginZ = obj.Location.PositionZ;
SyncInstances(session, landblock_id, instances);
}
public static Vector3? GetNudgeDir(string dir)
{
if (dir.Equals("north") || dir.Equals("n") || dir.Equals("y"))
return Vector3.UnitY;
else if (dir.Equals("south") || dir.Equals("s"))
return -Vector3.UnitY;
else if (dir.Equals("west") || dir.Equals("w"))
return -Vector3.UnitX;
else if (dir.Equals("east") || dir.Equals("e") || dir.Equals("x"))
return Vector3.UnitX;
else if (dir.Equals("northwest") || dir.Equals("nw"))
return Vector3.Normalize(new Vector3(-1, 1, 0));
else if (dir.Equals("northeast") || dir.Equals("ne"))
return Vector3.Normalize(new Vector3(1, 1, 0));
else if (dir.Equals("southwest") || dir.Equals("sw"))
return Vector3.Normalize(new Vector3(-1, -1, 0));
else if (dir.Equals("southeast") || dir.Equals("se"))
return Vector3.Normalize(new Vector3(1, -1, 0));
else if (dir.Equals("up") || dir.Equals("z"))
return Vector3.UnitZ;
else if (dir.Equals("down"))
return -Vector3.UnitZ;
else
return null;
}
[CommandHandler("rotate", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Adjusts the rotation of a landblock instance", "<dir>\nDirections: north, south, west, east, northwest, northeast, southwest, southeast, n, s, w, e, nw, ne, sw, se, -or-\n0-360, with 0 being north, and 90 being west")]
public static void HandleRotate(Session session, params string[] parameters)
{
WorldObject obj = null;
var curParam = 0;
if (parameters.Length == 2)
{
if (!uint.TryParse(parameters[curParam++].TrimStart("0x"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var guid))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Invalid guid: {parameters[0]}", ChatMessageType.Broadcast));
return;
}
obj = session.Player.FindObject(guid, Player.SearchLocations.Landblock);
if (obj == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find {parameters[0]}", ChatMessageType.Broadcast));
return;
}
}
else
obj = CommandHandlerHelper.GetLastAppraisedObject(session);
if (obj == null) return;
// ensure landblock instance
if (!obj.Guid.IsStatic())
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) is not landblock instance", ChatMessageType.Broadcast));
return;
}
if (obj.PhysicsObj == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) is not a physics object", ChatMessageType.Broadcast));
return;
}
// get direction
var dirname = parameters[curParam++].ToLower();
var dir = GetNudgeDir(dirname);
bool curRotate = false;
if (dir == null)
{
if (float.TryParse(dirname, out var degrees))
{
var rads = degrees.ToRadians();
var q = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, rads);
dir = Vector3.Transform(Vector3.UnitY, q);
}
else if (dirname.Equals("here") || dirname.Equals("me"))
{
dir = Vector3.Zero;
curRotate = true;
}
else
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Invalid direction: {dirname}", ChatMessageType.Broadcast));
return;
}
}
// get quaternion
var newRotation = Quaternion.Identity;
if (curRotate)
{
newRotation = session.Player.Location.Rotation;
}
else
{
var angle = Math.Atan2(-dir.Value.X, dir.Value.Y);
newRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)angle);
}
newRotation = Quaternion.Normalize(newRotation);
// get landblock for static guid
var landblock_id = (ushort)(obj.Guid.Full >> 12);
// get instances for landblock
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblock_id);
// find instance
var instance = instances.FirstOrDefault(i => i.Guid == obj.Guid.Full);
if (instance == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find instance for {obj.Name} ({obj.Guid})", ChatMessageType.Broadcast));
return;
}
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) new rotation: {newRotation}", ChatMessageType.Broadcast));
// update physics / ace rotation
obj.PhysicsObj.Position.Frame.Orientation = newRotation;
obj.Location.Rotation = newRotation;
// update instance
instance.AnglesW = newRotation.W;
instance.AnglesX = newRotation.X;
instance.AnglesY = newRotation.Y;
instance.AnglesZ = newRotation.Z;
SyncInstances(session, landblock_id, instances);
// broadcast new rotation
obj.SendUpdatePosition(true);
}
[CommandHandler("rotate-x", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Adjusts the rotation of a landblock instance along the x-axis", "<degrees>")]
public static void HandleRotateX(Session session, params string[] parameters)
{
HandleRotateAxis(session, Vector3.UnitX, parameters);
}
[CommandHandler("rotate-y", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Adjusts the rotation of a landblock instance along the y-axis", "<degrees>")]
public static void HandleRotateY(Session session, params string[] parameters)
{
HandleRotateAxis(session, Vector3.UnitY, parameters);
}
[CommandHandler("rotate-z", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Adjusts the rotation of a landblock instance along the z-axis", "<degrees>")]
public static void HandleRotateZ(Session session, params string[] parameters)
{
HandleRotateAxis(session, Vector3.UnitZ, parameters);
}
public static void HandleRotateAxis(Session session, Vector3 axis, params string[] parameters)
{
WorldObject obj = null;
var curParam = 0;
if (parameters.Length == 2)
{
if (!uint.TryParse(parameters[curParam++].TrimStart("0x"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var guid))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Invalid guid: {parameters[0]}", ChatMessageType.Broadcast));
return;
}
obj = session.Player.FindObject(guid, Player.SearchLocations.Landblock);
if (obj == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find {parameters[0]}", ChatMessageType.Broadcast));
return;
}
}
else
obj = CommandHandlerHelper.GetLastAppraisedObject(session);
if (obj == null) return;
// ensure landblock instance
if (!obj.Guid.IsStatic())
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) is not landblock instance", ChatMessageType.Broadcast));
return;
}
if (obj.PhysicsObj == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) is not a physics object", ChatMessageType.Broadcast));
return;
}
var degrees_str = parameters[curParam];
if (!float.TryParse(degrees_str, out var degrees))
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Invalid angle: {degrees_str}", ChatMessageType.Broadcast));
return;
}
var rads = degrees.ToRadians();
var q = Quaternion.CreateFromAxisAngle(axis, rads);
// get quaternion
var newRotation = Quaternion.Normalize(obj.PhysicsObj.Position.Frame.Orientation * q);
// get landblock for static guid
var landblock_id = (ushort)(obj.Guid.Full >> 12);
// get instances for landblock
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblock_id);
// find instance
var instance = instances.FirstOrDefault(i => i.Guid == obj.Guid.Full);
if (instance == null)
{
session.Network.EnqueueSend(new GameMessageSystemChat($"Couldn't find instance for {obj.Name} ({obj.Guid})", ChatMessageType.Broadcast));
return;
}
session.Network.EnqueueSend(new GameMessageSystemChat($"{obj.Name} ({obj.Guid}) new rotation: {newRotation}", ChatMessageType.Broadcast));
// update physics / ace rotation
obj.PhysicsObj.Position.Frame.Orientation = newRotation;
obj.Location.Rotation = newRotation;
// update instance
instance.AnglesW = newRotation.W;
instance.AnglesX = newRotation.X;
instance.AnglesY = newRotation.Y;
instance.AnglesZ = newRotation.Z;
SyncInstances(session, landblock_id, instances);
// broadcast new rotation
obj.SendUpdatePosition(true);
}
[CommandHandler("generate-classnames", AccessLevel.Developer, CommandHandlerFlag.None, "Generates WeenieClassName.cs from current world database")]
public static void HandleGenerateClassNames(Session session, params string[] parameters)
{
var lines = new List<string>();
var replaceChars = new Dictionary<string, string>()
{
{ " ", "_" },
{ "-", "_" },
{ "!", "" },
{ "#", "" },
{ "?", "" },
};
using (var ctx = new WorldDbContext())
{
var weenies = ctx.Weenie.OrderBy(i => i.ClassId);
lines.Add("namespace ACE.Server.Factories.Enum");
lines.Add("{");
lines.Add(" public enum WeenieClassName");
lines.Add(" {");
lines.Add(" undef = 0,");
foreach (var weenie in weenies)
{
var className = weenie.ClassName;
foreach (var kvp in replaceChars)
className = className.Replace(kvp.Key, kvp.Value);
if (className[0] >= '0' && className[0] <= '9')
className = $"_{className}";
lines.Add($" {className} = {weenie.ClassId},");
}
lines.Add(" }");
lines.Add("}");
}
var filename = "WeenieClassName.cs";
var sep = Path.DirectorySeparatorChar;
var path = $"..{sep}..{sep}..{sep}..{sep}Factories{sep}Enum{sep}{filename}";
if (!File.Exists(path))
path = filename;
File.WriteAllLines(path, lines);
CommandHandlerHelper.WriteOutputInfo(session, $"Wrote {path}");
}
[CommandHandler("vloc2loc", AccessLevel.Developer, CommandHandlerFlag.None, 1, "Output a set of LOCs for a given landblock found in the VLOCS dataset", "<LandblockID>\nExample: @vloc2loc 0x0007\n @vloc2loc 0xCE95")]
public static void HandleVLOCtoLOC(Session session, params string[] parameters)
{
var hex = parameters[0];
if (hex.StartsWith("0x", StringComparison.CurrentCultureIgnoreCase)
|| hex.StartsWith("&H", StringComparison.CurrentCultureIgnoreCase))
{
hex = hex[2..];
}
if (uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var lbid))
{
DirectoryInfo di = VerifyContentFolder(session);
if (!di.Exists) return;
var sep = Path.DirectorySeparatorChar;
var vloc_folder = $"{di.FullName}{sep}vlocs{sep}";
di = new DirectoryInfo(vloc_folder);
var vlocDB = vloc_folder + "vlocDB.txt";
var vlocs = di.Exists ? new FileInfo(vlocDB).Exists ? File.ReadLines(vlocDB).ToArray() : null : null;
if (vlocs == null)
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unable to read VLOC database file located here: {vlocDB}");
return;
}
// Name,ObjectClass,LandCell,X,Y
// Master MacTavish,37,-114359889,97.14075000286103,-63.93749958674113
if (vlocs.Length == 0 || !vlocs[0].Equals("Name,ObjectClass,LandCell,X,Y"))
{
CommandHandlerHelper.WriteOutputInfo(session, $"{vlocDB} does not appear to be a valid VLOC database file.");
return;
}
var vlocFile = vloc_folder + $"{lbid:X4}.txt";
var vi = new FileInfo(vlocFile);
if (vi.Exists)
vi.Delete();
for (var i = 1; i < vlocs.Length; i++)
{
var split = vlocs[i].Split(",");
var name = split[0].Trim();
var objectClass = split[1].Trim();
var strLandCell = split[2].Trim();
var strX = split[3].Trim();
var strY = split[4].Trim();
if (!int.TryParse(strLandCell, out var landCell))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unable to parse LandCell ({strLandCell}) value from line {i} in vlocDB: {vlocs[i]}");
continue;
}
var objCellId = (uint)landCell;
if (!float.TryParse(strX, out var x))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unable to parse X ({strX}) value from line {i} in vlocDB: {vlocs[i]}");
continue;
}
if (!float.TryParse(strY, out var y))
{
CommandHandlerHelper.WriteOutputInfo(session, $"Unable to parse Y ({strY}) value from line {i} in vlocDB: {vlocs[i]}");
continue;
}
if ((objCellId >> 16) != lbid) continue;
try
{
var pos = new Position(new Vector2(x, y));
pos.AdjustMapCoords();
pos.Translate(objCellId);
pos.FindZ();
using (StreamWriter sw = File.AppendText(vlocFile))
{
sw.WriteLine($"{name} - @teleloc {pos.ToLOCString()}");
}
}
catch (Exception)
{
using (StreamWriter sw = File.AppendText(vlocFile))
{
sw.WriteLine($"Unable to parse {name} - 0x{objCellId:X8} {strX}, {strY}");
}
}
}
vi = new FileInfo(vlocFile);
if (vi.Exists)
CommandHandlerHelper.WriteOutputInfo(session, $"Successfully wrote VLOCs for 0x{lbid:X4} to {vlocFile}");
else
CommandHandlerHelper.WriteOutputInfo(session, $"No VLOCs able to be written for 0x{lbid:X4}");
}
else
{
CommandHandlerHelper.WriteOutputInfo(session, $"Invalid Landblock ID: {parameters[0]}\nLandblock ID should be in the hex format such as this: @vloc2loc 0xAB94");
}
}
}
}