From 7c691426f5bd7dcaa226937f465b0b1bfa220223 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 5 Sep 2025 10:11:10 -0300
Subject: [PATCH 001/190] s6-lan-presets-es
AdminPanel ES + presets Docker LAN/Public (RESOLVE_IP) y ajustes S6
---
.vscode/settings.json | 3 ++
deploy/all-in-one/README.md | 14 ++++++++++
deploy/all-in-one/docker-compose.lan.yml | 8 ++++++
deploy/all-in-one/docker-compose.override.yml | 4 +++
deploy/all-in-one/docker-compose.public.yml | 8 ++++++
src/Web/AdminPanel/Components/Install.razor | 26 ++++++++---------
src/Web/AdminPanel/Pages/Accounts.razor | 16 +++++------
src/Web/AdminPanel/Pages/AdminUsers.razor | 16 +++++------
src/Web/AdminPanel/Pages/GameServer.razor | 4 +--
src/Web/AdminPanel/Pages/Index.razor | 8 +++---
src/Web/AdminPanel/Pages/LogFiles.razor | 10 +++----
src/Web/AdminPanel/Pages/LoggedIn.razor | 14 +++++-----
src/Web/AdminPanel/Pages/Servers.razor | 22 +++++++--------
src/Web/AdminPanel/Pages/Setup.razor | 24 ++++++++--------
src/Web/AdminPanel/Pages/Setup.razor.cs | 4 +--
src/Web/AdminPanel/Pages/Updates.razor | 28 +++++++++----------
src/Web/AdminPanel/Shared/NavMenu.razor | 28 +++++++++----------
17 files changed, 137 insertions(+), 100 deletions(-)
create mode 100644 .vscode/settings.json
create mode 100644 deploy/all-in-one/docker-compose.lan.yml
create mode 100644 deploy/all-in-one/docker-compose.public.yml
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..0b6deab44
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "chatgpt.config": {}
+}
\ No newline at end of file
diff --git a/deploy/all-in-one/README.md b/deploy/all-in-one/README.md
index 034b64dda..3b4575642 100644
--- a/deploy/all-in-one/README.md
+++ b/deploy/all-in-one/README.md
@@ -87,3 +87,17 @@ should be created.
If you click on 'Install', wait a bit until the database is set up and filled with the
data and voila, OpenMU is ready to use.
+
+## Presets for LAN vs Public
+
+To simplify IP resolution for clients, additional compose overlays are provided:
+
+- LAN testing (announce local IP):
+
+ `docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.lan.yml up -d --build`
+
+- Public internet (announce public IP):
+
+ `docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.public.yml up -d`
+
+You can switch between them by recreating the `openmu-startup` service with the respective overlays.
diff --git a/deploy/all-in-one/docker-compose.lan.yml b/deploy/all-in-one/docker-compose.lan.yml
new file mode 100644
index 000000000..0149a6485
--- /dev/null
+++ b/deploy/all-in-one/docker-compose.lan.yml
@@ -0,0 +1,8 @@
+version: '3'
+
+services:
+ openmu-startup:
+ environment:
+ # Anuncia la IP local a los clientes (uso en red local).
+ RESOLVE_IP: local
+
diff --git a/deploy/all-in-one/docker-compose.override.yml b/deploy/all-in-one/docker-compose.override.yml
index cdfcdb554..0706c750b 100644
--- a/deploy/all-in-one/docker-compose.override.yml
+++ b/deploy/all-in-one/docker-compose.override.yml
@@ -7,6 +7,10 @@ services:
context: ../../src
dockerfile: Startup/Dockerfile
restart: "no"
+ environment:
+ # Para pruebas en red local, anunciar IP local a los clientes.
+ # Cambiar a "public" si van a conectarse desde Internet.
+ RESOLVE_IP: local
ports:
- "8081:8080"
diff --git a/deploy/all-in-one/docker-compose.public.yml b/deploy/all-in-one/docker-compose.public.yml
new file mode 100644
index 000000000..bbf4e0275
--- /dev/null
+++ b/deploy/all-in-one/docker-compose.public.yml
@@ -0,0 +1,8 @@
+version: '3'
+
+services:
+ openmu-startup:
+ environment:
+ # Anuncia la IP pública a los clientes (uso por Internet).
+ RESOLVE_IP: public
+
diff --git a/src/Web/AdminPanel/Components/Install.razor b/src/Web/AdminPanel/Components/Install.razor
index 389730349..0824cbed2 100644
--- a/src/Web/AdminPanel/Components/Install.razor
+++ b/src/Web/AdminPanel/Components/Install.razor
@@ -1,18 +1,18 @@
@if (this.SelectedVersion is null)
{
- Loading ...
+ Cargando...
}
else if (this.IsInstalling)
{
- Installing, please wait ...
+ Instalando, por favor espere...
}
else if (this.IsInstalled)
{
-
Finished! Have fun :)
+
¡Listo! ¡Diviértete! :)
if (!AdminPanelEnvironment.IsHostingEmbedded)
{
-
Please restart the connect and game server containers.
+
Por favor reinicia los contenedores de connect y game server.
diff --git a/src/Web/AdminPanel/Pages/GameServer.razor b/src/Web/AdminPanel/Pages/GameServer.razor
index 1017575a3..329ce00e7 100644
--- a/src/Web/AdminPanel/Pages/GameServer.razor
+++ b/src/Web/AdminPanel/Pages/GameServer.razor
@@ -9,8 +9,8 @@
@if (this._gameServer is not null)
{
- OpenMU: Game Server @this._gameServer.Id
-
+ OpenMU: Servidor de Juego @this._gameServer.Id
+
diff --git a/src/Web/AdminPanel/Pages/Index.razor b/src/Web/AdminPanel/Pages/Index.razor
index 666bbd6d5..e769682c6 100644
--- a/src/Web/AdminPanel/Pages/Index.razor
+++ b/src/Web/AdminPanel/Pages/Index.razor
@@ -1,7 +1,7 @@
@page "/"
-
OpenMU AdminPanel
-OpenMU AdminPanel
-
+
Panel de Administración OpenMU
+Panel de Administración OpenMU
+
-Welcome to the admin panel of OpenMU.
+Bienvenido al panel de administración de OpenMU.
diff --git a/src/Web/AdminPanel/Pages/LogFiles.razor b/src/Web/AdminPanel/Pages/LogFiles.razor
index 7e1fed8eb..42cbdbe73 100644
--- a/src/Web/AdminPanel/Pages/LogFiles.razor
+++ b/src/Web/AdminPanel/Pages/LogFiles.razor
@@ -2,15 +2,15 @@
@using System.IO
-OpenMU: Log Files
-
+OpenMU: Archivos de Log
+
Database status: Can't connect to the database. Probably not created yet.
+
Estado de la base de datos: No se puede conectar a la base de datos. Probablemente aún no fue creada.
-
+
}
else if (!this.SetupService.IsInstalled)
{
-
Database status: Not created
+
Estado de la base de datos: No creada
-
+
}
else if (this.SetupService.IsUpdateRequired)
{
-
Database status: Update required
-
+
Estado de la base de datos: Se requiere actualización
+
}
else
{
-
Database status: Up-to-date
+
Estado de la base de datos: Actualizada
@if (!this._isDataInitialized)
{
-
Initialized game version: No initialized data found!
+
Versión de juego inicializada: ¡No se encontraron datos inicializados!
}
else
{
-
Initialized game version: @this._gameClientVersion
+
Versión de juego inicializada: @this._gameClientVersion
}
-
+
}
diff --git a/src/Web/AdminPanel/Pages/Setup.razor.cs b/src/Web/AdminPanel/Pages/Setup.razor.cs
index 2764fd870..0d148c72c 100644
--- a/src/Web/AdminPanel/Pages/Setup.razor.cs
+++ b/src/Web/AdminPanel/Pages/Setup.razor.cs
@@ -59,9 +59,9 @@ private void OnInstallClick()
private async Task OnReInstallClickAsync()
{
- if (await this.JsRuntime.InvokeAsync("confirm", "Are you sure? All the current data is getting deleted and freshly installed.").ConfigureAwait(false))
+ if (await this.JsRuntime.InvokeAsync("confirm", "¿Estás seguro? Todos los datos actuales serán eliminados y se instalarán nuevamente.").ConfigureAwait(false))
{
this.ShowInstall = true;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Web/AdminPanel/Pages/Updates.razor b/src/Web/AdminPanel/Pages/Updates.razor
index c9dcddad1..028e0852a 100644
--- a/src/Web/AdminPanel/Pages/Updates.razor
+++ b/src/Web/AdminPanel/Pages/Updates.razor
@@ -1,24 +1,24 @@
@page "/config-updates"
-OpenMU: Configuration Updates
-
-
- Add New
\ No newline at end of file
+ Agregar Nuevo
diff --git a/src/Web/AdminPanel/Pages/EditMap.cs b/src/Web/AdminPanel/Pages/EditMap.cs
index 4997c9a9b..647abf4bb 100644
--- a/src/Web/AdminPanel/Pages/EditMap.cs
+++ b/src/Web/AdminPanel/Pages/EditMap.cs
@@ -87,7 +87,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
if (this._maps is { })
{
builder.OpenComponent(0);
- builder.AddAttribute(1, nameof(Breadcrumb.Caption), "Map Editor");
+ builder.AddAttribute(1, nameof(Breadcrumb.Caption), "Editor de Mapas");
builder.CloseComponent();
builder.OpenComponent>(10);
builder.AddAttribute(12, nameof(CascadingValue.Value), this._context);
@@ -197,8 +197,8 @@ private async Task LoadDataAsync(CancellationToken cancellationToken)
}
catch (Exception ex)
{
- this.Logger.LogError(ex, $"Could not load game maps: {ex.Message}{Environment.NewLine}{ex.StackTrace}");
- await this.ModalService.ShowMessageAsync("Error", "Could not load the map data. Check the logs for details.").ConfigureAwait(false);
+ this.Logger.LogError(ex, $"No se pudieron cargar los mapas: {ex.Message}{Environment.NewLine}{ex.StackTrace}");
+ await this.ModalService.ShowMessageAsync("Error", "No se pudieron cargar los datos del mapa. Revisa los logs para más detalles.").ConfigureAwait(false);
}
await showModalTask.ConfigureAwait(false);
@@ -224,13 +224,13 @@ private async Task SaveChangesAsync()
{
var context = await this.GameConfigurationSource.GetContextAsync().ConfigureAwait(true);
var success = await context.SaveChangesAsync().ConfigureAwait(true);
- var text = success ? "The changes have been saved." : "There were no changes to save.";
+ var text = success ? "Los cambios se han guardado." : "No hay cambios para guardar.";
this.ToastService.ShowSuccess(text);
}
catch (Exception ex)
{
- this.Logger.LogError(ex, $"Error during saving");
- this.ToastService.ShowError($"An unexpected error occured: {ex.Message}. See logs for more details.");
+ this.Logger.LogError(ex, $"Error al guardar");
+ this.ToastService.ShowError($"Ocurrió un error inesperado: {ex.Message}. Mira los logs para más detalles.");
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Web/AdminPanel/Pages/Error.razor b/src/Web/AdminPanel/Pages/Error.razor
index 889695285..5d2fa98de 100644
--- a/src/Web/AdminPanel/Pages/Error.razor
+++ b/src/Web/AdminPanel/Pages/Error.razor
@@ -2,15 +2,14 @@
Error
Error.
-
An error occurred while processing your request.
+
Ocurrió un error al procesar tu solicitud.
-
Development Mode
+
Modo de Desarrollo
- Swapping to Development environment will display more detailed information about the error that occurred.
-
-
- The Development environment shouldn't be enabled for deployed applications.
- It can result in displaying sensitive information from exceptions to end users.
- For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
- and restarting the app.
-
\ No newline at end of file
+ Cambiar al entorno Development mostrará más información detallada sobre el error ocurrido.
+
+ El entorno Development no debe estar habilitado en producción.
+ Podría exponer información sensible de las excepciones a los usuarios.
+ Para depuración local, habilita el entorno Development estableciendo la variable de entorno ASPNETCORE_ENVIRONMENT en Development
+ y reiniciando la aplicación.
+.
diff --git a/src/Web/AdminPanel/Pages/MapPage.razor b/src/Web/AdminPanel/Pages/MapPage.razor
index d94540846..1c906c1bd 100644
--- a/src/Web/AdminPanel/Pages/MapPage.razor
+++ b/src/Web/AdminPanel/Pages/MapPage.razor
@@ -6,14 +6,14 @@
@using MUnique.OpenMU.Interfaces
@using MUnique.OpenMU.GameLogic
-OpenMU: Live Map
-
+OpenMU: Mapa en Vivo
+
@if (this._gameServer is not null)
{
From 11d11c1aa04ba85cb4b5dc08d18ba3a5bdadaadd Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Sun, 7 Sep 2025 01:22:00 -0300
Subject: [PATCH 013/190] WhiteWizardData and updates
---
.gitignore | 4 +
...artWhiteWizardInvasionChatCommandPlugIn.cs | 49 +++++
.../WhiteWizardInvasionConfiguration.cs | 62 +++++++
.../WhiteWizardInvasionPlugIn.cs | 175 ++++++++++++++++++
.../Initialization/Updates/UpdateVersion.cs | 7 +-
.../Updates/WhiteWizardDataUpdatePlugIn.cs | 128 +++++++++++++
.../InvasionMobsInitialization.cs | 88 +++++++++
src/Web/AdminPanel/Pages/Index.razor | 57 ++++++
8 files changed, 569 insertions(+), 1 deletion(-)
create mode 100644 src/GameLogic/PlugIns/ChatCommands/StartWhiteWizardInvasionChatCommandPlugIn.cs
create mode 100644 src/GameLogic/PlugIns/InvasionEvents/WhiteWizardInvasionConfiguration.cs
create mode 100644 src/GameLogic/PlugIns/InvasionEvents/WhiteWizardInvasionPlugIn.cs
create mode 100644 src/Persistence/Initialization/Updates/WhiteWizardDataUpdatePlugIn.cs
diff --git a/.gitignore b/.gitignore
index a3a35b892..d5f81bc9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -317,3 +317,7 @@ src/AdminPanel/wwwroot/content/js/app.js.map
# Mac OS filesystem files
.DS_Store
+
+# Local MU client sources (do not commit)
+/_MuMain/
+/MuMain/
diff --git a/src/GameLogic/PlugIns/ChatCommands/StartWhiteWizardInvasionChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/StartWhiteWizardInvasionChatCommandPlugIn.cs
new file mode 100644
index 000000000..19bdf037c
--- /dev/null
+++ b/src/GameLogic/PlugIns/ChatCommands/StartWhiteWizardInvasionChatCommandPlugIn.cs
@@ -0,0 +1,49 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic.PlugIns;
+using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
+using MUnique.OpenMU.GameLogic.PlugIns.InvasionEvents;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// A chat command plugin which handles the startww command.
+/// Starts the (custom) White Wizard invasion at the next possible time.
+///
+[Guid("55C53E9B-7D8B-4B83-9A87-0AC7A6B53E5E")]
+[PlugIn(nameof(StartWhiteWizardInvasionChatCommandPlugIn), "Handles the chat command '/startww'. Starts the White Wizard invasion at the next possible time.")]
+[ChatCommandHelp(Command, "Starts the White Wizard invasion at the next possible time.", CharacterStatus.GameMaster)]
+public class StartWhiteWizardInvasionChatCommandPlugIn : IChatCommandPlugIn
+{
+ private const string Command = "/startww";
+
+ ///
+ public string Key => Command;
+
+ ///
+ public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster;
+
+ ///
+ public async ValueTask HandleCommandAsync(Player player, string command)
+ {
+ // Get the periodic task plugin point container to enumerate actual instances
+ var pluginPoint = player.GameContext.PlugInManager.GetPlugInPoint();
+ if (pluginPoint is IPlugInContainer container)
+ {
+ var ww = container.ActivePlugIns.FirstOrDefault(p => p is WhiteWizardInvasionPlugIn);
+ if (ww is object)
+ {
+ // ForceStart is defined on PeriodicTaskBasePlugIn; use reflection to avoid generic constraints
+ ww.GetType().GetMethod("ForceStart")?.Invoke(ww, Array.Empty
From 675e490be9cab211c8efe5e8612094bc1a9fdedd Mon Sep 17 00:00:00 2001
From: "Emanuel B. Catania" <82476648+EmanuelCatania@users.noreply.github.com>
Date: Mon, 8 Sep 2025 15:40:33 -0300
Subject: [PATCH 018/190] Fix result reference
---
.../Craftings/BaseEventTicketCrafting.cs | 3 ++-
.../Craftings/DinorantCrafting.cs | 4 ++--
.../Craftings/FenrirUpgradeCrafting.cs | 3 ++-
.../Craftings/FenrirUpgradeCraftingGold.cs | 3 ++-
.../Craftings/GuardianOptionCrafting.cs | 4 ++--
.../Craftings/MountSeedSphereCrafting.cs | 4 ++--
.../Items/BaseItemCraftingHandler.cs | 24 +++++++++----------
.../Items/IItemCraftingHandler.cs | 7 +++---
.../PlayerActions/Items/ItemCraftAction.cs | 12 +++++-----
.../Items/SimpleItemCraftingHandler.cs | 4 +++-
.../NPC/IShowItemCraftingResultPlugIn.cs | 4 +++-
.../Items/ChaosMixHandlerPlugIn.cs | 2 +-
.../NPC/ShowItemCraftingResultPlugIn.cs | 4 ++--
.../ServerToClient/ConnectionExtensions.cs | 6 ++++-
.../ServerToClient/ServerToClientPackets.cs | 22 +++++++++++++++--
.../ServerToClient/ServerToClientPackets.xml | 10 ++++++++
.../ServerToClientPacketsRef.cs | 22 +++++++++++++++--
.../ServerToClientPacketTests.cs | 2 +-
18 files changed, 99 insertions(+), 41 deletions(-)
diff --git a/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs b/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs
index 041293c20..097f84440 100644
--- a/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs
+++ b/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs
@@ -37,9 +37,10 @@ protected BaseEventTicketCrafting(string resultItemName, string requiredEventIte
protected virtual CraftingResult IncorrectMixItemsResult => CraftingResult.IncorrectMixItems;
///
- public override CraftingResult? TryGetRequiredItems(Player player, out IList itemLinks, out byte successRate)
+ public override CraftingResult? TryGetRequiredItems(Player player, out IList itemLinks, out byte successRate, out byte bonusRate)
{
successRate = 0;
+ bonusRate = 0;
itemLinks = new List(3);
var item1 = player.TemporaryStorage!.Items.FirstOrDefault(item => item.Definition?.Name == this._requiredEventItemName1);
diff --git a/src/GameLogic/PlayerActions/Craftings/DinorantCrafting.cs b/src/GameLogic/PlayerActions/Craftings/DinorantCrafting.cs
index 0bf6ba897..1adb23ae1 100644
--- a/src/GameLogic/PlayerActions/Craftings/DinorantCrafting.cs
+++ b/src/GameLogic/PlayerActions/Craftings/DinorantCrafting.cs
@@ -25,9 +25,9 @@ public DinorantCrafting(SimpleCraftingSettings settings)
}
///
- public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate)
+ public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate, out byte bonusRate)
{
- var craftingResult = base.TryGetRequiredItems(player, out items, out successRate);
+ var craftingResult = base.TryGetRequiredItems(player, out items, out successRate, out bonusRate);
if (craftingResult is null)
{
var uniriaLink = items.Where(i => i.ItemRequirement.PossibleItems.Any(i => i.Name == "Horn of Uniria"));
diff --git a/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCrafting.cs b/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCrafting.cs
index 39a9815df..58276d9f9 100644
--- a/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCrafting.cs
+++ b/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCrafting.cs
@@ -18,9 +18,10 @@ public class FenrirUpgradeCrafting : BaseItemCraftingHandler
private readonly ItemPriceCalculator _priceCalculator = new();
///
- public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems)
+ public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems, out byte bonusRate)
{
successRateByItems = 0;
+ bonusRate = 0;
items = new List(4);
var inputItems = player.TemporaryStorage!.Items.ToList();
var itemsLevelAndOption4 = inputItems
diff --git a/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCraftingGold.cs b/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCraftingGold.cs
index 24f68c838..0b4fe1b1b 100644
--- a/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCraftingGold.cs
+++ b/src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCraftingGold.cs
@@ -19,9 +19,10 @@ public class FenrirUpgradeCraftingGold : BaseItemCraftingHandler
private readonly ItemPriceCalculator _priceCalculator = new();
///
- public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems)
+ public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems, out byte bonusRate)
{
successRateByItems = 0;
+ bonusRate = 0;
items = new List(4);
var inputItems = player.TemporaryStorage!.Items.ToList();
var itemsLevelAndOption4gold = inputItems
diff --git a/src/GameLogic/PlayerActions/Craftings/GuardianOptionCrafting.cs b/src/GameLogic/PlayerActions/Craftings/GuardianOptionCrafting.cs
index ee458793a..eadccaeb4 100644
--- a/src/GameLogic/PlayerActions/Craftings/GuardianOptionCrafting.cs
+++ b/src/GameLogic/PlayerActions/Craftings/GuardianOptionCrafting.cs
@@ -29,9 +29,9 @@ public GuardianOptionCrafting(SimpleCraftingSettings settings)
public static byte ItemReference { get; } = 0x88;
///
- public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate)
+ public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate, out byte bonusRate)
{
- if (base.TryGetRequiredItems(player, out items, out successRate) is { } error)
+ if (base.TryGetRequiredItems(player, out items, out successRate, out bonusRate) is { } error)
{
return error;
}
diff --git a/src/GameLogic/PlayerActions/Craftings/MountSeedSphereCrafting.cs b/src/GameLogic/PlayerActions/Craftings/MountSeedSphereCrafting.cs
index dcf1e44a4..a8ce92c3d 100644
--- a/src/GameLogic/PlayerActions/Craftings/MountSeedSphereCrafting.cs
+++ b/src/GameLogic/PlayerActions/Craftings/MountSeedSphereCrafting.cs
@@ -34,9 +34,9 @@ public MountSeedSphereCrafting(SimpleCraftingSettings settings)
public static byte SocketItemReference { get; } = 0x88;
///
- public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate)
+ public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate, out byte bonusRate)
{
- var result = base.TryGetRequiredItems(player, out items, out successRate);
+ var result = base.TryGetRequiredItems(player, out items, out successRate, out bonusRate);
if (result != default)
{
return result;
diff --git a/src/GameLogic/PlayerActions/Items/BaseItemCraftingHandler.cs b/src/GameLogic/PlayerActions/Items/BaseItemCraftingHandler.cs
index 3d767f76d..245d333e7 100644
--- a/src/GameLogic/PlayerActions/Items/BaseItemCraftingHandler.cs
+++ b/src/GameLogic/PlayerActions/Items/BaseItemCraftingHandler.cs
@@ -16,25 +16,25 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Items;
public abstract class BaseItemCraftingHandler : IItemCraftingHandler
{
///
- public async ValueTask<(CraftingResult Result, Item? Item)> DoMixAsync(Player player, byte socketSlot)
+ public async ValueTask<(CraftingResult Result, Item? Item, byte SuccessRate, byte BonusRate)> DoMixAsync(Player player, byte socketSlot)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
if (player.TemporaryStorage is null)
{
- return (CraftingResult.Failed, null);
+ return (CraftingResult.Failed, null, 0, 0);
}
- if (this.TryGetRequiredItems(player, out var items, out var successRate) is { } error)
+ if (this.TryGetRequiredItems(player, out var items, out var successRate, out var bonusRate) is { } error)
{
- return (error, null);
+ return (error, null, successRate, bonusRate);
}
- player.Logger.LogInformation("Crafting success chance: {successRate} %", successRate);
+ player.Logger.LogInformation("Crafting success chance: {successRate} % (+{bonusRate})", successRate, bonusRate);
var price = this.GetPrice(successRate, items);
if (!player.TryRemoveMoney(price))
{
- return (CraftingResult.NotEnoughMoney, null);
+ return (CraftingResult.NotEnoughMoney, null, successRate, bonusRate);
}
await player.InvokeViewPlugInAsync(p => p.UpdateMoneyAsync()).ConfigureAwait(false);
@@ -42,30 +42,30 @@ public abstract class BaseItemCraftingHandler : IItemCraftingHandler
var success = Rand.NextRandomBool(successRate);
if (success)
{
- player.Logger.LogInformation("Crafting succeeded with success chance: {successRate} %", successRate);
+ player.Logger.LogInformation("Crafting succeeded with success chance: {successRate} % (+{bonusRate})", successRate, bonusRate);
if (await this.DoTheMixAsync(items, player, socketSlot, successRate).ConfigureAwait(false) is { } item)
{
player.Logger.LogInformation("Crafted item: {item}", item);
player.BackupInventory = null;
- return (CraftingResult.Success, item);
+ return (CraftingResult.Success, item, successRate, bonusRate);
}
player.Logger.LogInformation("Crafting handler failed to mix the items.");
- return (CraftingResult.Failed, null);
+ return (CraftingResult.Failed, null, successRate, bonusRate);
}
- player.Logger.LogInformation("Crafting failed with success chance: {successRate} %", successRate);
+ player.Logger.LogInformation("Crafting failed with success chance: {successRate} % (+{bonusRate})", successRate, bonusRate);
foreach (var i in items)
{
await this.RequiredItemChangeAsync(player, i, false).ConfigureAwait(false);
}
- return (CraftingResult.Failed, null);
+ return (CraftingResult.Failed, null, successRate, bonusRate);
}
///
- public abstract CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems);
+ public abstract CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems, out byte bonusRate);
///
/// Gets the price based on the success rate and the required items.
diff --git a/src/GameLogic/PlayerActions/Items/IItemCraftingHandler.cs b/src/GameLogic/PlayerActions/Items/IItemCraftingHandler.cs
index 3d7706efe..57c976c2b 100644
--- a/src/GameLogic/PlayerActions/Items/IItemCraftingHandler.cs
+++ b/src/GameLogic/PlayerActions/Items/IItemCraftingHandler.cs
@@ -17,8 +17,8 @@ public interface IItemCraftingHandler
///
/// The mixing player.
/// The socket slot index for the and . It's a 0-based index.
- /// The crafting result and the resulting item; if there are multiple, only the last one is returned.
- ValueTask<(CraftingResult Result, Item? Item)> DoMixAsync(Player player, byte socketSlot);
+ /// The crafting result, success information and the resulting item; if there are multiple, only the last one is returned.
+ ValueTask<(CraftingResult Result, Item? Item, byte SuccessRate, byte BonusRate)> DoMixAsync(Player player, byte socketSlot);
///
/// Tries to get the required items for this crafting.
@@ -28,6 +28,7 @@ public interface IItemCraftingHandler
/// The player.
/// The items.
/// The success rate by items.
+ /// The bonus rate.
/// null, if the required items could be get; Otherwise, the corresponding error is returned.
- CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems);
+ CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRateByItems, out byte bonusRate);
}
\ No newline at end of file
diff --git a/src/GameLogic/PlayerActions/Items/ItemCraftAction.cs b/src/GameLogic/PlayerActions/Items/ItemCraftAction.cs
index 6376aa1e2..dfd39007f 100644
--- a/src/GameLogic/PlayerActions/Items/ItemCraftAction.cs
+++ b/src/GameLogic/PlayerActions/Items/ItemCraftAction.cs
@@ -27,7 +27,7 @@ public async ValueTask MixItemsAsync(Player player, byte mixTypeId, byte socketS
var crafting = npcStats?.ItemCraftings.FirstOrDefault(c => c.Number == mixTypeId);
if (crafting is null)
{
- await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(CraftingResult.IncorrectMixItems, null)).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(CraftingResult.IncorrectMixItems, 0, 0, null)).ConfigureAwait(false);
return;
}
@@ -37,22 +37,22 @@ public async ValueTask MixItemsAsync(Player player, byte mixTypeId, byte socketS
this._craftingHandlerCache.Add(crafting, craftingHandler);
}
- (CraftingResult, Item?) result;
+ (CraftingResult Result, Item? Item, byte SuccessRate, byte BonusRate) result;
try
{
result = await craftingHandler.DoMixAsync(player, socketSlot).ConfigureAwait(false);
}
catch
{
- result = (CraftingResult.LackingMixItems, null);
+ result = (CraftingResult.LackingMixItems, null, 0, 0);
}
var itemList = player.TemporaryStorage?.Items.ToList() ?? new List();
- await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(result.Item1, itemList.Count > 1 ? null : result.Item2)).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(result.Result, result.SuccessRate, result.BonusRate, itemList.Count > 1 ? null : result.Item)).ConfigureAwait(false);
await player.InvokeViewPlugInAsync(
p => p.ShowMerchantStoreItemListAsync(
itemList,
- npcStats!.NpcWindow == NpcWindow.PetTrainer && result.Item1 != CraftingResult.Success ? StoreKind.ResurrectionFailed : StoreKind.ChaosMachine))
+ npcStats!.NpcWindow == NpcWindow.PetTrainer && result.Result != CraftingResult.Success ? StoreKind.ResurrectionFailed : StoreKind.ChaosMachine))
.ConfigureAwait(false);
}
@@ -75,7 +75,7 @@ await player.InvokeViewPlugInAsync(
this._craftingHandlerCache.Add(itemCrafting, craftingHandler);
}
- if (craftingHandler.TryGetRequiredItems(player, out _, out _) is null)
+ if (craftingHandler.TryGetRequiredItems(player, out _, out _, out _) is null)
{
return itemCrafting;
}
diff --git a/src/GameLogic/PlayerActions/Items/SimpleItemCraftingHandler.cs b/src/GameLogic/PlayerActions/Items/SimpleItemCraftingHandler.cs
index 0ff7a91a0..95661bff5 100644
--- a/src/GameLogic/PlayerActions/Items/SimpleItemCraftingHandler.cs
+++ b/src/GameLogic/PlayerActions/Items/SimpleItemCraftingHandler.cs
@@ -27,9 +27,10 @@ public SimpleItemCraftingHandler(SimpleCraftingSettings settings)
}
///
- public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate)
+ public override CraftingResult? TryGetRequiredItems(Player player, out IList items, out byte successRate, out byte bonusRate)
{
successRate = 0;
+ bonusRate = 0;
int rate = this._settings.SuccessPercent;
long totalCraftingPrice = 0;
items = new List(this._settings.RequiredItems.Count);
@@ -117,6 +118,7 @@ public SimpleItemCraftingHandler(SimpleCraftingSettings settings)
}
successRate = (byte)Math.Min(100, rate);
+ bonusRate = (byte)Math.Max(0, successRate - this._settings.SuccessPercent);
return default;
}
diff --git a/src/GameLogic/Views/NPC/IShowItemCraftingResultPlugIn.cs b/src/GameLogic/Views/NPC/IShowItemCraftingResultPlugIn.cs
index 6b8d4b0c3..697f680ce 100644
--- a/src/GameLogic/Views/NPC/IShowItemCraftingResultPlugIn.cs
+++ b/src/GameLogic/Views/NPC/IShowItemCraftingResultPlugIn.cs
@@ -13,6 +13,8 @@ public interface IShowItemCraftingResultPlugIn : IViewPlugIn
/// Shows the crafting result.
///
/// The crafting result.
+ /// The success rate in percent.
+ /// The additional bonus rate in percent.
/// The created item.
- ValueTask ShowResultAsync(CraftingResult result, Item? createdItem);
+ ValueTask ShowResultAsync(CraftingResult result, byte successRate, byte bonusRate, Item? createdItem);
}
\ No newline at end of file
diff --git a/src/GameServer/MessageHandler/Items/ChaosMixHandlerPlugIn.cs b/src/GameServer/MessageHandler/Items/ChaosMixHandlerPlugIn.cs
index c6ad0db67..7b85e42dc 100644
--- a/src/GameServer/MessageHandler/Items/ChaosMixHandlerPlugIn.cs
+++ b/src/GameServer/MessageHandler/Items/ChaosMixHandlerPlugIn.cs
@@ -38,7 +38,7 @@ public async ValueTask HandlePacketAsync(Player player, Memory packet)
var crafting = this._mixAction.FindAppropriateCraftingByItems(player);
if (crafting is null)
{
- await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(CraftingResult.IncorrectMixItems, null)).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(CraftingResult.IncorrectMixItems, 0, 0, null)).ConfigureAwait(false);
return;
}
diff --git a/src/GameServer/RemoteView/NPC/ShowItemCraftingResultPlugIn.cs b/src/GameServer/RemoteView/NPC/ShowItemCraftingResultPlugIn.cs
index 4ccee2630..b62acb911 100644
--- a/src/GameServer/RemoteView/NPC/ShowItemCraftingResultPlugIn.cs
+++ b/src/GameServer/RemoteView/NPC/ShowItemCraftingResultPlugIn.cs
@@ -29,7 +29,7 @@ public ShowItemCraftingResultPlugIn(RemotePlayer player)
}
///
- public async ValueTask ShowResultAsync(CraftingResult result, Item? createdItem)
+ public async ValueTask ShowResultAsync(CraftingResult result, byte successRate, byte bonusRate, Item? createdItem)
{
var itemData = new byte[this._player.ItemSerializer.NeededSpace];
if (createdItem is { })
@@ -37,7 +37,7 @@ public async ValueTask ShowResultAsync(CraftingResult result, Item? createdItem)
this._player.ItemSerializer.SerializeItem(itemData, createdItem);
}
- await this._player.Connection.SendItemCraftingResultAsync(Convert(result), itemData).ConfigureAwait(false);
+ await this._player.Connection.SendItemCraftingResultAsync(Convert(result), successRate, bonusRate, itemData).ConfigureAwait(false);
}
private static ItemCraftingResult.CraftingResult Convert(CraftingResult result)
diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
index 9f3e850b9..6e5fd7e60 100644
--- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
+++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
@@ -543,6 +543,8 @@ int WritePacket()
///
/// The connection.
/// The changed player id.
+ /// The success rate in percent.
+ /// The additional bonus rate in percent.
/// The item data.
///
/// Is sent by the server when: The appearance of a player changed, all surrounding players are informed about it.
@@ -4586,7 +4588,7 @@ int WritePacket()
/// Is sent by the server when: After the player requested to execute an item crafting, e.g. at the chaos machine.
/// Causes reaction on client side: The game client updates the UI to show the resulting item.
///
- public static async ValueTask SendItemCraftingResultAsync(this IConnection? connection, ItemCraftingResult.CraftingResult @result, Memory @itemData)
+ public static async ValueTask SendItemCraftingResultAsync(this IConnection? connection, ItemCraftingResult.CraftingResult @result, byte @successRate, byte @bonusRate, Memory @itemData)
{
if (connection is null)
{
@@ -4598,6 +4600,8 @@ int WritePacket()
var length = ItemCraftingResultRef.GetRequiredSize(itemData.Length);
var packet = new ItemCraftingResultRef(connection.Output.GetSpan(length)[..length]);
packet.Result = @result;
+ packet.SuccessRate = @successRate;
+ packet.BonusRate = @bonusRate;
@itemData.Span.CopyTo(packet.ItemData);
return packet.Header.Length;
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
index 6c5980ab5..1624a3583 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
+++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
@@ -22436,12 +22436,30 @@ public ItemCraftingResult.CraftingResult Result
set => this._data.Span[3] = (byte)value;
}
+ ///
+ /// Gets or sets the success rate.
+ ///
+ public byte SuccessRate
+ {
+ get => this._data.Span[4];
+ set => this._data.Span[4] = value;
+ }
+
+ ///
+ /// Gets or sets the bonus rate.
+ ///
+ public byte BonusRate
+ {
+ get => this._data.Span[5];
+ set => this._data.Span[5] = value;
+ }
+
///
/// Gets or sets the item data.
///
public Span ItemData
{
- get => this._data.Slice(4).Span;
+ get => this._data.Slice(6).Span;
}
///
@@ -22463,7 +22481,7 @@ public Span ItemData
///
/// The length in bytes of on which the required size depends.
- public static int GetRequiredSize(int itemDataLength) => itemDataLength + 4;
+ public static int GetRequiredSize(int itemDataLength) => itemDataLength + 6;
}
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
index d270b0d74..3fcfda776 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
+++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
@@ -8147,6 +8147,16 @@
4
+ Byte
+ SuccessRate
+
+
+ 5
+ Byte
+ BonusRate
+
+
+ 6BinaryItemData
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
index 08387116d..d97ac700b 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
+++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
@@ -21301,12 +21301,30 @@ public ItemCraftingResult.CraftingResult Result
set => this._data[3] = (byte)value;
}
+ ///
+ /// Gets or sets the success rate.
+ ///
+ public byte SuccessRate
+ {
+ get => this._data[4];
+ set => this._data[4] = value;
+ }
+
+ ///
+ /// Gets or sets the bonus rate.
+ ///
+ public byte BonusRate
+ {
+ get => this._data[5];
+ set => this._data[5] = value;
+ }
+
///
/// Gets or sets the item data.
///
public Span ItemData
{
- get => this._data.Slice(4);
+ get => this._data.Slice(6);
}
///
@@ -21328,7 +21346,7 @@ public Span ItemData
///
/// The length in bytes of on which the required size depends.
- public static int GetRequiredSize(int itemDataLength) => itemDataLength + 4;
+ public static int GetRequiredSize(int itemDataLength) => itemDataLength + 6;
}
diff --git a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs
index e6d0a7642..e66f18765 100644
--- a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs
+++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs
@@ -4666,7 +4666,7 @@ public void ItemCraftingResult_PacketSizeValidation()
// Test GetRequiredSize method with sample data
const int testBinaryLength = 10;
var calculatedSize = ItemCraftingResultRef.GetRequiredSize(testBinaryLength);
- var expectedMinSize = testBinaryLength + 4;
+ var expectedMinSize = testBinaryLength + 6;
Assert.That(calculatedSize, Is.GreaterThanOrEqualTo(expectedMinSize),
"GetRequiredSize calculation incorrect for binary field");
From 53f526b87ef91cde5dbbb7140d72e718962d3dee Mon Sep 17 00:00:00 2001
From: "Emanuel B. Catania" <82476648+EmanuelCatania@users.noreply.github.com>
Date: Mon, 8 Sep 2025 18:39:42 -0300
Subject: [PATCH 019/190] Allow single plugin to customize all summons
---
.../Skills/TargetedSkillDefaultPlugin.cs | 12 ++++-
.../ExampleSummonConfigurationPlugIn.cs | 44 +++++++++++++++++++
.../PlugIns/ISummonConfigurationPlugIn.cs | 27 ++++++++++++
src/GameLogic/PlugIns/SelfDefensePlugIn.cs | 4 +-
4 files changed, 84 insertions(+), 3 deletions(-)
create mode 100644 src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
create mode 100644 src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 4bd07b93a..639e517f7 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -10,6 +10,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.PlugIns;
+using MUnique.OpenMU.DataModel.Configuration;
///
/// Action to perform a skill which is explicitly aimed to a target.
@@ -124,8 +125,17 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var effectApplied = false;
if (skill.SkillType == SkillType.SummonMonster)
{
+ MonsterDefinition? defaultDefinition = null;
if (SummonSkillToMonsterMapping.TryGetValue(skill.Number, out var monsterNumber)
- && player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } monsterDefinition)
+ && player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } mappedDefinition)
+ {
+ defaultDefinition = mappedDefinition;
+ }
+
+ var summonPlugin = player.GameContext.PlugInManager.GetPlugIn();
+ var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, defaultDefinition) ?? defaultDefinition;
+
+ if (monsterDefinition is not null)
{
await player.CreateSummonedMonsterAsync(monsterDefinition).ConfigureAwait(false);
}
diff --git a/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
new file mode 100644
index 000000000..a61ad4d60
--- /dev/null
+++ b/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
@@ -0,0 +1,44 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlugIns;
+
+using System.Linq;
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Example plug-in which replaces the default goblin summon with a Tantal and adjusts its stats.
+///
+[Guid("27F0E8B5-61D3-4FB4-96F2-8B1ED2BB8C54")]
+[PlugIn(nameof(ExampleSummonConfigurationPlugIn), "Example plug-in which replaces the goblin summon with a Tantal and adjusts its stats.")]
+public class ExampleSummonConfigurationPlugIn : ISummonConfigurationPlugIn
+{
+ ///
+ public MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition)
+ {
+ if (skill.Number != 30) // goblin summon
+ {
+ return defaultDefinition;
+ }
+
+ var baseDefinition = player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == 58); // Tantal
+ if (baseDefinition is null)
+ {
+ return defaultDefinition;
+ }
+
+ var clone = baseDefinition.Clone(player.GameContext.Configuration);
+ var healthAttribute = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
+ if (healthAttribute is not null)
+ {
+ healthAttribute.Value *= 2;
+ }
+
+ return clone;
+ }
+}
+
diff --git a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
new file mode 100644
index 000000000..567b37cf6
--- /dev/null
+++ b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
@@ -0,0 +1,27 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlugIns;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Provides monster definitions for summon skills and allows to adjust their stats.
+///
+[Guid("C1E5C063-9CF8-4FE7-9C0F-4BB6387E3C27")]
+[PlugInPoint("Summon configuration", "Provides monster definitions for summon skills.")]
+public interface ISummonConfigurationPlugIn : IPlugIn
+{
+ ///
+ /// Creates the monster definition which should be used for the summon skill.
+ ///
+ /// The summoning player.
+ /// The used summon skill.
+ /// The default monster definition which would be used without this plug-in.
+ /// The monster definition which should be summoned, or null if the default mapping should be used.
+ MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition);
+}
+
diff --git a/src/GameLogic/PlugIns/SelfDefensePlugIn.cs b/src/GameLogic/PlugIns/SelfDefensePlugIn.cs
index 5c31578d2..e4611609a 100644
--- a/src/GameLogic/PlugIns/SelfDefensePlugIn.cs
+++ b/src/GameLogic/PlugIns/SelfDefensePlugIn.cs
@@ -44,8 +44,8 @@ public void ForceStart()
public void AttackableGotHit(IAttackable attackable, IAttacker attacker, HitInfo hitInfo)
{
var defender = attackable as Player ?? (attackable as Monster)?.SummonedBy;
- var attackerPlayer = attacker as Player ?? (attackable as Monster)?.SummonedBy;
- if (defender is null || attackerPlayer is null)
+ var attackerPlayer = attacker as Player ?? (attacker as Monster)?.SummonedBy;
+ if (defender is null || attackerPlayer is null || defender == attackerPlayer)
{
return;
}
From b451af84c97e6037bc368d6208a5efffe7ee6290 Mon Sep 17 00:00:00 2001
From: "Emanuel B. Catania" <82476648+EmanuelCatania@users.noreply.github.com>
Date: Mon, 8 Sep 2025 19:08:20 -0300
Subject: [PATCH 020/190] Update ISummonConfigurationPlugIn.cs
fix
---
src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
index 567b37cf6..b96c5d976 100644
--- a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
+++ b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
@@ -13,7 +13,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns;
///
[Guid("C1E5C063-9CF8-4FE7-9C0F-4BB6387E3C27")]
[PlugInPoint("Summon configuration", "Provides monster definitions for summon skills.")]
-public interface ISummonConfigurationPlugIn : IPlugIn
+public interface ISummonConfigurationPlugIn
{
///
/// Creates the monster definition which should be used for the summon skill.
From 1a468b4e00691b42e9540d1a28dbbb394b786097 Mon Sep 17 00:00:00 2001
From: "Emanuel B. Catania" <82476648+EmanuelCatania@users.noreply.github.com>
Date: Mon, 8 Sep 2025 19:19:19 -0300
Subject: [PATCH 021/190] refactor: retrieve summon config plugin via plugin
point
---
.../PlayerActions/Skills/TargetedSkillDefaultPlugin.cs | 2 +-
src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 639e517f7..8b559b3fd 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -132,7 +132,7 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
defaultDefinition = mappedDefinition;
}
- var summonPlugin = player.GameContext.PlugInManager.GetPlugIn();
+ var summonPlugin = player.GameContext.PlugInManager.GetPlugInPoint();
var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, defaultDefinition) ?? defaultDefinition;
if (monsterDefinition is not null)
diff --git a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
index 567b37cf6..b96c5d976 100644
--- a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
+++ b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
@@ -13,7 +13,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns;
///
[Guid("C1E5C063-9CF8-4FE7-9C0F-4BB6387E3C27")]
[PlugInPoint("Summon configuration", "Provides monster definitions for summon skills.")]
-public interface ISummonConfigurationPlugIn : IPlugIn
+public interface ISummonConfigurationPlugIn
{
///
/// Creates the monster definition which should be used for the summon skill.
From 52c753806b59334e44085a0da9c66982ebb1a884 Mon Sep 17 00:00:00 2001
From: "Emanuel B. Catania" <82476648+EmanuelCatania@users.noreply.github.com>
Date: Mon, 8 Sep 2025 19:33:27 -0300
Subject: [PATCH 022/190] Update ISummonConfigurationPlugIn.cs
undo fix
---
src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
index b96c5d976..567b37cf6 100644
--- a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
+++ b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
@@ -13,7 +13,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns;
///
[Guid("C1E5C063-9CF8-4FE7-9C0F-4BB6387E3C27")]
[PlugInPoint("Summon configuration", "Provides monster definitions for summon skills.")]
-public interface ISummonConfigurationPlugIn
+public interface ISummonConfigurationPlugIn : IPlugIn
{
///
/// Creates the monster definition which should be used for the summon skill.
From 3116df730368cd857fc98205c8b751987d1fcb5e Mon Sep 17 00:00:00 2001
From: "Emanuel B. Catania" <82476648+EmanuelCatania@users.noreply.github.com>
Date: Tue, 9 Sep 2025 00:15:18 -0300
Subject: [PATCH 023/190] Add test for self-defense with own summon
---
.../Skills/TargetedSkillDefaultPlugin.cs | 17 ++++++-
.../ExampleSummonConfigurationPlugIn.cs | 44 +++++++++++++++++++
.../PlugIns/ISummonConfigurationPlugIn.cs | 27 ++++++++++++
src/GameLogic/PlugIns/SelfDefensePlugIn.cs | 6 +--
.../SelfDefensePlugInTest.cs | 34 ++++++++++++++
5 files changed, 124 insertions(+), 4 deletions(-)
create mode 100644 src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
create mode 100644 src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
create mode 100644 tests/MUnique.OpenMU.Tests/SelfDefensePlugInTest.cs
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 4bd07b93a..4a4b31d59 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -10,6 +10,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.PlugIns;
+using MUnique.OpenMU.DataModel.Configuration;
///
/// Action to perform a skill which is explicitly aimed to a target.
@@ -124,8 +125,17 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var effectApplied = false;
if (skill.SkillType == SkillType.SummonMonster)
{
+ MonsterDefinition? defaultDefinition = null;
if (SummonSkillToMonsterMapping.TryGetValue(skill.Number, out var monsterNumber)
- && player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } monsterDefinition)
+ && player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } mappedDefinition)
+ {
+ defaultDefinition = mappedDefinition;
+ }
+
+ var summonPlugin = player.GameContext.PlugInManager.GetPlugIn();
+ var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, defaultDefinition) ?? defaultDefinition;
+
+ if (monsterDefinition is not null)
{
await player.CreateSummonedMonsterAsync(monsterDefinition).ConfigureAwait(false);
}
@@ -249,6 +259,11 @@ private async ValueTask ApplySkillAsync(Player player, IAttackable targete
{
if (skill.SkillType == SkillType.DirectHit || skill.SkillType == SkillType.CastleSiegeSkill)
{
+ if (target is Monster { SummonedBy: { } owner } && owner == player)
+ {
+ continue;
+ }
+
if (player.Attributes![Stats.AmmunitionConsumptionRate] > player.Attributes[Stats.AmmunitionAmount])
{
break;
diff --git a/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
new file mode 100644
index 000000000..a61ad4d60
--- /dev/null
+++ b/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
@@ -0,0 +1,44 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlugIns;
+
+using System.Linq;
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Example plug-in which replaces the default goblin summon with a Tantal and adjusts its stats.
+///
+[Guid("27F0E8B5-61D3-4FB4-96F2-8B1ED2BB8C54")]
+[PlugIn(nameof(ExampleSummonConfigurationPlugIn), "Example plug-in which replaces the goblin summon with a Tantal and adjusts its stats.")]
+public class ExampleSummonConfigurationPlugIn : ISummonConfigurationPlugIn
+{
+ ///
+ public MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition)
+ {
+ if (skill.Number != 30) // goblin summon
+ {
+ return defaultDefinition;
+ }
+
+ var baseDefinition = player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == 58); // Tantal
+ if (baseDefinition is null)
+ {
+ return defaultDefinition;
+ }
+
+ var clone = baseDefinition.Clone(player.GameContext.Configuration);
+ var healthAttribute = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
+ if (healthAttribute is not null)
+ {
+ healthAttribute.Value *= 2;
+ }
+
+ return clone;
+ }
+}
+
diff --git a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
new file mode 100644
index 000000000..b96c5d976
--- /dev/null
+++ b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
@@ -0,0 +1,27 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.PlugIns;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Provides monster definitions for summon skills and allows to adjust their stats.
+///
+[Guid("C1E5C063-9CF8-4FE7-9C0F-4BB6387E3C27")]
+[PlugInPoint("Summon configuration", "Provides monster definitions for summon skills.")]
+public interface ISummonConfigurationPlugIn
+{
+ ///
+ /// Creates the monster definition which should be used for the summon skill.
+ ///
+ /// The summoning player.
+ /// The used summon skill.
+ /// The default monster definition which would be used without this plug-in.
+ /// The monster definition which should be summoned, or null if the default mapping should be used.
+ MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition);
+}
+
diff --git a/src/GameLogic/PlugIns/SelfDefensePlugIn.cs b/src/GameLogic/PlugIns/SelfDefensePlugIn.cs
index 5c31578d2..a0a605a15 100644
--- a/src/GameLogic/PlugIns/SelfDefensePlugIn.cs
+++ b/src/GameLogic/PlugIns/SelfDefensePlugIn.cs
@@ -43,9 +43,9 @@ public void ForceStart()
///
public void AttackableGotHit(IAttackable attackable, IAttacker attacker, HitInfo hitInfo)
{
- var defender = attackable as Player ?? (attackable as Monster)?.SummonedBy;
- var attackerPlayer = attacker as Player ?? (attackable as Monster)?.SummonedBy;
- if (defender is null || attackerPlayer is null)
+ var defender = attackable as Player ?? (attackable as ISummonable)?.SummonedBy;
+ var attackerPlayer = attacker as Player ?? (attacker as ISummonable)?.SummonedBy;
+ if (defender is null || attackerPlayer is null || defender == attackerPlayer)
{
return;
}
diff --git a/tests/MUnique.OpenMU.Tests/SelfDefensePlugInTest.cs b/tests/MUnique.OpenMU.Tests/SelfDefensePlugInTest.cs
new file mode 100644
index 000000000..aefdf230e
--- /dev/null
+++ b/tests/MUnique.OpenMU.Tests/SelfDefensePlugInTest.cs
@@ -0,0 +1,34 @@
+using System.Threading.Tasks;
+using Moq;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.NPC;
+using MUnique.OpenMU.GameLogic.PlugIns;
+using MUnique.OpenMU.DataModel.Configuration;
+
+namespace MUnique.OpenMU.Tests;
+
+///
+/// Tests for .
+///
+[TestFixture]
+public class SelfDefensePlugInTest
+{
+ ///
+ /// Ensures that hitting an own summon doesn't start self-defense.
+ ///
+ [Test]
+ public async ValueTask OwnSummonHitDoesNotStartSelfDefenseAsync()
+ {
+ var player = await TestHelper.CreatePlayerAsync().ConfigureAwait(false);
+
+ var summonMock = new Mock();
+ var summonable = summonMock.As();
+ summonable.Setup(s => s.SummonedBy).Returns(player);
+ summonable.Setup(s => s.Definition).Returns(new MonsterDefinition());
+
+ var plugIn = new SelfDefensePlugIn();
+ plugIn.AttackableGotHit(summonMock.Object, player, new HitInfo(1, 0, DamageAttributes.Undefined));
+
+ Assert.That(player.GameContext.SelfDefenseState, Is.Empty);
+ }
+}
From 5a9da71343980d2d85f56f8f13c306c97f519ede Mon Sep 17 00:00:00 2001
From: Emanuel
Date: Tue, 9 Sep 2025 06:35:18 +0000
Subject: [PATCH 024/190] Fix elf summon pluggin
---
.../all-in-one/docker-compose.admin-port.yml | 2 -
deploy/all-in-one/docker-compose.lan-ip.yml | 2 -
deploy/all-in-one/docker-compose.lan.yml | 2 -
deploy/all-in-one/docker-compose.no-nginx.yml | 2 -
deploy/all-in-one/docker-compose.npm-net.yml | 2 -
deploy/all-in-one/docker-compose.override.yml | 2 -
deploy/all-in-one/docker-compose.prod.yml | 2 -
.../all-in-one/docker-compose.public-dns.yml | 2 -
.../docker-compose.public-ip.example.yml | 2 -
deploy/all-in-one/docker-compose.public.yml | 2 -
deploy/all-in-one/docker-compose.yml | 2 -
.../Skills/TargetedSkillDefaultPlugin.cs | 10 +-
src/GameLogic/PlugIns/ElfSummonsAll.cs | 204 ++++++++++++++++++
.../ExampleSummonConfigurationPlugIn.cs | 44 ----
.../PlugIns/ISummonConfigurationPlugIn.cs | 4 +-
15 files changed, 211 insertions(+), 73 deletions(-)
create mode 100644 src/GameLogic/PlugIns/ElfSummonsAll.cs
delete mode 100644 src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
diff --git a/deploy/all-in-one/docker-compose.admin-port.yml b/deploy/all-in-one/docker-compose.admin-port.yml
index f1b79c210..f276e1b80 100644
--- a/deploy/all-in-one/docker-compose.admin-port.yml
+++ b/deploy/all-in-one/docker-compose.admin-port.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
ports:
diff --git a/deploy/all-in-one/docker-compose.lan-ip.yml b/deploy/all-in-one/docker-compose.lan-ip.yml
index b6dd0d472..267987d3b 100644
--- a/deploy/all-in-one/docker-compose.lan-ip.yml
+++ b/deploy/all-in-one/docker-compose.lan-ip.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
environment:
diff --git a/deploy/all-in-one/docker-compose.lan.yml b/deploy/all-in-one/docker-compose.lan.yml
index 0149a6485..eed023f67 100644
--- a/deploy/all-in-one/docker-compose.lan.yml
+++ b/deploy/all-in-one/docker-compose.lan.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
environment:
diff --git a/deploy/all-in-one/docker-compose.no-nginx.yml b/deploy/all-in-one/docker-compose.no-nginx.yml
index f004453b4..a8fcebee5 100644
--- a/deploy/all-in-one/docker-compose.no-nginx.yml
+++ b/deploy/all-in-one/docker-compose.no-nginx.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
image: munique/openmu
diff --git a/deploy/all-in-one/docker-compose.npm-net.yml b/deploy/all-in-one/docker-compose.npm-net.yml
index c960d1ec3..fddcae81d 100644
--- a/deploy/all-in-one/docker-compose.npm-net.yml
+++ b/deploy/all-in-one/docker-compose.npm-net.yml
@@ -1,5 +1,3 @@
-version: '3'
-
networks:
proxy_net:
external: true
diff --git a/deploy/all-in-one/docker-compose.override.yml b/deploy/all-in-one/docker-compose.override.yml
index 1713a5f9a..2c01bbdbb 100644
--- a/deploy/all-in-one/docker-compose.override.yml
+++ b/deploy/all-in-one/docker-compose.override.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
diff --git a/deploy/all-in-one/docker-compose.prod.yml b/deploy/all-in-one/docker-compose.prod.yml
index 4c800cc97..086b12fa0 100644
--- a/deploy/all-in-one/docker-compose.prod.yml
+++ b/deploy/all-in-one/docker-compose.prod.yml
@@ -1,5 +1,3 @@
-version: '3.4'
-
services:
openmu-startup:
restart: "unless-stopped"
diff --git a/deploy/all-in-one/docker-compose.public-dns.yml b/deploy/all-in-one/docker-compose.public-dns.yml
index bcf5148fa..adadfd5e0 100644
--- a/deploy/all-in-one/docker-compose.public-dns.yml
+++ b/deploy/all-in-one/docker-compose.public-dns.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
environment:
diff --git a/deploy/all-in-one/docker-compose.public-ip.example.yml b/deploy/all-in-one/docker-compose.public-ip.example.yml
index 14e8be3ab..f190f10c2 100644
--- a/deploy/all-in-one/docker-compose.public-ip.example.yml
+++ b/deploy/all-in-one/docker-compose.public-ip.example.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
environment:
diff --git a/deploy/all-in-one/docker-compose.public.yml b/deploy/all-in-one/docker-compose.public.yml
index bbf4e0275..0296f3e08 100644
--- a/deploy/all-in-one/docker-compose.public.yml
+++ b/deploy/all-in-one/docker-compose.public.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
openmu-startup:
environment:
diff --git a/deploy/all-in-one/docker-compose.yml b/deploy/all-in-one/docker-compose.yml
index d199e1159..59731c0b8 100644
--- a/deploy/all-in-one/docker-compose.yml
+++ b/deploy/all-in-one/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
nginx-80:
image: nginx:alpine
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index caafd9482..884b52a8a 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -127,14 +127,17 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
{
MonsterDefinition? defaultDefinition = null;
if (SummonSkillToMonsterMapping.TryGetValue(skill.Number, out var monsterNumber)
- && player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } mappedDefinition)
+ && player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } mappedDefinition)
{
defaultDefinition = mappedDefinition;
}
- var summonPlugin = player.GameContext.PlugInManager.GetPlugIn();
+ // ✅ pedir el plugin “keyed” por skill.Number
+ var summonPlugin = player.GameContext.PlugInManager
+ .GetStrategy(skill.Number);
- var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, defaultDefinition) ?? defaultDefinition;
+ var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, defaultDefinition)
+ ?? defaultDefinition;
if (monsterDefinition is not null)
{
@@ -145,7 +148,6 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
{
effectApplied = await this.ApplySkillAsync(player, target, skillEntry!).ConfigureAwait(false);
}
-
await player.ForEachWorldObserverAsync(p => p.ShowSkillAnimationAsync(player, target, skill, effectApplied), true).ConfigureAwait(false);
}
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
new file mode 100644
index 000000000..8fa93202e
--- /dev/null
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -0,0 +1,204 @@
+// ElfSummonsAll.cs
+namespace MUnique.OpenMU.GameLogic.PlugIns;
+
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.PlugIns;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+public sealed class NullToZeroIntConverter : JsonConverter
+{
+ public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => reader.TokenType == JsonTokenType.Null ? 0 : reader.GetInt32();
+
+ public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
+ => writer.WriteNumberValue(value);
+}
+
+#region Core
+
+public sealed class ElfSummonsConfigCore
+{
+ public static readonly ElfSummonsConfigCore Instance = new();
+
+ // null => no reemplaza (usa mapping por defecto del server)
+ public System.Collections.Generic.Dictionary Map { get; }
+ = new()
+ {
+ [30] = new SummonConfig { MonsterNumber = null },
+ [31] = new SummonConfig { MonsterNumber = null },
+ [32] = new SummonConfig { MonsterNumber = null },
+ [33] = new SummonConfig { MonsterNumber = null },
+ [34] = new SummonConfig { MonsterNumber = null },
+ [35] = new SummonConfig { MonsterNumber = null },
+ [36] = new SummonConfig { MonsterNumber = null },
+ };
+
+ private ElfSummonsConfigCore() { }
+
+ public MonsterDefinition? Resolve(Player player, Skill skill, MonsterDefinition? defaultDefinition)
+ {
+ if (!this.Map.TryGetValue(skill.Number, out var cfg) || cfg.MonsterNumber is null)
+ {
+ return null; // no reemplaza -> usa mapping por defecto
+ }
+
+ var baseDef = player.GameContext.Configuration.Monsters
+ .FirstOrDefault(m => m.Number == cfg.MonsterNumber.Value);
+ if (baseDef is null)
+ {
+ return defaultDefinition;
+ }
+
+ var clone = baseDef.Clone(player.GameContext.Configuration);
+
+ var hp = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
+ if (hp is not null && cfg.HpMul != 1.0f) hp.Value *= cfg.HpMul;
+
+ var minDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumPhysBaseDmg);
+ if (minDmg is not null && cfg.MinDmgMul != 1.0f) minDmg.Value *= cfg.MinDmgMul;
+
+ var maxDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumPhysBaseDmg);
+ if (maxDmg is not null && cfg.MaxDmgMul != 1.0f) maxDmg.Value *= cfg.MaxDmgMul;
+
+ var def = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseBase);
+ if (def is not null && cfg.DefMul != 1.0f) def.Value *= cfg.DefMul;
+
+ cfg.Customize?.Invoke(clone);
+ return clone;
+ }
+
+ public sealed class SummonConfig
+ {
+ public ushort? MonsterNumber { get; set; }
+ public float HpMul { get; set; } = 1.0f;
+ public float MinDmgMul { get; set; } = 1.0f;
+ public float MaxDmgMul { get; set; } = 1.0f;
+ public float DefMul { get; set; } = 1.0f;
+ public System.Action? Customize { get; set; }
+ }
+}
+
+#endregion
+
+#region POCO de configuración (lo que edita el ⚙️)
+
+// Usamos int no-nullable para que el Admin lo renderice.
+// 0 = usar mapeo por defecto del server.
+public class ElfSummonSkillConfiguration
+{
+ [JsonConverter(typeof(NullToZeroIntConverter))]
+ [System.ComponentModel.DisplayName("Monster Number (0 = default)")]
+ [System.ComponentModel.DataAnnotations.Range(0, 65535)]
+ public int MonsterNumber { get; set; } = 0;
+
+ public float HpMul { get; set; } = 1.0f;
+ public float MinDmgMul { get; set; } = 1.0f;
+ public float MaxDmgMul { get; set; } = 1.0f;
+ public float DefMul { get; set; } = 1.0f;
+}
+
+
+#endregion
+
+#region Base configurable
+
+public abstract class ElfSummonCfgBase :
+ ISummonConfigurationPlugIn,
+ ISupportCustomConfiguration,
+ ISupportDefaultCustomConfiguration
+{
+ public abstract short Key { get; }
+
+ private ElfSummonSkillConfiguration? _configuration;
+
+ public ElfSummonSkillConfiguration? Configuration
+ {
+ get => _configuration;
+ set
+ {
+ _configuration = value;
+ if (value is null)
+ {
+ return;
+ }
+
+ var entry = ElfSummonsConfigCore.Instance.Map[this.Key];
+
+ // Convertimos int => ushort? (0 = default/null). Validamos rango.
+ if (value.MonsterNumber <= 0)
+ {
+ entry.MonsterNumber = null;
+ }
+ else
+ {
+ var clamped = value.MonsterNumber > ushort.MaxValue
+ ? ushort.MaxValue
+ : (ushort)value.MonsterNumber;
+
+ entry.MonsterNumber = clamped;
+ }
+
+ entry.HpMul = value.HpMul;
+ entry.MinDmgMul = value.MinDmgMul;
+ entry.MaxDmgMul = value.MaxDmgMul;
+ entry.DefMul = value.DefMul;
+ }
+ }
+
+ public object CreateDefaultConfig()
+ {
+ var entry = ElfSummonsConfigCore.Instance.Map[this.Key];
+ return new ElfSummonSkillConfiguration
+ {
+ MonsterNumber = entry.MonsterNumber.HasValue ? entry.MonsterNumber.Value : 0,
+ HpMul = entry.HpMul,
+ MinDmgMul = entry.MinDmgMul,
+ MaxDmgMul = entry.MaxDmgMul,
+ DefMul = entry.DefMul,
+ };
+ }
+
+ public MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition)
+ => ElfSummonsConfigCore.Instance.Resolve(player, skill, defaultDefinition);
+}
+
+#endregion
+
+#region Wrappers (30..36)
+
+[Guid("A6E7C6A1-5D9A-4D7D-A001-000000000030")]
+[PlugIn("Elf Summon cfg — Goblin (30)", "Configurable summon (Goblin)")]
+public sealed class ElfSummonCfg30 : ElfSummonCfgBase { public override short Key => 30; }
+
+[Guid("A6E7C6A1-5D9A-4D7D-A001-000000000031")]
+[PlugIn("Elf Summon cfg — Stone Golem (31)", "Configurable summon (Stone Golem)")]
+public sealed class ElfSummonCfg31 : ElfSummonCfgBase { public override short Key => 31; }
+
+[Guid("A6E7C6A1-5D9A-4D7D-A001-000000000032")]
+[PlugIn("Elf Summon cfg — Assassin (32)", "Configurable summon (Assassin)")]
+public sealed class ElfSummonCfg32 : ElfSummonCfgBase { public override short Key => 32; }
+
+[Guid("A6E7C6A1-5D9A-4D7D-A001-000000000033")]
+[PlugIn("Elf Summon cfg — Elite Yeti (33)", "Configurable summon (Elite Yeti)")]
+public sealed class ElfSummonCfg33 : ElfSummonCfgBase { public override short Key => 33; }
+
+[Guid("A6E7C6A1-5D9A-4D7D-A001-000000000034")]
+[PlugIn("Elf Summon cfg — Dark Knight (34)", "Configurable summon (Dark Knight)")]
+public sealed class ElfSummonCfg34 : ElfSummonCfgBase { public override short Key => 34; }
+
+[Guid("A6E7C6A1-5D9A-4D7D-A001-000000000035")]
+[PlugIn("Elf Summon cfg — Bali (35)", "Configurable summon (Bali)")]
+public sealed class ElfSummonCfg35 : ElfSummonCfgBase { public override short Key => 35; }
+
+[Guid("A6E7C6A1-5D9A-4D7D-A001-000000000036")]
+[PlugIn("Elf Summon cfg — Soldier (36)", "Configurable summon (Soldier)")]
+public sealed class ElfSummonCfg36 : ElfSummonCfgBase { public override short Key => 36; }
+
+#endregion
diff --git a/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
deleted file mode 100644
index a61ad4d60..000000000
--- a/src/GameLogic/PlugIns/ExampleSummonConfigurationPlugIn.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-//
-// Licensed under the MIT License. See LICENSE file in the project root for full license information.
-//
-
-namespace MUnique.OpenMU.GameLogic.PlugIns;
-
-using System.Linq;
-using System.Runtime.InteropServices;
-using MUnique.OpenMU.DataModel.Configuration;
-using MUnique.OpenMU.GameLogic.Attributes;
-using MUnique.OpenMU.PlugIns;
-
-///
-/// Example plug-in which replaces the default goblin summon with a Tantal and adjusts its stats.
-///
-[Guid("27F0E8B5-61D3-4FB4-96F2-8B1ED2BB8C54")]
-[PlugIn(nameof(ExampleSummonConfigurationPlugIn), "Example plug-in which replaces the goblin summon with a Tantal and adjusts its stats.")]
-public class ExampleSummonConfigurationPlugIn : ISummonConfigurationPlugIn
-{
- ///
- public MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition)
- {
- if (skill.Number != 30) // goblin summon
- {
- return defaultDefinition;
- }
-
- var baseDefinition = player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == 58); // Tantal
- if (baseDefinition is null)
- {
- return defaultDefinition;
- }
-
- var clone = baseDefinition.Clone(player.GameContext.Configuration);
- var healthAttribute = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
- if (healthAttribute is not null)
- {
- healthAttribute.Value *= 2;
- }
-
- return clone;
- }
-}
-
diff --git a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
index 453e03e95..59913a59c 100644
--- a/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
+++ b/src/GameLogic/PlugIns/ISummonConfigurationPlugIn.cs
@@ -13,8 +13,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns;
///
[Guid("C1E5C063-9CF8-4FE7-9C0F-4BB6387E3C27")]
[PlugInPoint("Summon configuration", "Provides monster definitions for summon skills.")]
-public interface ISummonConfigurationPlugIn
-
+public interface ISummonConfigurationPlugIn : IStrategyPlugIn
{
///
/// Creates the monster definition which should be used for the summon skill.
@@ -25,4 +24,3 @@ public interface ISummonConfigurationPlugIn
/// The monster definition which should be summoned, or null if the default mapping should be used.
MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition);
}
-
From 53f0888886c4c42fda2e781e678e1e5f1cfdfaf8 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Thu, 11 Sep 2025 21:03:33 -0300
Subject: [PATCH 025/190] fixing elf summon plugins
fixing the ia errors xD
---
README.md | 68 +++++++++++++++++++
.../C1-86-ItemCraftingResult_by-server.md | 4 +-
.../Attributes/MonsterAttributeHolder.cs | 13 ++--
.../Skills/AreaSkillAttackAction.cs | 6 +-
.../Skills/AreaSkillHitAction.cs | 10 ++-
src/GameLogic/PlugIns/ElfSummonsAll.cs | 43 +++++++++---
.../ServerToClient/ConnectionExtensions.cs | 4 +-
7 files changed, 129 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index c55ce943e..576fdf0a8 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,40 @@ This fork diverges from the original OpenMU project and introduces:
- New crafting recipes plus fixes for craftings, skills and grid issues.
- Updated White Wizard event data and related fixes.
+### Elf Summon Plug-in (Invocaciones Elfa)
+
+Este fork incluye un plugin configurable para cambiar las invocaciones de la Elfa (skills 30..36) y ajustar sus stats sin reiniciar el servidor.
+
+Ubicación del código: `src/GameLogic/PlugIns/ElfSummonsAll.cs`.
+
+Qué permite
+- Reemplazar el monstruo invocado por cada skill (30..36) o mantener el mapeo por defecto.
+- Ajustar multiplicadores de vida, daño mínimo/máximo y defensa por skill.
+- Aplicar cambios de configuración en caliente; basta con desinvocar y volver a invocar.
+
+Cómo habilitarlo
+- Compilar el proyecto (ver QuickStart) para que el tipo de plugin se descubra por reflección.
+- En el Panel de Administración: Plugins → filtrá por “Summon configuration”.
+- Vas a ver 7 entradas: “Elf Summon cfg … (30..36)”. Activá las que quieras usar.
+- Editá la “Custom Configuration” de cada una. Campos disponibles:
+ - `MonsterNumber` (int): 0 = usa el mapeo por defecto del servidor. >0 = número del monstruo a invocar.
+ - `HpMul` (float): multiplicador de vida.
+ - `MinDmgMul` (float): multiplicador de daño mínimo físico base.
+ - `MaxDmgMul` (float): multiplicador de daño máximo físico base.
+ - `DefMul` (float): multiplicador de defensa base.
+
+Notas importantes (si querés usar solo el plugin en otro repo)
+- Ajuste de cache de stats de monstruos (requerido para que se apliquen los multiplicadores en summons):
+ - En `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, evitá cachear por `MonsterDefinition` (que iguala por Id), porque los clones de summon comparten Id. Tomá los atributos por instancia. Este fork ya incorpora este cambio.
+- Evitar daño a tu propia invocación con skills en área (recomendado):
+ - En `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` y `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, excluí de los targets a `Monster { SummonedBy == player }`. Este fork ya lo trae aplicado.
+- Hot‑reload de configuración: El Admin Panel ya propaga cambios al `PlugInManager`. No hace falta reiniciar; desinvocá y volvé a invocar para ver los nuevos stats.
+- HUD de “pet” (barra tipo Fenrir/Raven): Las invocaciones de elfa no usan el sistema de mascotas por ítem, por lo que el cliente no muestra esa barra. Ver el nombre/owner sí está soportado. Para HUD de pet se requieren cambios de cliente.
+
+Ejemplos de uso
+- Mantener el monstruo por defecto y solo subir la vida al doble: `{"MonsterNumber":0, "HpMul":2.0}`.
+- Cambiar el mob del skill 35 (Bali) a otro número y 50% más de daño: `{"MonsterNumber":123, "MinDmgMul":1.5, "MaxDmgMul":1.5}`.
+
## Current project state
This project is currently under development without any release.
@@ -177,6 +211,40 @@ Este fork se desvía del proyecto original OpenMU e introduce:
- Nuevas recetas de crafteo y correcciones para crafteo, habilidades y problemas de cuadrícula.
- Datos actualizados del evento White Wizard y correcciones relacionadas.
+### Plugin de invocaciones de Elfa
+
+Este fork incluye un plugin configurable para cambiar las invocaciones de la Elfa (skills 30..36) y ajustar sus stats sin reiniciar el servidor.
+
+Ubicacion del codigo: `src/GameLogic/PlugIns/ElfSummonsAll.cs`.
+
+Que permite
+- Reemplazar el monstruo invocado por cada skill (30..36) o mantener el mapeo por defecto.
+- Ajustar multiplicadores de vida, dano minimo/maximo y defensa por skill.
+- Aplicar cambios de configuracion en caliente; basta con desinvocar y volver a invocar.
+
+Como habilitarlo
+- Compilar el proyecto (ver QuickStart) para que el tipo de plugin se descubra por reflexion.
+- En el Panel de Administracion: Plugins -> filtrar por "Summon configuration".
+- Vas a ver 7 entradas: "Elf Summon cfg ... (30..36)". Activa las que quieras usar.
+- Edita la "Custom Configuration" de cada una. Campos disponibles:
+ - `MonsterNumber` (int): 0 = usa el mapeo por defecto del servidor. >0 = numero del monstruo a invocar.
+ - `HpMul` (float): multiplicador de vida.
+ - `MinDmgMul` (float): multiplicador de dano minimo fisico base.
+ - `MaxDmgMul` (float): multiplicador de dano maximo fisico base.
+ - `DefMul` (float): multiplicador de defensa base.
+
+Notas importantes (si queres usar solo el plugin en otro repo)
+- Ajuste de cache de stats de monstruos (requerido para que se apliquen los multiplicadores en summons):
+ - En `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, evita cachear por `MonsterDefinition` (que iguala por Id), porque los clones de summon comparten Id. Toma los atributos por instancia. Este fork ya incorpora este cambio.
+- Evitar dano a tu propia invocacion con skills en area (recomendado):
+ - En `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` y `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, exclui de los targets a `Monster { SummonedBy == player }`. Este fork ya lo trae aplicado.
+- Hot-reload de configuracion: El Admin Panel ya propaga cambios al `PlugInManager`. No hace falta reiniciar; desinvoca y volve a invocar para ver los nuevos stats.
+- HUD de "pet" (barra tipo Fenrir/Raven): Las invocaciones de elfa no usan el sistema de mascotas por item, por lo que el cliente no muestra esa barra. Ver el nombre/owner si esta soportado. Para HUD de pet se requieren cambios de cliente.
+
+Ejemplos de uso
+- Mantener el monstruo por defecto y solo subir la vida al doble: `{"MonsterNumber": 0, "HpMul": 2.0}`.
+- Cambiar el mob del skill 35 (Bali) a otro numero y 50% mas de dano: `{"MonsterNumber": 123, "MinDmgMul": 1.5, "MaxDmgMul": 1.5}`.
+
## Estado actual del proyecto
Este proyecto se encuentra actualmente en desarrollo sin ningún lanzamiento.
diff --git a/docs/Packets/C1-86-ItemCraftingResult_by-server.md b/docs/Packets/C1-86-ItemCraftingResult_by-server.md
index 3cd762f85..f4ecf55a0 100644
--- a/docs/Packets/C1-86-ItemCraftingResult_by-server.md
+++ b/docs/Packets/C1-86-ItemCraftingResult_by-server.md
@@ -16,7 +16,9 @@ The game client updates the UI to show the resulting item.
| 1 | 1 | Byte | | Packet header - length of the packet |
| 2 | 1 | Byte | 0x86 | Packet header - packet type identifier |
| 3 | 1 | CraftingResult | | Result |
-| 4 | | Binary | | ItemData |
+| 4 | 1 | Byte | | SuccessRate |
+| 5 | 1 | Byte | | BonusRate |
+| 6 | | Binary | | ItemData |
### CraftingResult Enum
diff --git a/src/GameLogic/Attributes/MonsterAttributeHolder.cs b/src/GameLogic/Attributes/MonsterAttributeHolder.cs
index 7eb2ad332..66eb7bd37 100644
--- a/src/GameLogic/Attributes/MonsterAttributeHolder.cs
+++ b/src/GameLogic/Attributes/MonsterAttributeHolder.cs
@@ -30,7 +30,8 @@ public class MonsterAttributeHolder : IAttributeSystem
{ Stats.CurrentHealth, (m, v) => m.Health = (int)v },
};
- private static readonly ConcurrentDictionary> MonsterStatAttributesCache = new();
+ // Avoid caching by MonsterDefinition equality (which is based on Id) because cloned definitions
+ // for summons may share the same Id but have different attribute values. We resolve per-instance.
private readonly AttackableNpcBase _monster;
@@ -159,9 +160,11 @@ public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
private static IDictionary GetStatAttributeOfMonster(MonsterDefinition monsterDefinition)
{
- return MonsterStatAttributesCache.GetOrAdd(monsterDefinition, monsterDef => monsterDef.Attributes.ToDictionary(
- m => m.AttributeDefinition ?? throw Error.NotInitializedProperty(m, nameof(m.AttributeDefinition)),
- m => m.Value));
+ // Build a fresh map for this concrete instance to respect any runtime-adjusted values
+ // (e.g. cloned summon definitions with modified stats).
+ return monsterDefinition.Attributes.ToDictionary(
+ m => m.AttributeDefinition ?? throw Error.NotInitializedProperty(m, nameof(m.AttributeDefinition)),
+ m => m.Value);
}
private IDictionary GetAttributeDictionary()
@@ -178,4 +181,4 @@ private IDictionary GetAttributeDicti
return attributes;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
index b01459d57..e62f7247c 100644
--- a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
+++ b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
@@ -13,6 +13,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Pathfinding;
+using MUnique.OpenMU.GameLogic.NPC;
///
/// Action to attack with a skill which inflicts damage to an area of the current map of the player.
@@ -218,6 +219,9 @@ private static IEnumerable GetTargetsInRange(Player player, Point t
.Where(a => !a.IsAtSafezone())
?? [];
+ // Don't hit own summoned monsters with area skills.
+ targetsInRange = targetsInRange.Where(a => a is not Monster { SummonedBy: { } owner } || owner != player);
+
if (skill.AreaSkillSettings is { UseFrustumFilter: true } areaSkillSettings)
{
var filter = FrustumFilters.GetOrAdd(areaSkillSettings, static s => new FrustumBasedTargetFilter(s.FrustumStartWidth, s.FrustumEndWidth, s.FrustumDistance));
@@ -252,4 +256,4 @@ private async ValueTask ApplySkillAsync(Player player, SkillEntry skillEntry, IA
await strategy.AfterTargetGotAttackedAsync(player, target, skillEntry, targetAreaCenter, hitInfo).ConfigureAwait(false);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs b/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs
index e0483071a..d83acc8ce 100644
--- a/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs
+++ b/src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs
@@ -4,6 +4,8 @@
namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
+using MUnique.OpenMU.GameLogic.NPC;
+
///
/// Action to hit targets with an area skill, which requires explicit hits .
///
@@ -31,10 +33,16 @@ public async ValueTask AttackTargetAsync(Player player, IAttackable target, Skil
// We don't log it as hacker attempt, since the AreaSkillAttackAction already does handle this.
}
+ // Don't allow hitting own summoned monster.
+ if (target is Monster { SummonedBy: { } owner } && owner == player)
+ {
+ return;
+ }
+
if (target.CheckSkillTargetRestrictions(player, skill.Skill))
{
await target.AttackByAsync(player, skill, false).ConfigureAwait(false);
await target.TryApplyElementalEffectsAsync(player, skill).ConfigureAwait(false);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index 8fa93202e..def4256d3 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -44,31 +44,56 @@ private ElfSummonsConfigCore() { }
public MonsterDefinition? Resolve(Player player, Skill skill, MonsterDefinition? defaultDefinition)
{
- if (!this.Map.TryGetValue(skill.Number, out var cfg) || cfg.MonsterNumber is null)
+ if (!this.Map.TryGetValue(skill.Number, out var cfg))
{
- return null; // no reemplaza -> usa mapping por defecto
+ return null; // sin configuración -> usa mapping por defecto
+ }
+
+ // Seleccionamos la definición base: o la configurada, o la por defecto del servidor.
+ MonsterDefinition? baseDef;
+ if (cfg.MonsterNumber is null)
+ {
+ // Mantener mapeo por defecto, pero permitir modificar stats del defaultDefinition
+ baseDef = defaultDefinition;
+ }
+ else
+ {
+ baseDef = player.GameContext.Configuration.Monsters
+ .FirstOrDefault(m => m.Number == cfg.MonsterNumber.Value)
+ ?? defaultDefinition; // fallback al default si no existe la custom
}
- var baseDef = player.GameContext.Configuration.Monsters
- .FirstOrDefault(m => m.Number == cfg.MonsterNumber.Value);
if (baseDef is null)
{
- return defaultDefinition;
+ // No hay nada que clonar; dejamos que el handler use su flujo por defecto
+ return null;
}
var clone = baseDef.Clone(player.GameContext.Configuration);
var hp = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
- if (hp is not null && cfg.HpMul != 1.0f) hp.Value *= cfg.HpMul;
+ if (hp is not null && Math.Abs(cfg.HpMul - 1.0f) > float.Epsilon)
+ {
+ hp.Value *= cfg.HpMul;
+ }
var minDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumPhysBaseDmg);
- if (minDmg is not null && cfg.MinDmgMul != 1.0f) minDmg.Value *= cfg.MinDmgMul;
+ if (minDmg is not null && Math.Abs(cfg.MinDmgMul - 1.0f) > float.Epsilon)
+ {
+ minDmg.Value *= cfg.MinDmgMul;
+ }
var maxDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumPhysBaseDmg);
- if (maxDmg is not null && cfg.MaxDmgMul != 1.0f) maxDmg.Value *= cfg.MaxDmgMul;
+ if (maxDmg is not null && Math.Abs(cfg.MaxDmgMul - 1.0f) > float.Epsilon)
+ {
+ maxDmg.Value *= cfg.MaxDmgMul;
+ }
var def = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseBase);
- if (def is not null && cfg.DefMul != 1.0f) def.Value *= cfg.DefMul;
+ if (def is not null && Math.Abs(cfg.DefMul - 1.0f) > float.Epsilon)
+ {
+ def.Value *= cfg.DefMul;
+ }
cfg.Customize?.Invoke(clone);
return clone;
diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
index 6e5fd7e60..4fecc7178 100644
--- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
+++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs
@@ -543,8 +543,6 @@ int WritePacket()
///
/// The connection.
/// The changed player id.
- /// The success rate in percent.
- /// The additional bonus rate in percent.
/// The item data.
///
/// Is sent by the server when: The appearance of a player changed, all surrounding players are informed about it.
@@ -4583,6 +4581,8 @@ int WritePacket()
///
/// The connection.
/// The result.
+ /// The success rate.
+ /// The bonus rate.
/// The item data.
///
/// Is sent by the server when: After the player requested to execute an item crafting, e.g. at the chaos machine.
From ef3eb6a0ba6b68ec3d87acc7ab43a4e86d37220b Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Thu, 11 Sep 2025 21:32:42 -0300
Subject: [PATCH 026/190] fix aura sumons
---
.../Skills/TargetedSkillDefaultPlugin.cs | 48 ++++++++++++++-
src/GameLogic/PlugIns/ElfSummonsAll.cs | 58 ++++++++++++++++---
2 files changed, 96 insertions(+), 10 deletions(-)
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 884b52a8a..db4e865fb 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -162,6 +162,18 @@ protected virtual IEnumerable DetermineTargets(Player player, IAtta
{
if (skill.Target == SkillTarget.ImplicitPlayer)
{
+ // Include own summon for buffs/regeneration
+ if (skill.SkillType is SkillType.Buff or SkillType.Regeneration)
+ {
+ var list = new List { player };
+ var summon = player.Summon?.Item1;
+ if (summon is { IsAlive: true })
+ {
+ list.Add(summon);
+ }
+ return list;
+ }
+
return player.GetAsEnumerable();
}
@@ -169,9 +181,43 @@ protected virtual IEnumerable DetermineTargets(Player player, IAtta
{
if (player.Party != null)
{
+ if (skill.SkillType is SkillType.Buff or SkillType.Regeneration)
+ {
+ var result = new List();
+ foreach (var member in player.Party.PartyList.OfType())
+ {
+ if (player.Observers.Contains(member))
+ {
+ result.Add(member);
+ }
+
+ var memberSummon = member.Summon?.Item1;
+ if (memberSummon is { IsAlive: true })
+ {
+ result.Add(memberSummon);
+ }
+ }
+
+ if (result.Count > 0)
+ {
+ return result;
+ }
+ }
+
return player.Party.PartyList.OfType().Where(p => player.Observers.Contains((IWorldObserver)p));
}
+ if (skill.SkillType is SkillType.Buff or SkillType.Regeneration)
+ {
+ var list = new List { player };
+ var summon = player.Summon?.Item1;
+ if (summon is { IsAlive: true })
+ {
+ list.Add(summon);
+ }
+ return list;
+ }
+
return player.GetAsEnumerable();
}
@@ -317,4 +363,4 @@ private async ValueTask ApplySkillAsync(Player player, IAttackable targete
return success;
}
-}
\ No newline at end of file
+}
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index def4256d3..9105d32d5 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -71,28 +71,37 @@ private ElfSummonsConfigCore() { }
var clone = baseDef.Clone(player.GameContext.Configuration);
+ // Dynamic scaling by summoners TotalEnergy: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
+ var energy = player.Attributes?[Stats.TotalEnergy] ?? 0;
+ var steps = cfg.EnergyPerStep > 0 ? (int)(energy / cfg.EnergyPerStep) : 0;
+ var energyScale = 1.0f + Math.Max(0, steps) * Math.Max(0, cfg.PercentPerStep);
+
var hp = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
- if (hp is not null && Math.Abs(cfg.HpMul - 1.0f) > float.Epsilon)
+ if (hp is not null)
{
- hp.Value *= cfg.HpMul;
+ var mul = cfg.HpMul * energyScale;
+ if (Math.Abs(mul - 1.0f) > float.Epsilon) hp.Value *= mul;
}
var minDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumPhysBaseDmg);
- if (minDmg is not null && Math.Abs(cfg.MinDmgMul - 1.0f) > float.Epsilon)
+ if (minDmg is not null)
{
- minDmg.Value *= cfg.MinDmgMul;
+ var mul = cfg.MinDmgMul * energyScale;
+ if (Math.Abs(mul - 1.0f) > float.Epsilon) minDmg.Value *= mul;
}
var maxDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumPhysBaseDmg);
- if (maxDmg is not null && Math.Abs(cfg.MaxDmgMul - 1.0f) > float.Epsilon)
+ if (maxDmg is not null)
{
- maxDmg.Value *= cfg.MaxDmgMul;
+ var mul = cfg.MaxDmgMul * energyScale;
+ if (Math.Abs(mul - 1.0f) > float.Epsilon) maxDmg.Value *= mul;
}
var def = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseBase);
- if (def is not null && Math.Abs(cfg.DefMul - 1.0f) > float.Epsilon)
+ if (def is not null)
{
- def.Value *= cfg.DefMul;
+ var mul = cfg.DefMul * energyScale;
+ if (Math.Abs(mul - 1.0f) > float.Epsilon) def.Value *= mul;
}
cfg.Customize?.Invoke(clone);
@@ -106,6 +115,9 @@ public sealed class SummonConfig
public float MinDmgMul { get; set; } = 1.0f;
public float MaxDmgMul { get; set; } = 1.0f;
public float DefMul { get; set; } = 1.0f;
+ // Dynamic scaling by Energy: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
+ public int EnergyPerStep { get; set; } = 0; // 0 disables scaling
+ public float PercentPerStep { get; set; } = 0.0f; // e.g. 0.05 for +5% per step
public System.Action? Customize { get; set; }
}
}
@@ -127,6 +139,9 @@ public class ElfSummonSkillConfiguration
public float MinDmgMul { get; set; } = 1.0f;
public float MaxDmgMul { get; set; } = 1.0f;
public float DefMul { get; set; } = 1.0f;
+ // Dynamic scaling by Energy: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
+ public int EnergyPerStep { get; set; } = 0; // 0 = disabled
+ public float PercentPerStep { get; set; } = 0.0f; // e.g. 0.05 for +5% per 1000 energy
}
@@ -174,6 +189,8 @@ public ElfSummonSkillConfiguration? Configuration
entry.MinDmgMul = value.MinDmgMul;
entry.MaxDmgMul = value.MaxDmgMul;
entry.DefMul = value.DefMul;
+ entry.EnergyPerStep = value.EnergyPerStep;
+ entry.PercentPerStep = value.PercentPerStep;
}
}
@@ -187,11 +204,34 @@ public object CreateDefaultConfig()
MinDmgMul = entry.MinDmgMul,
MaxDmgMul = entry.MaxDmgMul,
DefMul = entry.DefMul,
+ EnergyPerStep = entry.EnergyPerStep,
+ PercentPerStep = entry.PercentPerStep,
};
}
public MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition)
- => ElfSummonsConfigCore.Instance.Resolve(player, skill, defaultDefinition);
+ {
+ // Best-effort: Pull latest configuration directly from GameConfiguration in case change events didn't arrive (e.g. separate process/container lifecycle).
+ try
+ {
+ var typeId = this.GetType().GUID;
+ var plugInConfig = player.GameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId);
+ if (plugInConfig is not null)
+ {
+ var latest = plugInConfig.GetConfiguration(player.GameContext.PlugInManager.CustomConfigReferenceHandler);
+ if (latest is not null)
+ {
+ this.Configuration = latest; // updates core map
+ }
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+
+ return ElfSummonsConfigCore.Instance.Resolve(player, skill, defaultDefinition);
+ }
}
#endregion
From 3dba63ade04fc80fa45068ca049260817335cecb Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Thu, 11 Sep 2025 21:43:00 -0300
Subject: [PATCH 027/190] add docu
---
README.md | 84 +++++++++++++++++++++++++++++--------------------------
1 file changed, 45 insertions(+), 39 deletions(-)
diff --git a/README.md b/README.md
index 576fdf0a8..7df3ab09d 100644
--- a/README.md
+++ b/README.md
@@ -47,41 +47,44 @@ This fork diverges from the original OpenMU project and introduces:
- New crafting recipes plus fixes for craftings, skills and grid issues.
- Updated White Wizard event data and related fixes.
-### Elf Summon Plug-in (Invocaciones Elfa)
-
-Este fork incluye un plugin configurable para cambiar las invocaciones de la Elfa (skills 30..36) y ajustar sus stats sin reiniciar el servidor.
-
-Ubicación del código: `src/GameLogic/PlugIns/ElfSummonsAll.cs`.
-
-Qué permite
-- Reemplazar el monstruo invocado por cada skill (30..36) o mantener el mapeo por defecto.
-- Ajustar multiplicadores de vida, daño mínimo/máximo y defensa por skill.
-- Aplicar cambios de configuración en caliente; basta con desinvocar y volver a invocar.
-
-Cómo habilitarlo
-- Compilar el proyecto (ver QuickStart) para que el tipo de plugin se descubra por reflección.
-- En el Panel de Administración: Plugins → filtrá por “Summon configuration”.
-- Vas a ver 7 entradas: “Elf Summon cfg … (30..36)”. Activá las que quieras usar.
-- Editá la “Custom Configuration” de cada una. Campos disponibles:
- - `MonsterNumber` (int): 0 = usa el mapeo por defecto del servidor. >0 = número del monstruo a invocar.
- - `HpMul` (float): multiplicador de vida.
- - `MinDmgMul` (float): multiplicador de daño mínimo físico base.
- - `MaxDmgMul` (float): multiplicador de daño máximo físico base.
- - `DefMul` (float): multiplicador de defensa base.
-
-Notas importantes (si querés usar solo el plugin en otro repo)
-- Ajuste de cache de stats de monstruos (requerido para que se apliquen los multiplicadores en summons):
- - En `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, evitá cachear por `MonsterDefinition` (que iguala por Id), porque los clones de summon comparten Id. Tomá los atributos por instancia. Este fork ya incorpora este cambio.
-- Evitar daño a tu propia invocación con skills en área (recomendado):
- - En `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` y `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, excluí de los targets a `Monster { SummonedBy == player }`. Este fork ya lo trae aplicado.
-- Hot‑reload de configuración: El Admin Panel ya propaga cambios al `PlugInManager`. No hace falta reiniciar; desinvocá y volvé a invocar para ver los nuevos stats.
-- HUD de “pet” (barra tipo Fenrir/Raven): Las invocaciones de elfa no usan el sistema de mascotas por ítem, por lo que el cliente no muestra esa barra. Ver el nombre/owner sí está soportado. Para HUD de pet se requieren cambios de cliente.
-
-Ejemplos de uso
-- Mantener el monstruo por defecto y solo subir la vida al doble: `{"MonsterNumber":0, "HpMul":2.0}`.
-- Cambiar el mob del skill 35 (Bali) a otro número y 50% más de daño: `{"MonsterNumber":123, "MinDmgMul":1.5, "MaxDmgMul":1.5}`.
-
-## Current project state
+### Elf Summon Plug-in
+
+This fork includes a configurable plug-in to change Elf summons (skills 30..36) and tweak their stats without restarting the server.
+
+Code location: `src/GameLogic/PlugIns/ElfSummonsAll.cs`.
+
+What it provides
+- Replace the summoned monster per skill (30..36) or keep the default mapping.
+- Adjust per-skill multipliers: HP, minimum/maximum physical base damage, and defense.
+- Dynamic scaling by Energy: `scale = 1 + floor(TotalEnergy / EnergyPerStep) * PercentPerStep` applied to HP/DMG/DEF.
+- Buff/regeneration skills also include your own summon (and party members summons) when the target mode is self/party.
+- Apply configuration changes at runtime; just unsummon and summon again.
+
+How to enable
+- Build the project (see QuickStart) so the plug-in type is discovered.
+- In the Admin Panel: Plugins ? filter by "Summon configuration".
+- You will see 7 entries: "Elf Summon cfg (30..36)". Activate the ones you need.
+- Edit the "Custom Configuration" of each. Available fields:
+ - `MonsterNumber` (int): 0 = use the server default mapping; >0 = monster number to summon.
+ - `HpMul` (float): HP multiplier.
+ - `MinDmgMul` (float): minimum physical base damage multiplier.
+ - `MaxDmgMul` (float): maximum physical base damage multiplier.
+ - `DefMul` (float): base defense multiplier.
+ - `EnergyPerStep` (int): 0 to disable; otherwise size of each Energy step (e.g. 1000).
+ - `PercentPerStep` (float): added per step (e.g. 0.05 = +5%).
+
+Important notes (for using this plug-in in another repo)
+- Monster stat cache adjustment (required so multipliers apply to summons):
+ - In `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, dont cache by `MonsterDefinition` (equals by Id). Summoned clones share Id; read attributes per-instance instead. Included in this fork.
+- Prevent damage to your own summon with area skills (recommended):
+ - In `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` and `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, exclude `Monster { SummonedBy == player }` from targets. Included in this fork.
+- Configuration hot-reload: On each summon creation, the plug-in fetches the latest CustomConfiguration from GameConfiguration. No restart required; just re-summon.
+- Pet HUD (Fenrir/Raven bar): Elf summons dont use the item-pet system, so the stock client doesnt show that bar. Name/owner display is supported. Pet HUD would require client changes.
+
+Examples
+- Keep default monster and double HP: `{"MonsterNumber": 0, "HpMul": 2.0}`.
+- +5% per 1000 Energy: `{"MonsterNumber": 0, "EnergyPerStep": 1000, "PercentPerStep": 0.05}`.
+- Change skill 35 (Bali) to another monster with +50% damage: `{"MonsterNumber": 123, "MinDmgMul": 1.5, "MaxDmgMul": 1.5}`.## Current project state
This project is currently under development without any release.
You can try the current state by using the available docker image, also
@@ -220,6 +223,8 @@ Ubicacion del codigo: `src/GameLogic/PlugIns/ElfSummonsAll.cs`.
Que permite
- Reemplazar el monstruo invocado por cada skill (30..36) o mantener el mapeo por defecto.
- Ajustar multiplicadores de vida, dano minimo/maximo y defensa por skill.
+- Escalado dinamico por Energia: `scale = 1 + floor(TotalEnergy / EnergyPerStep) * PercentPerStep` aplicado a HP/DMG/DEF.
+- Los skills de Buff/Regeneration tambien incluyen a tu propio summon (y a los summons del party) cuando el target es self/party.
- Aplicar cambios de configuracion en caliente; basta con desinvocar y volver a invocar.
Como habilitarlo
@@ -232,20 +237,21 @@ Como habilitarlo
- `MinDmgMul` (float): multiplicador de dano minimo fisico base.
- `MaxDmgMul` (float): multiplicador de dano maximo fisico base.
- `DefMul` (float): multiplicador de defensa base.
+ - `EnergyPerStep` (int): 0 para desactivar; si no, tamano de cada paso de Energia (p.ej. 1000).
+ - `PercentPerStep` (float): incremento por paso (p.ej. 0.05 = +5%).
Notas importantes (si queres usar solo el plugin en otro repo)
- Ajuste de cache de stats de monstruos (requerido para que se apliquen los multiplicadores en summons):
- En `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, evita cachear por `MonsterDefinition` (que iguala por Id), porque los clones de summon comparten Id. Toma los atributos por instancia. Este fork ya incorpora este cambio.
- Evitar dano a tu propia invocacion con skills en area (recomendado):
- En `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` y `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, exclui de los targets a `Monster { SummonedBy == player }`. Este fork ya lo trae aplicado.
-- Hot-reload de configuracion: El Admin Panel ya propaga cambios al `PlugInManager`. No hace falta reiniciar; desinvoca y volve a invocar para ver los nuevos stats.
+- Hot-reload de configuracion: En cada creacion del summon, el plugin lee la CustomConfiguration mas reciente desde GameConfiguration. No hace falta reiniciar; desinvoca y volve a invocar para ver los nuevos stats.
- HUD de "pet" (barra tipo Fenrir/Raven): Las invocaciones de elfa no usan el sistema de mascotas por item, por lo que el cliente no muestra esa barra. Ver el nombre/owner si esta soportado. Para HUD de pet se requieren cambios de cliente.
Ejemplos de uso
- Mantener el monstruo por defecto y solo subir la vida al doble: `{"MonsterNumber": 0, "HpMul": 2.0}`.
-- Cambiar el mob del skill 35 (Bali) a otro numero y 50% mas de dano: `{"MonsterNumber": 123, "MinDmgMul": 1.5, "MaxDmgMul": 1.5}`.
-
-## Estado actual del proyecto
+- +5% por cada 1000 de Energia: `{"MonsterNumber": 0, "EnergyPerStep": 1000, "PercentPerStep": 0.05}`.
+- Cambiar el mob del skill 35 (Bali) a otro numero y 50% mas de dano: `{"MonsterNumber": 123, "MinDmgMul": 1.5, "MaxDmgMul": 1.5}`.## Estado actual del proyecto
Este proyecto se encuentra actualmente en desarrollo sin ningún lanzamiento.
Puedes probar el estado actual utilizando la imagen de docker disponible, mencionada también en la [guía rápida](QuickStart.md).
From b1d3de367701763baa8d8beae876f50f9c33b4c4 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Thu, 11 Sep 2025 21:52:38 -0300
Subject: [PATCH 028/190] fix updates bugs
---
src/GameLogic/PlugIns/ElfSummonsAll.cs | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index 9105d32d5..f5e6e2b9b 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -211,11 +211,14 @@ public object CreateDefaultConfig()
public MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition)
{
- // Best-effort: Pull latest configuration directly from GameConfiguration in case change events didn't arrive (e.g. separate process/container lifecycle).
+ // Best-effort: Pull latest configuration directly from persistence, bypassing caches,
+ // so cross-process/container changes apply without restart.
try
{
var typeId = this.GetType().GUID;
- var plugInConfig = player.GameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId);
+ using var cfgCtx = player.GameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), useCache: false);
+ var all = cfgCtx.GetAsync().AsTask().GetAwaiter().GetResult();
+ var plugInConfig = all.FirstOrDefault(c => c.TypeId == typeId);
if (plugInConfig is not null)
{
var latest = plugInConfig.GetConfiguration(player.GameContext.PlugInManager.CustomConfigReferenceHandler);
@@ -227,7 +230,7 @@ public object CreateDefaultConfig()
}
catch
{
- // ignore
+ // ignore and continue with current in-memory config
}
return ElfSummonsConfigCore.Instance.Resolve(player, skill, defaultDefinition);
From a5ca33e25bbf0cabb8238617b02ed336fe3427c5 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Thu, 11 Sep 2025 22:19:18 -0300
Subject: [PATCH 029/190] fix stats
---
.../Attributes/MonsterAttributeHolder.cs | 10 ++-
src/GameLogic/PlugIns/ElfSummonsAll.cs | 69 ++++++++++---------
2 files changed, 45 insertions(+), 34 deletions(-)
diff --git a/src/GameLogic/Attributes/MonsterAttributeHolder.cs b/src/GameLogic/Attributes/MonsterAttributeHolder.cs
index 66eb7bd37..0f394ebc0 100644
--- a/src/GameLogic/Attributes/MonsterAttributeHolder.cs
+++ b/src/GameLogic/Attributes/MonsterAttributeHolder.cs
@@ -17,8 +17,14 @@ public class MonsterAttributeHolder : IAttributeSystem
new Dictionary>
{
{ Stats.CurrentHealth, m => m.Health },
- { Stats.DefensePvm, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) },
- { Stats.DefensePvp, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) },
+ { Stats.DefensePvm, m =>
+ m.Attributes.GetValueOfAttribute(Stats.DefenseBase)
+ + m.Attributes.GetValueOfAttribute(Stats.DefenseFinal)
+ + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) },
+ { Stats.DefensePvp, m =>
+ m.Attributes.GetValueOfAttribute(Stats.DefenseBase)
+ + m.Attributes.GetValueOfAttribute(Stats.DefenseFinal)
+ + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) },
{ Stats.DamageReceiveDecrement, m => 1.0f },
{ Stats.AttackDamageIncrease, m => 1.0f },
{ Stats.ShieldBypassChance, m => 1.0f },
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index f5e6e2b9b..0392a5dce 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -71,37 +71,59 @@ private ElfSummonsConfigCore() { }
var clone = baseDef.Clone(player.GameContext.Configuration);
- // Dynamic scaling by summoners TotalEnergy: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
+ // Dynamic scaling by summoners TotalEnergy only:
+ // scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
var energy = player.Attributes?[Stats.TotalEnergy] ?? 0;
var steps = cfg.EnergyPerStep > 0 ? (int)(energy / cfg.EnergyPerStep) : 0;
var energyScale = 1.0f + Math.Max(0, steps) * Math.Max(0, cfg.PercentPerStep);
+ // Apply scaling to base stats of the chosen monster
var hp = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
- if (hp is not null)
+ if (hp is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
{
- var mul = cfg.HpMul * energyScale;
- if (Math.Abs(mul - 1.0f) > float.Epsilon) hp.Value *= mul;
+ hp.Value *= energyScale;
}
+ // Physical base damage
var minDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumPhysBaseDmg);
- if (minDmg is not null)
+ if (minDmg is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
{
- var mul = cfg.MinDmgMul * energyScale;
- if (Math.Abs(mul - 1.0f) > float.Epsilon) minDmg.Value *= mul;
+ minDmg.Value *= energyScale;
}
-
var maxDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumPhysBaseDmg);
- if (maxDmg is not null)
+ if (maxDmg is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
+ {
+ maxDmg.Value *= energyScale;
+ }
+
+ // Wizardry base damage (some monsters use wizardry damage)
+ var minWiz = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumWizBaseDmg);
+ if (minWiz is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
+ {
+ minWiz.Value *= energyScale;
+ }
+ var maxWiz = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumWizBaseDmg);
+ if (maxWiz is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
{
- var mul = cfg.MaxDmgMul * energyScale;
- if (Math.Abs(mul - 1.0f) > float.Epsilon) maxDmg.Value *= mul;
+ maxWiz.Value *= energyScale;
+ }
+
+ // Curse base damage (rare cases)
+ var minCurse = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumCurseBaseDmg);
+ if (minCurse is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
+ {
+ minCurse.Value *= energyScale;
+ }
+ var maxCurse = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumCurseBaseDmg);
+ if (maxCurse is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
+ {
+ maxCurse.Value *= energyScale;
}
var def = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseBase);
- if (def is not null)
+ if (def is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
{
- var mul = cfg.DefMul * energyScale;
- if (Math.Abs(mul - 1.0f) > float.Epsilon) def.Value *= mul;
+ def.Value *= energyScale;
}
cfg.Customize?.Invoke(clone);
@@ -111,11 +133,7 @@ private ElfSummonsConfigCore() { }
public sealed class SummonConfig
{
public ushort? MonsterNumber { get; set; }
- public float HpMul { get; set; } = 1.0f;
- public float MinDmgMul { get; set; } = 1.0f;
- public float MaxDmgMul { get; set; } = 1.0f;
- public float DefMul { get; set; } = 1.0f;
- // Dynamic scaling by Energy: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
+ // Dynamic scaling by Energy only: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
public int EnergyPerStep { get; set; } = 0; // 0 disables scaling
public float PercentPerStep { get; set; } = 0.0f; // e.g. 0.05 for +5% per step
public System.Action? Customize { get; set; }
@@ -134,11 +152,6 @@ public class ElfSummonSkillConfiguration
[System.ComponentModel.DisplayName("Monster Number (0 = default)")]
[System.ComponentModel.DataAnnotations.Range(0, 65535)]
public int MonsterNumber { get; set; } = 0;
-
- public float HpMul { get; set; } = 1.0f;
- public float MinDmgMul { get; set; } = 1.0f;
- public float MaxDmgMul { get; set; } = 1.0f;
- public float DefMul { get; set; } = 1.0f;
// Dynamic scaling by Energy: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
public int EnergyPerStep { get; set; } = 0; // 0 = disabled
public float PercentPerStep { get; set; } = 0.0f; // e.g. 0.05 for +5% per 1000 energy
@@ -185,10 +198,6 @@ public ElfSummonSkillConfiguration? Configuration
entry.MonsterNumber = clamped;
}
- entry.HpMul = value.HpMul;
- entry.MinDmgMul = value.MinDmgMul;
- entry.MaxDmgMul = value.MaxDmgMul;
- entry.DefMul = value.DefMul;
entry.EnergyPerStep = value.EnergyPerStep;
entry.PercentPerStep = value.PercentPerStep;
}
@@ -200,10 +209,6 @@ public object CreateDefaultConfig()
return new ElfSummonSkillConfiguration
{
MonsterNumber = entry.MonsterNumber.HasValue ? entry.MonsterNumber.Value : 0,
- HpMul = entry.HpMul,
- MinDmgMul = entry.MinDmgMul,
- MaxDmgMul = entry.MaxDmgMul,
- DefMul = entry.DefMul,
EnergyPerStep = entry.EnergyPerStep,
PercentPerStep = entry.PercentPerStep,
};
From 6247574772f3576a3491381b5887c377db121e11 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 02:51:39 -0300
Subject: [PATCH 030/190] fix updater
---
README.md | 54 ++++------
src/GameLogic/PlugIns/ElfSummonsAll.cs | 21 +++-
.../Updates/ElfSummonDefaultsUpdatePlugIn.cs | 101 ++++++++++++++++++
.../Initialization/Updates/UpdateVersion.cs | 5 +
src/Startup/Logging/ConsoleLog.cs | 10 ++
src/Startup/Logging/InMemoryLogBuffer.cs | 37 +++++++
src/Startup/Logging/InMemorySerilogSink.cs | 40 +++++++
src/Startup/Program.cs | 15 ++-
8 files changed, 245 insertions(+), 38 deletions(-)
create mode 100644 src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs
create mode 100644 src/Startup/Logging/ConsoleLog.cs
create mode 100644 src/Startup/Logging/InMemoryLogBuffer.cs
create mode 100644 src/Startup/Logging/InMemorySerilogSink.cs
diff --git a/README.md b/README.md
index 7df3ab09d..7d0787b92 100644
--- a/README.md
+++ b/README.md
@@ -49,42 +49,35 @@ This fork diverges from the original OpenMU project and introduces:
### Elf Summon Plug-in
-This fork includes a configurable plug-in to change Elf summons (skills 30..36) and tweak their stats without restarting the server.
+This fork includes a configurable plug-in to change Elf summons (skills 30..36) and scale their stats by Energy without restarting the server.
Code location: `src/GameLogic/PlugIns/ElfSummonsAll.cs`.
What it provides
- Replace the summoned monster per skill (30..36) or keep the default mapping.
-- Adjust per-skill multipliers: HP, minimum/maximum physical base damage, and defense.
-- Dynamic scaling by Energy: `scale = 1 + floor(TotalEnergy / EnergyPerStep) * PercentPerStep` applied to HP/DMG/DEF.
+- Dynamic scaling by Energy applied to the base stats of the chosen monster (HP, base damage Phys/Wiz/Curse, DefenseBase):
+ `scale = 1 + floor(TotalEnergy / EnergyPerStep) * PercentPerStep`.
- Buff/regeneration skills also include your own summon (and party members summons) when the target mode is self/party.
- Apply configuration changes at runtime; just unsummon and summon again.
How to enable
-- Build the project (see QuickStart) so the plug-in type is discovered.
- In the Admin Panel: Plugins ? filter by "Summon configuration".
- You will see 7 entries: "Elf Summon cfg (30..36)". Activate the ones you need.
- Edit the "Custom Configuration" of each. Available fields:
- `MonsterNumber` (int): 0 = use the server default mapping; >0 = monster number to summon.
- - `HpMul` (float): HP multiplier.
- - `MinDmgMul` (float): minimum physical base damage multiplier.
- - `MaxDmgMul` (float): maximum physical base damage multiplier.
- - `DefMul` (float): base defense multiplier.
- `EnergyPerStep` (int): 0 to disable; otherwise size of each Energy step (e.g. 1000).
- `PercentPerStep` (float): added per step (e.g. 0.05 = +5%).
Important notes (for using this plug-in in another repo)
-- Monster stat cache adjustment (required so multipliers apply to summons):
+- Monster stat cache adjustment (required so scaling applies to summons):
- In `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, dont cache by `MonsterDefinition` (equals by Id). Summoned clones share Id; read attributes per-instance instead. Included in this fork.
- Prevent damage to your own summon with area skills (recommended):
- In `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` and `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, exclude `Monster { SummonedBy == player }` from targets. Included in this fork.
-- Configuration hot-reload: On each summon creation, the plug-in fetches the latest CustomConfiguration from GameConfiguration. No restart required; just re-summon.
+- Configuration hot-reload: On each summon creation, the plug-in fetches the latest CustomConfiguration from the database (no cache). No restart required; just re-summon.
- Pet HUD (Fenrir/Raven bar): Elf summons dont use the item-pet system, so the stock client doesnt show that bar. Name/owner display is supported. Pet HUD would require client changes.
Examples
-- Keep default monster and double HP: `{"MonsterNumber": 0, "HpMul": 2.0}`.
-- +5% per 1000 Energy: `{"MonsterNumber": 0, "EnergyPerStep": 1000, "PercentPerStep": 0.05}`.
-- Change skill 35 (Bali) to another monster with +50% damage: `{"MonsterNumber": 123, "MinDmgMul": 1.5, "MaxDmgMul": 1.5}`.## Current project state
+- +5% per 1000 Energy using default monster: `{"MonsterNumber": 0, "EnergyPerStep": 1000, "PercentPerStep": 0.05}`.## Current project state
This project is currently under development without any release.
You can try the current state by using the available docker image, also
@@ -216,42 +209,35 @@ Este fork se desvía del proyecto original OpenMU e introduce:
### Plugin de invocaciones de Elfa
-Este fork incluye un plugin configurable para cambiar las invocaciones de la Elfa (skills 30..36) y ajustar sus stats sin reiniciar el servidor.
+Este fork incluye un plugin configurable para cambiar las invocaciones de la Elfa (skills 30..36) y escalar sus stats en base a la Energa, sin reiniciar el servidor.
Ubicacion del codigo: `src/GameLogic/PlugIns/ElfSummonsAll.cs`.
Que permite
- Reemplazar el monstruo invocado por cada skill (30..36) o mantener el mapeo por defecto.
-- Ajustar multiplicadores de vida, dano minimo/maximo y defensa por skill.
-- Escalado dinamico por Energia: `scale = 1 + floor(TotalEnergy / EnergyPerStep) * PercentPerStep` aplicado a HP/DMG/DEF.
-- Los skills de Buff/Regeneration tambien incluyen a tu propio summon (y a los summons del party) cuando el target es self/party.
-- Aplicar cambios de configuracion en caliente; basta con desinvocar y volver a invocar.
+- Escalado por Energia aplicado a los stats base del monstruo elegido (HP, dao base Fis/Wiz/Curse, DefenseBase):
+ `scale = 1 + floor(TotalEnergy / EnergyPerStep) * PercentPerStep`.
+- Los skills de Buff/Regeneration incluyen al summon propio (y los del party) cuando el target es self/party.
+- Cambios de configuracion en caliente; basta con desinvocar y volver a invocar.
Como habilitarlo
-- Compilar el proyecto (ver QuickStart) para que el tipo de plugin se descubra por reflexion.
- En el Panel de Administracion: Plugins -> filtrar por "Summon configuration".
- Vas a ver 7 entradas: "Elf Summon cfg ... (30..36)". Activa las que quieras usar.
- Edita la "Custom Configuration" de cada una. Campos disponibles:
- - `MonsterNumber` (int): 0 = usa el mapeo por defecto del servidor. >0 = numero del monstruo a invocar.
- - `HpMul` (float): multiplicador de vida.
- - `MinDmgMul` (float): multiplicador de dano minimo fisico base.
- - `MaxDmgMul` (float): multiplicador de dano maximo fisico base.
- - `DefMul` (float): multiplicador de defensa base.
- - `EnergyPerStep` (int): 0 para desactivar; si no, tamano de cada paso de Energia (p.ej. 1000).
+ - `MonsterNumber` (int): 0 = usa el mapeo por defecto del servidor; >0 = numero de monstruo a invocar.
+ - `EnergyPerStep` (int): 0 para desactivar; si no, tamao de cada paso de Energia (p.ej. 1000).
- `PercentPerStep` (float): incremento por paso (p.ej. 0.05 = +5%).
Notas importantes (si queres usar solo el plugin en otro repo)
-- Ajuste de cache de stats de monstruos (requerido para que se apliquen los multiplicadores en summons):
- - En `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, evita cachear por `MonsterDefinition` (que iguala por Id), porque los clones de summon comparten Id. Toma los atributos por instancia. Este fork ya incorpora este cambio.
-- Evitar dano a tu propia invocacion con skills en area (recomendado):
- - En `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` y `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, exclui de los targets a `Monster { SummonedBy == player }`. Este fork ya lo trae aplicado.
-- Hot-reload de configuracion: En cada creacion del summon, el plugin lee la CustomConfiguration mas reciente desde GameConfiguration. No hace falta reiniciar; desinvoca y volve a invocar para ver los nuevos stats.
-- HUD de "pet" (barra tipo Fenrir/Raven): Las invocaciones de elfa no usan el sistema de mascotas por item, por lo que el cliente no muestra esa barra. Ver el nombre/owner si esta soportado. Para HUD de pet se requieren cambios de cliente.
+- Ajuste de cache de stats de monstruos (requerido para que el escalado aplique):
+ - En `src/GameLogic/Attributes/MonsterAttributeHolder.cs`, evita cachear por `MonsterDefinition` (igual por Id). Los clones del summon comparten Id; leer por instancia. Incluido en este fork.
+- Evitar dao al propio summon con skills en area (recomendado):
+ - En `src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs` y `src/GameLogic/PlayerActions/Skills/AreaSkillHitAction.cs`, excluir `Monster { SummonedBy == player }` de los targets. Incluido en este fork.
+- Hot-reload: En cada creacion del summon, el plugin lee la CustomConfiguration mas reciente desde la base de datos (sin cache). No hace falta reiniciar; desinvoca y volve a invocar.
+- HUD de "pet": Las invocaciones de elfa no usan el sistema de mascotas por item, por lo que el cliente no muestra esa barra.
Ejemplos de uso
-- Mantener el monstruo por defecto y solo subir la vida al doble: `{"MonsterNumber": 0, "HpMul": 2.0}`.
-- +5% por cada 1000 de Energia: `{"MonsterNumber": 0, "EnergyPerStep": 1000, "PercentPerStep": 0.05}`.
-- Cambiar el mob del skill 35 (Bali) a otro numero y 50% mas de dano: `{"MonsterNumber": 123, "MinDmgMul": 1.5, "MaxDmgMul": 1.5}`.## Estado actual del proyecto
+- +5% por cada 1000 de Energia usando el mob por defecto: `{"MonsterNumber": 0, "EnergyPerStep": 1000, "PercentPerStep": 0.05}`.## Estado actual del proyecto
Este proyecto se encuentra actualmente en desarrollo sin ningún lanzamiento.
Puedes probar el estado actual utilizando la imagen de docker disponible, mencionada también en la [guía rápida](QuickStart.md).
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index 0392a5dce..5be18e1d0 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -126,6 +126,23 @@ private ElfSummonsConfigCore() { }
def.Value *= energyScale;
}
+ // Attack/Defense rate scaling for better hit chance and survivability
+ var atkRate = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.AttackRatePvm);
+ if (atkRate is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
+ {
+ atkRate.Value *= energyScale;
+ }
+ var defRatePvm = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseRatePvm);
+ if (defRatePvm is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
+ {
+ defRatePvm.Value *= energyScale;
+ }
+ var defRatePvp = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseRatePvp);
+ if (defRatePvp is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
+ {
+ defRatePvp.Value *= energyScale;
+ }
+
cfg.Customize?.Invoke(clone);
return clone;
}
@@ -134,8 +151,8 @@ public sealed class SummonConfig
{
public ushort? MonsterNumber { get; set; }
// Dynamic scaling by Energy only: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
- public int EnergyPerStep { get; set; } = 0; // 0 disables scaling
- public float PercentPerStep { get; set; } = 0.0f; // e.g. 0.05 for +5% per step
+ public int EnergyPerStep { get; set; } = 1000; // default: per 1000 Energy
+ public float PercentPerStep { get; set; } = 0.05f; // default: +5% per step
public System.Action? Customize { get; set; }
}
}
diff --git a/src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs
new file mode 100644
index 000000000..54b67d201
--- /dev/null
+++ b/src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs
@@ -0,0 +1,101 @@
+// This update sets sensible defaults for Elf Summon configuration
+// on already-initialized servers: EnergyPerStep=1000 and PercentPerStep=0.05
+
+namespace MUnique.OpenMU.Persistence.Initialization.Updates;
+
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.PlugIns;
+
+[PlugIn(PlugInName, PlugInDescription)]
+[Guid("4C92E2E8-1E5B-4E1D-B2B4-0D5A29B87C42")]
+public class ElfSummonDefaultsUpdatePlugIn : UpdatePlugInBase
+{
+ internal const string PlugInName = "Elf Summon energy scaling defaults";
+ internal const string PlugInDescription = "Sets default EnergyPerStep=1000 and PercentPerStep=0.05 for Elf Summon configuration (skills 30..36).";
+
+ // TypeIds of ElfSummonCfg30..36 (see ElfSummonsAll.cs)
+ private static readonly Guid[] ElfSummonTypeIds =
+ [
+ new("A6E7C6A1-5D9A-4D7D-A001-000000000030"),
+ new("A6E7C6A1-5D9A-4D7D-A001-000000000031"),
+ new("A6E7C6A1-5D9A-4D7D-A001-000000000032"),
+ new("A6E7C6A1-5D9A-4D7D-A001-000000000033"),
+ new("A6E7C6A1-5D9A-4D7D-A001-000000000034"),
+ new("A6E7C6A1-5D9A-4D7D-A001-000000000035"),
+ new("A6E7C6A1-5D9A-4D7D-A001-000000000036"),
+ ];
+
+ public override UpdateVersion Version => (UpdateVersion)46;
+
+ public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id;
+
+ public override string Name => PlugInName;
+
+ public override string Description => PlugInDescription;
+
+ public override bool IsMandatory => false;
+
+ public override DateTime CreatedAt => new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
+ {
+ foreach (var cfg in gameConfiguration.PlugInConfigurations.Where(c => ElfSummonTypeIds.Contains(c.TypeId)))
+ {
+ cfg.CustomConfiguration = UpdateJson(cfg.CustomConfiguration);
+ }
+
+ return ValueTask.CompletedTask;
+ }
+
+ private static string UpdateJson(string? json)
+ {
+ int monsterNumber = 0;
+ int energyPerStep = 1000;
+ float percentPerStep = 0.05f;
+
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(json))
+ {
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ if (root.TryGetProperty("MonsterNumber", out var m))
+ {
+ monsterNumber = m.GetInt32();
+ }
+ if (root.TryGetProperty("EnergyPerStep", out var eps))
+ {
+ var val = eps.GetInt32();
+ if (val > 0)
+ {
+ energyPerStep = val;
+ }
+ }
+ if (root.TryGetProperty("PercentPerStep", out var pps))
+ {
+ var val = pps.GetSingle();
+ if (val > 0)
+ {
+ percentPerStep = val;
+ }
+ }
+ }
+ }
+ catch
+ {
+ // ignore and write defaults
+ }
+
+ var result = new
+ {
+ MonsterNumber = monsterNumber,
+ EnergyPerStep = energyPerStep,
+ PercentPerStep = percentPerStep,
+ };
+
+ return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
+ }
+}
+
diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs
index 386532a2d..50c567f22 100644
--- a/src/Persistence/Initialization/Updates/UpdateVersion.cs
+++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs
@@ -235,6 +235,11 @@ public enum UpdateVersion
///
FixChaosMixesSeason6 = 45,
+ ///
+ /// The version of the .
+ ///
+ ElfSummonDefaults = 46,
+
///
/// The version of the .
///
diff --git a/src/Startup/Logging/ConsoleLog.cs b/src/Startup/Logging/ConsoleLog.cs
new file mode 100644
index 000000000..36edc0560
--- /dev/null
+++ b/src/Startup/Logging/ConsoleLog.cs
@@ -0,0 +1,10 @@
+namespace MUnique.OpenMU.Startup.Logging;
+
+///
+/// Static access to the shared in-memory log buffer for simple registration.
+///
+internal static class ConsoleLog
+{
+ public static InMemoryLogBuffer Buffer { get; set; } = new();
+}
+
diff --git a/src/Startup/Logging/InMemoryLogBuffer.cs b/src/Startup/Logging/InMemoryLogBuffer.cs
new file mode 100644
index 000000000..ffd909760
--- /dev/null
+++ b/src/Startup/Logging/InMemoryLogBuffer.cs
@@ -0,0 +1,37 @@
+namespace MUnique.OpenMU.Startup.Logging;
+
+using System.Collections.Concurrent;
+
+///
+/// In-memory ring buffer for recent log lines.
+///
+internal sealed class InMemoryLogBuffer
+{
+ private readonly ConcurrentQueue _queue = new();
+ private readonly int _capacity;
+
+ public InMemoryLogBuffer(int capacity = 2000)
+ {
+ _capacity = Math.Max(100, capacity);
+ }
+
+ public void Add(string line)
+ {
+ _queue.Enqueue(line);
+ while (_queue.Count > _capacity && _queue.TryDequeue(out _))
+ {
+ // drop old
+ }
+ }
+
+ public IReadOnlyList Tail(int count = 200)
+ {
+ if (count <= 0)
+ {
+ return Array.Empty();
+ }
+
+ return _queue.Reverse().Take(count).Reverse().ToList();
+ }
+}
+
diff --git a/src/Startup/Logging/InMemorySerilogSink.cs b/src/Startup/Logging/InMemorySerilogSink.cs
new file mode 100644
index 000000000..939dbcb8b
--- /dev/null
+++ b/src/Startup/Logging/InMemorySerilogSink.cs
@@ -0,0 +1,40 @@
+namespace MUnique.OpenMU.Startup.Logging;
+
+using Serilog.Core;
+using Serilog.Events;
+using System.Globalization;
+
+///
+/// Serilog sink which writes formatted log events into an .
+///
+internal sealed class InMemorySerilogSink : ILogEventSink
+{
+ private readonly InMemoryLogBuffer _buffer;
+
+ public InMemorySerilogSink(InMemoryLogBuffer buffer)
+ {
+ _buffer = buffer;
+ }
+
+ public void Emit(LogEvent logEvent)
+ {
+ try
+ {
+ var timestamp = logEvent.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
+ var level = logEvent.Level.ToString().ToUpperInvariant().PadRight(7);
+ var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
+ var line = $"{timestamp} [{level}] {message}";
+ if (logEvent.Exception is { } ex)
+ {
+ line += $" | {ex.GetType().Name}: {ex.Message}";
+ }
+
+ _buffer.Add(line);
+ }
+ catch
+ {
+ // ignore
+ }
+ }
+}
+
diff --git a/src/Startup/Program.cs b/src/Startup/Program.cs
index 0eadcd0b4..2f056b62d 100644
--- a/src/Startup/Program.cs
+++ b/src/Startup/Program.cs
@@ -37,6 +37,7 @@ namespace MUnique.OpenMU.Startup;
using Nito.AsyncEx.Synchronous;
using Serilog;
using Serilog.Debugging;
+using MUnique.OpenMU.Startup.Logging;
///
/// The startup class for an all-in-one game server.
@@ -65,8 +66,13 @@ public Program()
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true, true)
.Build();
+ var buffer = new InMemoryLogBuffer();
+ // Store buffer in a static service container by registering into DI later
+ ConsoleLog.Buffer = buffer;
+
this._logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
+ .WriteTo.Sink(new InMemorySerilogSink(buffer))
.CreateLogger();
}
@@ -242,7 +248,9 @@ private async Task CreateHostAsync(string[] args)
builder.AddAdminPanel(includeMapApp: true);
}
- builder.Services.AddSingleton(this._servers)
+ builder.Services
+ .AddSingleton(ConsoleLog.Buffer)
+ .AddSingleton(this._servers)
.AddSingleton()
.AddSingleton()
.AddSingleton()
@@ -297,6 +305,9 @@ private async Task CreateHostAsync(string[] args)
var host = builder.Build();
+ // Expose log tail endpoint (minimal api)
+ host.MapGet("/api/logs/tail", (InMemoryLogBuffer buf, int? take) => buf.Tail(take ?? 200));
+
// NpgsqlLoggingConfiguration.InitializeLogging(host.Services.GetRequiredService())
this._logger.Information("Host created");
@@ -528,4 +539,4 @@ private async Task InitializeDataAsync(string version, ILoggerFactory loggerFact
var initialization = plugInManager.GetStrategy(version) ?? throw new Exception("Data initialization plugin not found");
await initialization.CreateInitialDataAsync(3, true).ConfigureAwait(false);
}
-}
\ No newline at end of file
+}
From 5bae1cc81956e5722848ac7647e04098557cbe9c Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 03:22:33 -0300
Subject: [PATCH 031/190] fix default settings
---
src/GameLogic/PlugIns/ElfSummonsAll.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index 5be18e1d0..941d74529 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -170,8 +170,8 @@ public class ElfSummonSkillConfiguration
[System.ComponentModel.DataAnnotations.Range(0, 65535)]
public int MonsterNumber { get; set; } = 0;
// Dynamic scaling by Energy: scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
- public int EnergyPerStep { get; set; } = 0; // 0 = disabled
- public float PercentPerStep { get; set; } = 0.0f; // e.g. 0.05 for +5% per 1000 energy
+ public int EnergyPerStep { get; set; } = 1000; // default enabled: per 1000 energy
+ public float PercentPerStep { get; set; } = 0.05f; // default: +5% per step
}
@@ -215,8 +215,8 @@ public ElfSummonSkillConfiguration? Configuration
entry.MonsterNumber = clamped;
}
- entry.EnergyPerStep = value.EnergyPerStep;
- entry.PercentPerStep = value.PercentPerStep;
+ entry.EnergyPerStep = value.EnergyPerStep > 0 ? value.EnergyPerStep : 1000;
+ entry.PercentPerStep = value.PercentPerStep > 0 ? value.PercentPerStep : 0.05f;
}
}
From 9eb480c0905049ed2bdb57dba5decec5769f00c1 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 11:24:48 -0300
Subject: [PATCH 032/190] fix update
---
.../Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs | 3 +--
src/Persistence/Initialization/Updates/UpdateVersion.cs | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs
index 54b67d201..fc7742468 100644
--- a/src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs
+++ b/src/Persistence/Initialization/Updates/ElfSummonDefaultsUpdatePlugIn.cs
@@ -27,7 +27,7 @@ public class ElfSummonDefaultsUpdatePlugIn : UpdatePlugInBase
new("A6E7C6A1-5D9A-4D7D-A001-000000000036"),
];
- public override UpdateVersion Version => (UpdateVersion)46;
+ public override UpdateVersion Version => UpdateVersion.ElfSummonDefaults;
public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id;
@@ -98,4 +98,3 @@ private static string UpdateJson(string? json)
return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
}
}
-
diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs
index 50c567f22..c5a8f8e42 100644
--- a/src/Persistence/Initialization/Updates/UpdateVersion.cs
+++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs
@@ -238,7 +238,7 @@ public enum UpdateVersion
///
/// The version of the .
///
- ElfSummonDefaults = 46,
+ ElfSummonDefaults = 250,
///
/// The version of the .
From 0ab7e284e601fa66b897a7bdc699f3a695c73aa7 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 15:21:10 -0300
Subject: [PATCH 033/190] delete bugs
---
.../Skills/TargetedSkillDefaultPlugin.cs | 107 ++++++++++++++++++
src/GameLogic/PlugIns/ElfSummonsAll.cs | 75 +-----------
2 files changed, 109 insertions(+), 73 deletions(-)
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index db4e865fb..1cbe4b004 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -5,6 +5,7 @@
namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
using System.Runtime.InteropServices;
+using System.Reflection;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns;
@@ -139,6 +140,9 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, defaultDefinition)
?? defaultDefinition;
+ // Apply energy scaling as a fallback (and in addition), so it works even if the plugin is not activated
+ monsterDefinition = this.CloneAndScaleSummonDefinition(player, monsterDefinition, skill.Number);
+
if (monsterDefinition is not null)
{
await player.CreateSummonedMonsterAsync(monsterDefinition).ConfigureAwait(false);
@@ -151,6 +155,109 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
await player.ForEachWorldObserverAsync(p => p.ShowSkillAnimationAsync(player, target, skill, effectApplied), true).ConfigureAwait(false);
}
+ private MonsterDefinition? CloneAndScaleSummonDefinition(Player player, MonsterDefinition? baseDefinition, short skillNumber)
+ {
+ if (baseDefinition is null)
+ {
+ return null;
+ }
+
+ var clone = baseDefinition.Clone(player.GameContext.Configuration);
+
+ // Read energy scaling settings from plugin configuration, even if plugin is inactive.
+ // Defaults if not configured.
+ int energyPerStep = 1000;
+ float percentPerStep = 0.05f;
+
+ try
+ {
+ var type = AppDomain.CurrentDomain
+ .GetAssemblies()
+ .SelectMany(a => a.DefinedTypes)
+ .FirstOrDefault(t => typeof(ISummonConfigurationPlugIn).IsAssignableFrom(t)
+ && !t.IsAbstract && !t.IsInterface
+ && this.TryGetSummonKey(t) == skillNumber);
+
+ var typeId = type?.GUID;
+ var plugInConfig = typeId is null ? null : player.GameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId.Value);
+ if (plugInConfig is not null)
+ {
+ var parsed = plugInConfig.GetConfiguration(player.GameContext.PlugInManager.CustomConfigReferenceHandler);
+ if (parsed is not null)
+ {
+ if (parsed.EnergyPerStep > 0)
+ {
+ energyPerStep = parsed.EnergyPerStep;
+ }
+
+ if (parsed.PercentPerStep > 0)
+ {
+ percentPerStep = parsed.PercentPerStep;
+ }
+ }
+ }
+ }
+ catch
+ {
+ // ignore and use defaults
+ }
+
+ var energy = player.Attributes?[Stats.TotalEnergy] ?? 0;
+ var steps = energyPerStep > 0 ? (int)(energy / energyPerStep) : 0;
+ var energyScale = 1.0f + Math.Max(0, steps) * Math.Max(0, percentPerStep);
+
+ if (Math.Abs(energyScale - 1.0f) < float.Epsilon)
+ {
+ return clone; // nothing to scale
+ }
+
+ void Scale(MUnique.OpenMU.AttributeSystem.AttributeDefinition stat)
+ {
+ var attr = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == stat);
+ if (attr is not null)
+ {
+ attr.Value *= energyScale;
+ }
+ }
+
+ // HP
+ Scale(Stats.MaximumHealth);
+ // Defense base
+ Scale(Stats.DefenseBase);
+ // Damage bases
+ Scale(Stats.MinimumPhysBaseDmg);
+ Scale(Stats.MaximumPhysBaseDmg);
+ Scale(Stats.MinimumWizBaseDmg);
+ Scale(Stats.MaximumWizBaseDmg);
+ Scale(Stats.MinimumCurseBaseDmg);
+ Scale(Stats.MaximumCurseBaseDmg);
+ // Rates to reduce MISS and improve survivability
+ Scale(Stats.AttackRatePvm);
+ Scale(Stats.DefenseRatePvm);
+ Scale(Stats.DefenseRatePvp);
+
+ return clone;
+ }
+
+ private short TryGetSummonKey(Type pluginType)
+ {
+ try
+ {
+ var instance = Activator.CreateInstance(pluginType);
+ var prop = pluginType.GetProperty("Key", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ if (prop?.GetValue(instance) is short k)
+ {
+ return k;
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+
+ return short.MinValue;
+ }
+
///
/// Determines the targets of the skill. It can be overridden by derived classes to provide custom target selection logic.
///
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index 941d74529..b1c5afd86 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -69,80 +69,9 @@ private ElfSummonsConfigCore() { }
return null;
}
+ // Only pick/clone the base monster definition here. Energy scaling is centralized
+ // in TargetedSkillDefaultPlugin so it applies even when this plug-in is inactive.
var clone = baseDef.Clone(player.GameContext.Configuration);
-
- // Dynamic scaling by summoners TotalEnergy only:
- // scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep
- var energy = player.Attributes?[Stats.TotalEnergy] ?? 0;
- var steps = cfg.EnergyPerStep > 0 ? (int)(energy / cfg.EnergyPerStep) : 0;
- var energyScale = 1.0f + Math.Max(0, steps) * Math.Max(0, cfg.PercentPerStep);
-
- // Apply scaling to base stats of the chosen monster
- var hp = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumHealth);
- if (hp is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- hp.Value *= energyScale;
- }
-
- // Physical base damage
- var minDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumPhysBaseDmg);
- if (minDmg is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- minDmg.Value *= energyScale;
- }
- var maxDmg = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumPhysBaseDmg);
- if (maxDmg is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- maxDmg.Value *= energyScale;
- }
-
- // Wizardry base damage (some monsters use wizardry damage)
- var minWiz = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumWizBaseDmg);
- if (minWiz is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- minWiz.Value *= energyScale;
- }
- var maxWiz = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumWizBaseDmg);
- if (maxWiz is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- maxWiz.Value *= energyScale;
- }
-
- // Curse base damage (rare cases)
- var minCurse = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MinimumCurseBaseDmg);
- if (minCurse is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- minCurse.Value *= energyScale;
- }
- var maxCurse = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.MaximumCurseBaseDmg);
- if (maxCurse is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- maxCurse.Value *= energyScale;
- }
-
- var def = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseBase);
- if (def is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- def.Value *= energyScale;
- }
-
- // Attack/Defense rate scaling for better hit chance and survivability
- var atkRate = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.AttackRatePvm);
- if (atkRate is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- atkRate.Value *= energyScale;
- }
- var defRatePvm = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseRatePvm);
- if (defRatePvm is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- defRatePvm.Value *= energyScale;
- }
- var defRatePvp = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.DefenseRatePvp);
- if (defRatePvp is not null && Math.Abs(energyScale - 1.0f) > float.Epsilon)
- {
- defRatePvp.Value *= energyScale;
- }
-
cfg.Customize?.Invoke(clone);
return clone;
}
From 067f1b2c57c083125d7fb8a4c4fbeb388e1f4cf4 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 16:34:38 -0300
Subject: [PATCH 034/190] fix scaling
---
src/GameLogic/Player.cs | 17 +++++++++++++++--
.../Skills/TargetedSkillDefaultPlugin.cs | 4 ++--
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index 7616a9b14..c336d9236 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -1008,6 +1008,15 @@ public async ValueTask RespawnAtAsync(ExitGate gate)
await this.PlayerState.TryAdvanceToAsync(GameLogic.PlayerState.EnteredWorld).ConfigureAwait(false);
this.IsAlive = true;
await this.CurrentMap!.AddAsync(this).ConfigureAwait(false);
+
+ // Ensure summon is re-created on the current map as well.
+ if (this.Summon?.Item1 is { IsAlive: true } summon)
+ {
+ var definition = summon.Definition;
+ await summon.DisposeAsync().ConfigureAwait(false);
+ this.Summon = null;
+ await this.CreateSummonedMonsterAsync(definition).ConfigureAwait(false);
+ }
}
else
{
@@ -1053,8 +1062,12 @@ public async ValueTask ClientReadyAfterMapChangeAsync()
if (this.Summon?.Item1 is { IsAlive: true } summon)
{
- await this.CurrentMap.AddAsync(summon).ConfigureAwait(false);
- summon.OnSpawn();
+ // Recreate the summon on the new map to keep internal map references consistent.
+ // The existing summon instance still references the old map in its immutable CurrentMap property.
+ var definition = summon.Definition;
+ await summon.DisposeAsync().ConfigureAwait(false);
+ this.Summon = null;
+ await this.CreateSummonedMonsterAsync(definition).ConfigureAwait(false);
}
}
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 1cbe4b004..c7ac4cc4a 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -203,8 +203,8 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
}
var energy = player.Attributes?[Stats.TotalEnergy] ?? 0;
- var steps = energyPerStep > 0 ? (int)(energy / energyPerStep) : 0;
- var energyScale = 1.0f + Math.Max(0, steps) * Math.Max(0, percentPerStep);
+ var ratio = energyPerStep > 0 ? (float)(energy / (float)energyPerStep) : 0f;
+ var energyScale = 1.0f + Math.Max(0f, ratio) * Math.Max(0, percentPerStep);
if (Math.Abs(energyScale - 1.0f) < float.Epsilon)
{
From b75a245da90d7e268836fbf4ef40ae84cccc3a91 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 17:49:27 -0300
Subject: [PATCH 035/190] fix player logic
---
src/GameLogic/Player.cs | 35 ++++++++++++++++---
.../Skills/TargetedSkillDefaultPlugin.cs | 35 ++++++++++++++++---
2 files changed, 62 insertions(+), 8 deletions(-)
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index c336d9236..5077ee8ea 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -1585,17 +1585,44 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
throw new InvalidOperationException("Can't add a summon for a player which isn't spawned yet.");
}
+ // Find a valid spawn point close to the player (walkable and outside safezone).
+ Point spawnPoint = this.Position;
+ var terrain = gameMap.Terrain;
+ bool found = false;
+ for (var radius = 1; radius <= 5 && !found; radius++)
+ {
+ for (var attempts = 0; attempts < 12 && !found; attempts++)
+ {
+ var p = terrain.GetRandomCoordinate(this.Position, (byte)radius);
+ if (terrain.WalkMap[p.X, p.Y] && !terrain.SafezoneMap[p.X, p.Y])
+ {
+ spawnPoint = p;
+ found = true;
+ }
+ }
+ }
+
var area = new MonsterSpawnArea
{
GameMap = gameMap.Definition,
MonsterDefinition = definition,
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
Quantity = 1,
- X1 = (byte)Math.Max(this.Position.X - 3, byte.MinValue),
- X2 = (byte)Math.Min(this.Position.X + 3, byte.MaxValue),
- Y1 = (byte)Math.Max(this.Position.Y - 3, byte.MinValue),
- Y2 = (byte)Math.Min(this.Position.Y + 3, byte.MaxValue),
};
+
+ if (found)
+ {
+ area.X1 = area.X2 = spawnPoint.X;
+ area.Y1 = area.Y2 = spawnPoint.Y;
+ }
+ else
+ {
+ // Fallback: small area around player; may fail in safezone but it's the best effort.
+ area.X1 = (byte)Math.Max(this.Position.X - 3, byte.MinValue);
+ area.X2 = (byte)Math.Min(this.Position.X + 3, byte.MaxValue);
+ area.Y1 = (byte)Math.Max(this.Position.Y - 3, byte.MinValue);
+ area.Y2 = (byte)Math.Min(this.Position.Y + 3, byte.MaxValue);
+ }
var intelligence = new SummonedMonsterIntelligence(this);
var monster = new Monster(area, definition, gameMap, NullDropGenerator.Instance, intelligence, this.GameContext.PlugInManager, this.GameContext.PathFinderPool);
area.MaximumHealthOverride = (int)monster.Attributes[Stats.MaximumHealth];
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index c7ac4cc4a..adf6a96d1 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -126,19 +126,46 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var effectApplied = false;
if (skill.SkillType == SkillType.SummonMonster)
{
- MonsterDefinition? defaultDefinition = null;
+ MonsterDefinition? baseDefinition = null;
if (SummonSkillToMonsterMapping.TryGetValue(skill.Number, out var monsterNumber)
&& player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } mappedDefinition)
{
- defaultDefinition = mappedDefinition;
+ baseDefinition = mappedDefinition;
+ }
+
+ // Allow MonsterNumber override from plug-in configuration even if the plug-in is not active.
+ try
+ {
+ var type = AppDomain.CurrentDomain
+ .GetAssemblies()
+ .SelectMany(a => a.DefinedTypes)
+ .FirstOrDefault(t => typeof(ISummonConfigurationPlugIn).IsAssignableFrom(t)
+ && !t.IsAbstract && !t.IsInterface
+ && this.TryGetSummonKey(t) == skill.Number);
+
+ var typeId = type?.GUID;
+ var plugInConfig = typeId is null ? null : player.GameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId.Value);
+ var parsed = plugInConfig?.GetConfiguration(player.GameContext.PlugInManager.CustomConfigReferenceHandler);
+ if (parsed is { MonsterNumber: > 0 })
+ {
+ var customDef = player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == (short)parsed.MonsterNumber);
+ if (customDef is { })
+ {
+ baseDefinition = customDef;
+ }
+ }
+ }
+ catch
+ {
+ // ignore - fall back to default mapping
}
// ✅ pedir el plugin “keyed” por skill.Number
var summonPlugin = player.GameContext.PlugInManager
.GetStrategy(skill.Number);
- var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, defaultDefinition)
- ?? defaultDefinition;
+ var monsterDefinition = summonPlugin?.CreateSummonMonsterDefinition(player, skill, baseDefinition)
+ ?? baseDefinition;
// Apply energy scaling as a fallback (and in addition), so it works even if the plugin is not activated
monsterDefinition = this.CloneAndScaleSummonDefinition(player, monsterDefinition, skill.Number);
From 8c4d0b17ba885e4c5e4fcbfdfbc5078ed01cf3a9 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 18:09:23 -0300
Subject: [PATCH 036/190] cache cleaning
---
src/Directory.Build.props | 14 ++++++++---
src/GameLogic/PlugIns/ElfSummonsAll.cs | 24 ++-----------------
....OpenMU.Persistence.SourceGenerator.csproj | 2 +-
src/PlugIns/MUnique.OpenMU.PlugIns.csproj | 2 +-
.../MUnique.OpenMU.SourceGenerators.csproj | 2 +-
src/Startup/Program.cs | 22 +++++++++++++++++
6 files changed, 38 insertions(+), 28 deletions(-)
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 8ce803c30..139d0b502 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -12,9 +12,17 @@
runtime; build; native; contentfiles; analyzers
-
+
-
+
+
+ false
+ false
+
+ $(NoWarn);CS1591;CS0162;CS0168;CS1711;CS1723;CS1696;CS0067
+
+
+
@@ -36,4 +44,4 @@
-
\ No newline at end of file
+
diff --git a/src/GameLogic/PlugIns/ElfSummonsAll.cs b/src/GameLogic/PlugIns/ElfSummonsAll.cs
index b1c5afd86..f24b1f09b 100644
--- a/src/GameLogic/PlugIns/ElfSummonsAll.cs
+++ b/src/GameLogic/PlugIns/ElfSummonsAll.cs
@@ -162,28 +162,8 @@ public object CreateDefaultConfig()
public MonsterDefinition? CreateSummonMonsterDefinition(Player player, Skill skill, MonsterDefinition? defaultDefinition)
{
- // Best-effort: Pull latest configuration directly from persistence, bypassing caches,
- // so cross-process/container changes apply without restart.
- try
- {
- var typeId = this.GetType().GUID;
- using var cfgCtx = player.GameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), useCache: false);
- var all = cfgCtx.GetAsync().AsTask().GetAwaiter().GetResult();
- var plugInConfig = all.FirstOrDefault(c => c.TypeId == typeId);
- if (plugInConfig is not null)
- {
- var latest = plugInConfig.GetConfiguration(player.GameContext.PlugInManager.CustomConfigReferenceHandler);
- if (latest is not null)
- {
- this.Configuration = latest; // updates core map
- }
- }
- }
- catch
- {
- // ignore and continue with current in-memory config
- }
-
+ // Rely on PlugInManager.PlugInConfigurationChanged to keep Configuration updated.
+ // Just resolve using the current in-memory configuration.
return ElfSummonsConfigCore.Instance.Resolve(player, skill, defaultDefinition);
}
}
diff --git a/src/Persistence/SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj b/src/Persistence/SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj
index 58b61435e..fba24c4e3 100644
--- a/src/Persistence/SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj
+++ b/src/Persistence/SourceGenerator/MUnique.OpenMU.Persistence.SourceGenerator.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/PlugIns/MUnique.OpenMU.PlugIns.csproj b/src/PlugIns/MUnique.OpenMU.PlugIns.csproj
index e9c4ea9e0..b3b24ed80 100644
--- a/src/PlugIns/MUnique.OpenMU.PlugIns.csproj
+++ b/src/PlugIns/MUnique.OpenMU.PlugIns.csproj
@@ -31,7 +31,7 @@
-
+
diff --git a/src/SourceGenerators/MUnique.OpenMU.SourceGenerators.csproj b/src/SourceGenerators/MUnique.OpenMU.SourceGenerators.csproj
index 24ec1cdc6..d3cf40061 100644
--- a/src/SourceGenerators/MUnique.OpenMU.SourceGenerators.csproj
+++ b/src/SourceGenerators/MUnique.OpenMU.SourceGenerators.csproj
@@ -16,7 +16,7 @@
allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ all
diff --git a/src/Startup/Program.cs b/src/Startup/Program.cs
index 2f056b62d..518fb8f78 100644
--- a/src/Startup/Program.cs
+++ b/src/Startup/Program.cs
@@ -355,6 +355,28 @@ private ICollection PlugInConfigurationsFactory(IServicePro
var typesWithCustomConfig = pluginManager.KnownPlugInTypes.Where(t => t.GetInterfaces().Contains(typeof(ISupportDefaultCustomConfiguration))).ToDictionary(t => t.GUID, t => t);
using var notificationSuspension = context.SuspendChangeNotifications();
+
+ // 1) Remove configurations with unknown plugin type ids (e.g., leftovers from previous builds)
+ var knownTypeIds = new HashSet(pluginManager.KnownPlugInTypes.Select(t => t.GUID));
+ var unknownConfigs = configs.Where(c => !knownTypeIds.Contains(c.TypeId)).ToList();
+ if (unknownConfigs.Count > 0)
+ {
+ foreach (var uc in unknownConfigs)
+ {
+ try
+ {
+ _ = context.DeleteAsync(uc).AsTask().WaitAndUnwrapException();
+ }
+ catch (Exception ex)
+ {
+ this._logger.Warning(ex, "Failed to delete unknown plug-in configuration with TypeId {typeId}", uc.TypeId);
+ }
+ }
+
+ // Update local list to reflect deletions
+ configs = configs.Except(unknownConfigs).ToList();
+ _ = context.SaveChangesAsync().AsTask().WaitAndUnwrapException();
+ }
var typesWithMissingCustomConfigs = configs.Where(c => string.IsNullOrWhiteSpace(c.CustomConfiguration) && typesWithCustomConfig.ContainsKey(c.TypeId)).ToList();
if (typesWithMissingCustomConfigs.Any())
{
From 70eef192d089f5853950159f61fc62fc5c7735e4 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 18:36:04 -0300
Subject: [PATCH 037/190] refactor summon
Summon Skill refactor
---
src/GameLogic/AttackableExtensions.cs | 7 ++++++-
src/GameLogic/NPC/AttackableNpcBase.cs | 8 +++++++-
src/GameLogic/Player.cs | 16 ++++++++++++----
.../Skills/TargetedSkillDefaultPlugin.cs | 4 ++--
4 files changed, 27 insertions(+), 8 deletions(-)
diff --git a/src/GameLogic/AttackableExtensions.cs b/src/GameLogic/AttackableExtensions.cs
index de8483880..494e4372b 100644
--- a/src/GameLogic/AttackableExtensions.cs
+++ b/src/GameLogic/AttackableExtensions.cs
@@ -522,6 +522,11 @@ public static int GetRequiredValue(this IAttacker attacker, AttributeRequirement
/// The calculated base experience.
public static double CalculateBaseExperience(this IAttackable killedObject, float killerLevel)
{
+ // Summoned monsters should not yield experience.
+ if (killedObject is Monster { SummonedBy: { } })
+ {
+ return 0;
+ }
var targetLevel = killedObject.Attributes[Stats.Level];
var tempExperience = (targetLevel + 25) * targetLevel / 3.0;
@@ -860,4 +865,4 @@ private static int GetMasterSkillTreeMasteryPvpDamageBonus(IAttacker attacker)
return 0;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/GameLogic/NPC/AttackableNpcBase.cs b/src/GameLogic/NPC/AttackableNpcBase.cs
index 04b0462fc..5effb14a5 100644
--- a/src/GameLogic/NPC/AttackableNpcBase.cs
+++ b/src/GameLogic/NPC/AttackableNpcBase.cs
@@ -241,6 +241,12 @@ protected virtual async ValueTask OnDeathAsync(IAttacker attacker)
await this.ForEachWorldObserverAsync(p => p.ObjectGotKilledAsync(this, attacker), true).ConfigureAwait(false);
+ // Do not award experience or drop items for summoned monsters.
+ if (this is Monster { SummonedBy: { } })
+ {
+ return;
+ }
+
var player = this.GetHitNotificationTarget(attacker);
if (player is { })
{
@@ -382,4 +388,4 @@ private async ValueTask DropItemDelayedAsync(Player player, int gainedExp)
player.Logger.LogDebug(ex, "Dropping an item failed after killing '{this}': {ex}", this, ex);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index 5077ee8ea..e3f75dfef 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -1008,15 +1008,15 @@ public async ValueTask RespawnAtAsync(ExitGate gate)
await this.PlayerState.TryAdvanceToAsync(GameLogic.PlayerState.EnteredWorld).ConfigureAwait(false);
this.IsAlive = true;
await this.CurrentMap!.AddAsync(this).ConfigureAwait(false);
-
- // Ensure summon is re-created on the current map as well.
- if (this.Summon?.Item1 is { IsAlive: true } summon)
+ if (!this.CurrentMap.Terrain.SafezoneMap[this.SelectedCharacter.PositionX, this.SelectedCharacter.PositionY]
+ && this.Summon?.Item1 is { IsAlive: true } summon)
{
var definition = summon.Definition;
await summon.DisposeAsync().ConfigureAwait(false);
this.Summon = null;
await this.CreateSummonedMonsterAsync(definition).ConfigureAwait(false);
}
+
}
else
{
@@ -1060,7 +1060,9 @@ public async ValueTask ClientReadyAfterMapChangeAsync()
await this.WarpToSafezoneAsync().ConfigureAwait(false);
}
- if (this.Summon?.Item1 is { IsAlive: true } summon)
+ // Recreate summon only if not in safezone; otherwise skip until player leaves safezone.
+ if (!this.CurrentMap.Terrain.SafezoneMap[this.SelectedCharacter.PositionX, this.SelectedCharacter.PositionY]
+ && this.Summon?.Item1 is { IsAlive: true } summon)
{
// Recreate the summon on the new map to keep internal map references consistent.
// The existing summon instance still references the old map in its immutable CurrentMap property.
@@ -1602,6 +1604,12 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
}
}
+ if (!found && terrain.SafezoneMap[this.Position.X, this.Position.Y])
+ {
+ // Can't summon in safezone; silently abort.
+ return;
+ }
+
var area = new MonsterSpawnArea
{
GameMap = gameMap.Definition,
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index adf6a96d1..ca0b00424 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -230,8 +230,8 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
}
var energy = player.Attributes?[Stats.TotalEnergy] ?? 0;
- var ratio = energyPerStep > 0 ? (float)(energy / (float)energyPerStep) : 0f;
- var energyScale = 1.0f + Math.Max(0f, ratio) * Math.Max(0, percentPerStep);
+ var steps = energyPerStep > 0 ? (int)(energy / energyPerStep) : 0;
+ var energyScale = 1.0f + Math.Max(0, steps) * Math.Max(0, percentPerStep);
if (Math.Abs(energyScale - 1.0f) < float.Epsilon)
{
From b73b9dbc008c5b3ccdfa7e38f85f1e861f14d0a6 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 18:48:08 -0300
Subject: [PATCH 038/190] docker fix
Redirect docker to fork
---
deploy/all-in-one/docker-compose.from-fork.yml | 13 +++++++++++++
deploy/all-in-one/docker-compose.override.yml | 2 ++
2 files changed, 15 insertions(+)
create mode 100644 deploy/all-in-one/docker-compose.from-fork.yml
diff --git a/deploy/all-in-one/docker-compose.from-fork.yml b/deploy/all-in-one/docker-compose.from-fork.yml
new file mode 100644
index 000000000..1b56f4e97
--- /dev/null
+++ b/deploy/all-in-one/docker-compose.from-fork.yml
@@ -0,0 +1,13 @@
+services:
+
+ openmu-startup:
+ # Always build the image from your fork (remote git context)
+ # You can override the context with an env var OPENMU_FORK_CONTEXT
+ # Example value: https://github.com/EmanuelCatania/OpenMU-S2.git#master:src
+ image: openmu-fork:dev
+ build:
+ context: ${OPENMU_FORK_CONTEXT:-https://github.com/EmanuelCatania/OpenMU-S2.git#master:src}
+ dockerfile: Startup/Dockerfile
+ pull_policy: never
+ restart: "no"
+
diff --git a/deploy/all-in-one/docker-compose.override.yml b/deploy/all-in-one/docker-compose.override.yml
index 2c01bbdbb..1e8bd78cd 100644
--- a/deploy/all-in-one/docker-compose.override.yml
+++ b/deploy/all-in-one/docker-compose.override.yml
@@ -1,9 +1,11 @@
services:
openmu-startup:
+ image: openmu-local:dev
build:
context: ../../src
dockerfile: Startup/Dockerfile
+ pull_policy: never
restart: "no"
environment:
# Para pruebas en red local, anunciar IP local a los clientes.
From e9561f4fb060059951bc7be24530de2e843a5c1e Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 19:15:38 -0300
Subject: [PATCH 039/190] add logs summons
---
.../all-in-one/docker-compose.from-fork.yml | 4 ++-
src/GameLogic/Player.cs | 3 ++
.../Skills/TargetedSkillDefaultPlugin.cs | 32 +++++++++++++++++++
3 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/deploy/all-in-one/docker-compose.from-fork.yml b/deploy/all-in-one/docker-compose.from-fork.yml
index 1b56f4e97..2d62f7752 100644
--- a/deploy/all-in-one/docker-compose.from-fork.yml
+++ b/deploy/all-in-one/docker-compose.from-fork.yml
@@ -10,4 +10,6 @@ services:
dockerfile: Startup/Dockerfile
pull_policy: never
restart: "no"
-
+ environment:
+ # Enable detailed summon diagnostics in server logs
+ SUMMON_DIAG: "true"
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index e3f75dfef..7fa0e0284 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -1607,6 +1607,7 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
if (!found && terrain.SafezoneMap[this.Position.X, this.Position.Y])
{
// Can't summon in safezone; silently abort.
+ this.Logger.LogInformation("[SUMMON] Abort: Player in safezone, no valid spawn tile for summon.");
return;
}
@@ -1622,6 +1623,7 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
{
area.X1 = area.X2 = spawnPoint.X;
area.Y1 = area.Y2 = spawnPoint.Y;
+ this.Logger.LogInformation($"[SUMMON] Spawning at {spawnPoint.X},{spawnPoint.Y} on map {gameMap.Definition.Number}");
}
else
{
@@ -1630,6 +1632,7 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
area.X2 = (byte)Math.Min(this.Position.X + 3, byte.MaxValue);
area.Y1 = (byte)Math.Max(this.Position.Y - 3, byte.MinValue);
area.Y2 = (byte)Math.Min(this.Position.Y + 3, byte.MaxValue);
+ this.Logger.LogInformation($"[SUMMON] Using fallback area around player: X[{area.X1}-{area.X2}] Y[{area.Y1}-{area.Y2}] on map {gameMap.Definition.Number}");
}
var intelligence = new SummonedMonsterIntelligence(this);
var monster = new Monster(area, definition, gameMap, NullDropGenerator.Instance, intelligence, this.GameContext.PlugInManager, this.GameContext.PathFinderPool);
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index ca0b00424..f149677b5 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -20,6 +20,10 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
[Guid("eb2949fb-5ed2-407e-a4e8-e3015ed5692b")]
public class TargetedSkillDefaultPlugin : TargetedSkillPluginBase
{
+ private static readonly bool SummonDiagEnabled =
+ string.Equals(Environment.GetEnvironmentVariable("SUMMON_DIAG"), "1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(Environment.GetEnvironmentVariable("SUMMON_DIAG"), "true", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(Environment.GetEnvironmentVariable("SUMMON_DIAG"), "yes", StringComparison.OrdinalIgnoreCase);
private static readonly Dictionary SummonSkillToMonsterMapping = new()
{
{ 30, 26 }, // Goblin
@@ -152,6 +156,10 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
if (customDef is { })
{
baseDefinition = customDef;
+ if (SummonDiagEnabled)
+ {
+ player.Logger.LogInformation($"[SUMMON] Override MonsterNumber by config for skill {skill.Number}: {customDef.Designation} ({customDef.Number})");
+ }
}
}
}
@@ -172,6 +180,10 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
if (monsterDefinition is not null)
{
+ if (SummonDiagEnabled)
+ {
+ player.Logger.LogInformation($"[SUMMON] Creating summon for skill {skill.Number}: {monsterDefinition.Designation} ({monsterDefinition.Number})");
+ }
await player.CreateSummonedMonsterAsync(monsterDefinition).ConfigureAwait(false);
}
}
@@ -232,12 +244,22 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var energy = player.Attributes?[Stats.TotalEnergy] ?? 0;
var steps = energyPerStep > 0 ? (int)(energy / energyPerStep) : 0;
var energyScale = 1.0f + Math.Max(0, steps) * Math.Max(0, percentPerStep);
+ if (SummonDiagEnabled)
+ {
+ player.Logger.LogInformation($"[SUMMON] Energy={energy}, steps={steps}, energyPerStep={energyPerStep}, percentPerStep={percentPerStep}, scale={energyScale:0.###}");
+ }
if (Math.Abs(energyScale - 1.0f) < float.Epsilon)
{
return clone; // nothing to scale
}
+ float GetValue(MUnique.OpenMU.AttributeSystem.AttributeDefinition stat)
+ {
+ var attr = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == stat);
+ return attr?.Value ?? 0;
+ }
+
void Scale(MUnique.OpenMU.AttributeSystem.AttributeDefinition stat)
{
var attr = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == stat);
@@ -263,6 +285,16 @@ void Scale(MUnique.OpenMU.AttributeSystem.AttributeDefinition stat)
Scale(Stats.DefenseRatePvm);
Scale(Stats.DefenseRatePvp);
+ if (SummonDiagEnabled)
+ {
+ player.Logger.LogInformation(
+ $"[SUMMON] Stats after scale: HP={GetValue(Stats.MaximumHealth):0}, DefBase={GetValue(Stats.DefenseBase):0}, " +
+ $"PhysMin/Max={GetValue(Stats.MinimumPhysBaseDmg):0}/{GetValue(Stats.MaximumPhysBaseDmg):0}, " +
+ $"WizMin/Max={GetValue(Stats.MinimumWizBaseDmg):0}/{GetValue(Stats.MaximumWizBaseDmg):0}, " +
+ $"CurseMin/Max={GetValue(Stats.MinimumCurseBaseDmg):0}/{GetValue(Stats.MaximumCurseBaseDmg):0}, " +
+ $"AtkRatePvM={GetValue(Stats.AttackRatePvm):0}, DefRatePvM={GetValue(Stats.DefenseRatePvm):0}, DefRatePvP={GetValue(Stats.DefenseRatePvp):0}");
+ }
+
return clone;
}
From 94646e669db8d97bf314e03a3153a9523486e364 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 20:36:32 -0300
Subject: [PATCH 040/190] fix skills
---
.../Skills/TargetedSkillDefaultPlugin.cs | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index f149677b5..ffa169e50 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -247,6 +247,15 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
if (SummonDiagEnabled)
{
player.Logger.LogInformation($"[SUMMON] Energy={energy}, steps={steps}, energyPerStep={energyPerStep}, percentPerStep={percentPerStep}, scale={energyScale:0.###}");
+ try
+ {
+ var count = clone.Attributes?.Count ?? 0;
+ var present = clone.Attributes?.Select(a => a.AttributeDefinition?.Designation ?? a.AttributeDefinition?.Id.ToString() ?? "")
+ .Take(10).ToArray() ?? Array.Empty();
+ player.Logger.LogInformation($"[SUMMON] AttrCount={count}, sample=[{string.Join(", ", present)}]");
+ player.Logger.LogInformation($"[SUMMON] StatIds: MaxHP={Stats.MaximumHealth.Id}, DefBase={Stats.DefenseBase.Id}, PhysMin={Stats.MinimumPhysBaseDmg.Id}, PhysMax={Stats.MaximumPhysBaseDmg.Id}");
+ }
+ catch { }
}
if (Math.Abs(energyScale - 1.0f) < float.Epsilon)
@@ -256,13 +265,13 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
float GetValue(MUnique.OpenMU.AttributeSystem.AttributeDefinition stat)
{
- var attr = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == stat);
+ var attr = clone.Attributes?.FirstOrDefault(a => a.AttributeDefinition == stat);
return attr?.Value ?? 0;
}
void Scale(MUnique.OpenMU.AttributeSystem.AttributeDefinition stat)
{
- var attr = clone.Attributes.FirstOrDefault(a => a.AttributeDefinition == stat);
+ var attr = clone.Attributes?.FirstOrDefault(a => a.AttributeDefinition == stat);
if (attr is not null)
{
attr.Value *= energyScale;
From 2b6c19820407dc09b6db93ac3c6d6f2d0e2d38ef Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 20:45:43 -0300
Subject: [PATCH 041/190] elf summon final fix xD
---
.../Skills/TargetedSkillDefaultPlugin.cs | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index ffa169e50..1bafb2e5c 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -12,6 +12,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.PlugIns;
using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence;
///
/// Action to perform a skill which is explicitly aimed to a target.
@@ -203,6 +204,33 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var clone = baseDefinition.Clone(player.GameContext.Configuration);
+ // Fallback: If attributes are not populated, try to load them from persistence (no cache).
+ if (clone.Attributes is null || clone.Attributes.Count == 0)
+ {
+ try
+ {
+ using var ctx = player.GameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(MUnique.OpenMU.DataModel.Configuration.MonsterDefinition), useCache: false);
+ var all = ctx.GetAsync().AsTask().GetAwaiter().GetResult();
+ var dbDef = all.FirstOrDefault(d => d.Number == baseDefinition.Number);
+ if (dbDef?.Attributes?.Any() == true)
+ {
+ foreach (var a in dbDef.Attributes)
+ {
+ clone.Attributes?.Add(new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value });
+ }
+
+ if (SummonDiagEnabled)
+ {
+ player.Logger.LogInformation($"[SUMMON] Fallback loaded {clone.Attributes?.Count ?? 0} attributes for monster {dbDef.Designation} ({dbDef.Number})");
+ }
+ }
+ }
+ catch
+ {
+ // ignore - we scale what we have
+ }
+ }
+
// Read energy scaling settings from plugin configuration, even if plugin is inactive.
// Defaults if not configured.
int energyPerStep = 1000;
From 1b30216019d43d3d7731e63a0c7766d5f20fcea4 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 21:13:47 -0300
Subject: [PATCH 042/190] add logic stats
---
.../MonsterDefinitionAttributeCache.cs | 68 +++++++++++++++++++
.../Skills/TargetedSkillDefaultPlugin.cs | 36 +++++++++-
2 files changed, 103 insertions(+), 1 deletion(-)
create mode 100644 src/GameLogic/MonsterDefinitionAttributeCache.cs
diff --git a/src/GameLogic/MonsterDefinitionAttributeCache.cs b/src/GameLogic/MonsterDefinitionAttributeCache.cs
new file mode 100644
index 000000000..4513f1ac5
--- /dev/null
+++ b/src/GameLogic/MonsterDefinitionAttributeCache.cs
@@ -0,0 +1,68 @@
+namespace MUnique.OpenMU.GameLogic;
+
+using System.Collections.Concurrent;
+using MUnique.OpenMU.DataModel.Configuration;
+
+///
+/// Caches monster base attributes by monster number, loaded once from persistence (similar to AdminPanel data source).
+///
+internal static class MonsterDefinitionAttributeCache
+{
+ private static readonly ConcurrentDictionary> Cache = new();
+ private static volatile bool _loaded;
+
+ public static void EnsureLoaded(IGameContext gameContext)
+ {
+ if (_loaded)
+ {
+ return;
+ }
+
+ lock (Cache)
+ {
+ if (_loaded)
+ {
+ return;
+ }
+
+ try
+ {
+ using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(MonsterDefinition), useCache: false);
+ var all = ctx.GetAsync().AsTask().GetAwaiter().GetResult();
+ foreach (var def in all)
+ {
+ if (def?.Attributes is { Count: > 0 })
+ {
+ Cache[def.Number] = def.Attributes
+ .Select(a => new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value })
+ .ToList();
+ }
+ }
+ }
+ catch
+ {
+ // ignore; cache may remain empty and runtime code can handle it.
+ }
+ finally
+ {
+ _loaded = true;
+ }
+ }
+ }
+
+ public static bool TryFillAttributes(IGameContext gameContext, short monsterNumber, MonsterDefinition target)
+ {
+ EnsureLoaded(gameContext);
+ if (Cache.TryGetValue(monsterNumber, out var list) && list.Count > 0)
+ {
+ foreach (var a in list)
+ {
+ target.Attributes?.Add(new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value });
+ }
+ return true;
+ }
+
+ return false;
+ }
+}
+
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 1bafb2e5c..b7ae01782 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -204,7 +204,41 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var clone = baseDefinition.Clone(player.GameContext.Configuration);
- // Fallback: If attributes are not populated, try to load them from persistence (no cache).
+ // Fallback 1: If attributes are not populated, try to take them from any map spawn which uses the same monster number.
+ if (clone.Attributes is null || clone.Attributes.Count == 0)
+ {
+ try
+ {
+ var refDef = player.GameContext.Configuration.Maps
+ .SelectMany(m => m.MonsterSpawns)
+ .Select(s => s.MonsterDefinition)
+ .FirstOrDefault(d => d is { } && d.Number == baseDefinition.Number && d.Attributes?.Any() == true);
+
+ if (refDef?.Attributes?.Any() == true)
+ {
+ foreach (var a in refDef.Attributes)
+ {
+ clone.Attributes?.Add(new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value });
+ }
+
+ if (SummonDiagEnabled)
+ {
+ player.Logger.LogInformation($"[SUMMON] Fallback-from-map loaded {clone.Attributes?.Count ?? 0} attributes for monster {refDef.Designation} ({refDef.Number})");
+ }
+ }
+ else
+ {
+ // Try unified cache (loads once from persistence similar to admin panel data source)
+ if (MonsterDefinitionAttributeCache.TryFillAttributes(player.GameContext, baseDefinition.Number, clone) && SummonDiagEnabled)
+ {
+ player.Logger.LogInformation($"[SUMMON] Fallback-from-cache loaded {clone.Attributes?.Count ?? 0} attributes for monster {baseDefinition.Designation} ({baseDefinition.Number})");
+ }
+ }
+ }
+ catch { }
+ }
+
+ // Fallback 2: Try to load them from persistence (no cache).
if (clone.Attributes is null || clone.Attributes.Count == 0)
{
try
From d235ddc4baa46ce0fc9b402cc46ac93dc95c81da Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 21:24:38 -0300
Subject: [PATCH 043/190] test
---
.../MonsterDefinitionAttributeCache.cs | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/src/GameLogic/MonsterDefinitionAttributeCache.cs b/src/GameLogic/MonsterDefinitionAttributeCache.cs
index 4513f1ac5..8e1f7ee1a 100644
--- a/src/GameLogic/MonsterDefinitionAttributeCache.cs
+++ b/src/GameLogic/MonsterDefinitionAttributeCache.cs
@@ -27,15 +27,19 @@ public static void EnsureLoaded(IGameContext gameContext)
try
{
- using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(MonsterDefinition), useCache: false);
- var all = ctx.GetAsync().AsTask().GetAwaiter().GetResult();
- foreach (var def in all)
+ // Load using the GameConfiguration aggregate root (same approach as Admin Panel IDataSource)
+ using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(GameConfiguration), useCache: false);
+ var owner = ctx.GetAsync().AsTask().GetAwaiter().GetResult().FirstOrDefault();
+ if (owner is not null)
{
- if (def?.Attributes is { Count: > 0 })
+ foreach (var def in owner.Monsters)
{
- Cache[def.Number] = def.Attributes
- .Select(a => new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value })
- .ToList();
+ if (def?.Attributes is { Count: > 0 })
+ {
+ Cache[def.Number] = def.Attributes
+ .Select(a => new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value })
+ .ToList();
+ }
}
}
}
@@ -65,4 +69,3 @@ public static bool TryFillAttributes(IGameContext gameContext, short monsterNumb
return false;
}
}
-
From 3f01b97c03cf162b1691cc93e1960cc7a87b88bb Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 21:36:39 -0300
Subject: [PATCH 044/190] testing cacge
---
src/GameLogic/MonsterDefinitionAttributeCache.cs | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/GameLogic/MonsterDefinitionAttributeCache.cs b/src/GameLogic/MonsterDefinitionAttributeCache.cs
index 8e1f7ee1a..0bdc0262b 100644
--- a/src/GameLogic/MonsterDefinitionAttributeCache.cs
+++ b/src/GameLogic/MonsterDefinitionAttributeCache.cs
@@ -2,6 +2,7 @@ namespace MUnique.OpenMU.GameLogic;
using System.Collections.Concurrent;
using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.Persistence;
///
/// Caches monster base attributes by monster number, loaded once from persistence (similar to AdminPanel data source).
@@ -27,10 +28,10 @@ public static void EnsureLoaded(IGameContext gameContext)
try
{
- // Load using the GameConfiguration aggregate root (same approach as Admin Panel IDataSource)
- using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(GameConfiguration), useCache: false);
- var owner = ctx.GetAsync().AsTask().GetAwaiter().GetResult().FirstOrDefault();
- if (owner is not null)
+ // Load using the AdminPanel-like data source (includes children according to GameConfigurationHelper)
+ var ds = new GameConfigurationDataSource(gameContext.LoggerFactory.CreateLogger(), gameContext.PersistenceContextProvider);
+ var ownerObj = ds.GetOwnerAsync(default).AsTask().GetAwaiter().GetResult();
+ if (ownerObj is GameConfiguration owner)
{
foreach (var def in owner.Monsters)
{
From 9a11405326e6504c1720c55ad5e1dbe2ac3e0b03 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 21:48:35 -0300
Subject: [PATCH 045/190] fix cache issues
---
src/GameLogic/MonsterDefinitionAttributeCache.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/GameLogic/MonsterDefinitionAttributeCache.cs b/src/GameLogic/MonsterDefinitionAttributeCache.cs
index 0bdc0262b..caa9dfa7c 100644
--- a/src/GameLogic/MonsterDefinitionAttributeCache.cs
+++ b/src/GameLogic/MonsterDefinitionAttributeCache.cs
@@ -31,6 +31,8 @@ public static void EnsureLoaded(IGameContext gameContext)
// Load using the AdminPanel-like data source (includes children according to GameConfigurationHelper)
var ds = new GameConfigurationDataSource(gameContext.LoggerFactory.CreateLogger(), gameContext.PersistenceContextProvider);
var ownerObj = ds.GetOwnerAsync(default).AsTask().GetAwaiter().GetResult();
+ // Force-load child collections like the Admin Panel does (builds internal dictionary by enumerating children)
+ _ = ds.GetAll();
if (ownerObj is GameConfiguration owner)
{
foreach (var def in owner.Monsters)
From f10d56d0068ce6f5a45c5122b5e03a0c8173158c Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Fri, 12 Sep 2025 22:14:43 -0300
Subject: [PATCH 046/190] fix logic summon
---
.../MonsterDefinitionAttributeCache.cs | 49 ++++++++++++++++---
.../Skills/TargetedSkillDefaultPlugin.cs | 31 +++++++++++-
2 files changed, 71 insertions(+), 9 deletions(-)
diff --git a/src/GameLogic/MonsterDefinitionAttributeCache.cs b/src/GameLogic/MonsterDefinitionAttributeCache.cs
index caa9dfa7c..0a8fe56b3 100644
--- a/src/GameLogic/MonsterDefinitionAttributeCache.cs
+++ b/src/GameLogic/MonsterDefinitionAttributeCache.cs
@@ -1,17 +1,48 @@
namespace MUnique.OpenMU.GameLogic;
using System.Collections.Concurrent;
+using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Persistence;
///
/// Caches monster base attributes by monster number, loaded once from persistence (similar to AdminPanel data source).
+/// Stores stat-id/value pairs to allow safe cloning into any target type.
///
internal static class MonsterDefinitionAttributeCache
{
- private static readonly ConcurrentDictionary> Cache = new();
+ private static readonly ConcurrentDictionary> Cache = new();
private static volatile bool _loaded;
+ private static void AddAttributeCorrectly(IGameContext context, ICollection? target, Guid statId, float value)
+ {
+ if (target is null)
+ {
+ return;
+ }
+
+ var def = context.Configuration.Attributes.FirstOrDefault(a => a.Id == statId);
+ if (def is null)
+ {
+ return;
+ }
+
+ var collectionType = target.GetType();
+ if (collectionType.IsGenericType && collectionType.GetGenericTypeDefinition() == typeof(MUnique.OpenMU.Persistence.CollectionAdapter<,>))
+ {
+ var efItemType = collectionType.GetGenericArguments()[1];
+ if (Activator.CreateInstance(efItemType) is MonsterAttribute efAttr)
+ {
+ efAttr.AttributeDefinition = def;
+ efAttr.Value = value;
+ target.Add(efAttr);
+ return;
+ }
+ }
+
+ target.Add(new MonsterAttribute { AttributeDefinition = def, Value = value });
+ }
+
public static void EnsureLoaded(IGameContext gameContext)
{
if (_loaded)
@@ -31,17 +62,21 @@ public static void EnsureLoaded(IGameContext gameContext)
// Load using the AdminPanel-like data source (includes children according to GameConfigurationHelper)
var ds = new GameConfigurationDataSource(gameContext.LoggerFactory.CreateLogger(), gameContext.PersistenceContextProvider);
var ownerObj = ds.GetOwnerAsync(default).AsTask().GetAwaiter().GetResult();
- // Force-load child collections like the Admin Panel does (builds internal dictionary by enumerating children)
- _ = ds.GetAll();
+ _ = ds.GetAll(); // touch to ensure attributes get materialized
if (ownerObj is GameConfiguration owner)
{
foreach (var def in owner.Monsters)
{
if (def?.Attributes is { Count: > 0 })
{
- Cache[def.Number] = def.Attributes
- .Select(a => new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value })
+ var pairs = def.Attributes
+ .Where(a => a.AttributeDefinition is { })
+ .Select(a => (a.AttributeDefinition!.Id, a.Value))
.ToList();
+ if (pairs.Count > 0)
+ {
+ Cache[def.Number] = pairs;
+ }
}
}
}
@@ -62,9 +97,9 @@ public static bool TryFillAttributes(IGameContext gameContext, short monsterNumb
EnsureLoaded(gameContext);
if (Cache.TryGetValue(monsterNumber, out var list) && list.Count > 0)
{
- foreach (var a in list)
+ foreach (var (statId, value) in list)
{
- target.Attributes?.Add(new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value });
+ AddAttributeCorrectly(gameContext, target.Attributes, statId, value);
}
return true;
}
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index b7ae01782..836258446 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -204,6 +204,33 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
var clone = baseDefinition.Clone(player.GameContext.Configuration);
+ static void AddAttributeCorrectly(IGameContext context, ICollection? target, MUnique.OpenMU.AttributeSystem.AttributeDefinition? definition, float value)
+ {
+ if (target is null || definition is null)
+ {
+ return;
+ }
+
+ // Resolve AttributeDefinition to the instance within the current game configuration, if present.
+ var resolvedDef = context.Configuration.Attributes.FirstOrDefault(a => a.Id == definition.Id) ?? definition;
+
+ var collectionType = target.GetType();
+ if (collectionType.IsGenericType && collectionType.GetGenericTypeDefinition() == typeof(MUnique.OpenMU.Persistence.CollectionAdapter<,>))
+ {
+ var efItemType = collectionType.GetGenericArguments()[1];
+ if (Activator.CreateInstance(efItemType) is MonsterAttribute efAttr)
+ {
+ efAttr.AttributeDefinition = resolvedDef;
+ efAttr.Value = value;
+ target.Add(efAttr);
+ return;
+ }
+ }
+
+ // Fallback: Plain collection which accepts base type.
+ target.Add(new MonsterAttribute { AttributeDefinition = resolvedDef, Value = value });
+ }
+
// Fallback 1: If attributes are not populated, try to take them from any map spawn which uses the same monster number.
if (clone.Attributes is null || clone.Attributes.Count == 0)
{
@@ -218,7 +245,7 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
{
foreach (var a in refDef.Attributes)
{
- clone.Attributes?.Add(new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value });
+ AddAttributeCorrectly(player.GameContext, clone.Attributes, a.AttributeDefinition, a.Value);
}
if (SummonDiagEnabled)
@@ -250,7 +277,7 @@ public override async ValueTask PerformSkillAsync(Player player, IAttackable tar
{
foreach (var a in dbDef.Attributes)
{
- clone.Attributes?.Add(new MonsterAttribute { AttributeDefinition = a.AttributeDefinition, Value = a.Value });
+ AddAttributeCorrectly(player.GameContext, clone.Attributes, a.AttributeDefinition, a.Value);
}
if (SummonDiagEnabled)
From 11a9ae40deb57547a16a92890cf1873c8ab57ca6 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Sat, 13 Sep 2025 04:47:51 -0300
Subject: [PATCH 047/190] fix plugins
---
.../Skills/TargetedSkillDefaultPlugin.cs | 52 ++++++++++++-------
1 file changed, 34 insertions(+), 18 deletions(-)
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 836258446..45adc69c2 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -292,35 +292,51 @@ static void AddAttributeCorrectly(IGameContext context, ICollection a.DefinedTypes)
- .FirstOrDefault(t => typeof(ISummonConfigurationPlugIn).IsAssignableFrom(t)
- && !t.IsAbstract && !t.IsInterface
- && this.TryGetSummonKey(t) == skillNumber);
+ if (MUnique.OpenMU.GameLogic.PlugIns.ElfSummonsConfigCore.Instance.Map.TryGetValue(skillNumber, out var entry))
+ {
+ if (entry.EnergyPerStep > 0)
+ {
+ energyPerStep = entry.EnergyPerStep;
+ }
- var typeId = type?.GUID;
- var plugInConfig = typeId is null ? null : player.GameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId.Value);
- if (plugInConfig is not null)
+ if (entry.PercentPerStep > 0)
+ {
+ percentPerStep = entry.PercentPerStep;
+ }
+ }
+ else
{
- var parsed = plugInConfig.GetConfiguration(player.GameContext.PlugInManager.CustomConfigReferenceHandler);
- if (parsed is not null)
+ // Fallback: Read directly from plugin configuration store.
+ var type = AppDomain.CurrentDomain
+ .GetAssemblies()
+ .SelectMany(a => a.DefinedTypes)
+ .FirstOrDefault(t => typeof(ISummonConfigurationPlugIn).IsAssignableFrom(t)
+ && !t.IsAbstract && !t.IsInterface
+ && this.TryGetSummonKey(t) == skillNumber);
+
+ var typeId = type?.GUID;
+ var plugInConfig = typeId is null ? null : player.GameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId.Value);
+ if (plugInConfig is not null)
{
- if (parsed.EnergyPerStep > 0)
+ var parsed = plugInConfig.GetConfiguration(player.GameContext.PlugInManager.CustomConfigReferenceHandler);
+ if (parsed is not null)
{
- energyPerStep = parsed.EnergyPerStep;
- }
+ if (parsed.EnergyPerStep > 0)
+ {
+ energyPerStep = parsed.EnergyPerStep;
+ }
- if (parsed.PercentPerStep > 0)
- {
- percentPerStep = parsed.PercentPerStep;
+ if (parsed.PercentPerStep > 0)
+ {
+ percentPerStep = parsed.PercentPerStep;
+ }
}
}
}
From dd84d082e74272eab34d9e648970b1a78e8c085e Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Sat, 13 Sep 2025 05:37:59 -0300
Subject: [PATCH 048/190] final cfg summon
better summon mechanics
---
.../NPC/SummonedMonsterIntelligence.cs | 10 +++++++++-
src/GameLogic/Player.cs | 17 ++++++++---------
.../Skills/AreaSkillAttackAction.cs | 3 +--
.../Skills/TargetedSkillDefaultPlugin.cs | 5 +----
4 files changed, 19 insertions(+), 16 deletions(-)
diff --git a/src/GameLogic/NPC/SummonedMonsterIntelligence.cs b/src/GameLogic/NPC/SummonedMonsterIntelligence.cs
index 4456897a3..d13ab477e 100644
--- a/src/GameLogic/NPC/SummonedMonsterIntelligence.cs
+++ b/src/GameLogic/NPC/SummonedMonsterIntelligence.cs
@@ -17,6 +17,8 @@ public sealed class SummonedMonsterIntelligence : BasicMonsterIntelligence
public SummonedMonsterIntelligence(Player owner)
{
this.Owner = owner;
+ // Summons should be allowed to walk within safezones to follow their owner.
+ this.CanWalkOnSafezone = true;
}
///
@@ -27,6 +29,12 @@ public SummonedMonsterIntelligence(Player owner)
///
public override void RegisterHit(IAttacker attacker)
{
+ // Do not aggro against the owner, even if the owner hits the summon intentionally.
+ if (attacker is Player p && p == this.Owner)
+ {
+ return;
+ }
+
if (this.CurrentTarget is null
|| attacker.IsInRange(this.Npc.Position, this.Npc.Definition.AttackRange))
{
@@ -71,4 +79,4 @@ protected override async ValueTask CanAttackAsync()
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index 7fa0e0284..209cb1dd9 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -1060,9 +1060,8 @@ public async ValueTask ClientReadyAfterMapChangeAsync()
await this.WarpToSafezoneAsync().ConfigureAwait(false);
}
- // Recreate summon only if not in safezone; otherwise skip until player leaves safezone.
- if (!this.CurrentMap.Terrain.SafezoneMap[this.SelectedCharacter.PositionX, this.SelectedCharacter.PositionY]
- && this.Summon?.Item1 is { IsAlive: true } summon)
+ // Recreate summon on the new map to keep internal map references consistent, even in safezone.
+ if (this.Summon?.Item1 is { IsAlive: true } summon)
{
// Recreate the summon on the new map to keep internal map references consistent.
// The existing summon instance still references the old map in its immutable CurrentMap property.
@@ -1587,7 +1586,7 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
throw new InvalidOperationException("Can't add a summon for a player which isn't spawned yet.");
}
- // Find a valid spawn point close to the player (walkable and outside safezone).
+ // Find a valid spawn point close to the player (walkable). Prefer outside safezone, but allow safezone when the owner is there.
Point spawnPoint = this.Position;
var terrain = gameMap.Terrain;
bool found = false;
@@ -1596,7 +1595,7 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
for (var attempts = 0; attempts < 12 && !found; attempts++)
{
var p = terrain.GetRandomCoordinate(this.Position, (byte)radius);
- if (terrain.WalkMap[p.X, p.Y] && !terrain.SafezoneMap[p.X, p.Y])
+ if (terrain.WalkMap[p.X, p.Y] && (!terrain.SafezoneMap[p.X, p.Y] || terrain.SafezoneMap[this.Position.X, this.Position.Y]))
{
spawnPoint = p;
found = true;
@@ -1604,11 +1603,11 @@ public async ValueTask CreateSummonedMonsterAsync(MonsterDefinition definition)
}
}
- if (!found && terrain.SafezoneMap[this.Position.X, this.Position.Y])
+ // If still not found, try owner's current position if walkable.
+ if (!found && terrain.WalkMap[this.Position.X, this.Position.Y])
{
- // Can't summon in safezone; silently abort.
- this.Logger.LogInformation("[SUMMON] Abort: Player in safezone, no valid spawn tile for summon.");
- return;
+ spawnPoint = this.Position;
+ found = true;
}
var area = new MonsterSpawnArea
diff --git a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
index e62f7247c..5517e91a1 100644
--- a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
+++ b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
@@ -219,8 +219,7 @@ private static IEnumerable GetTargetsInRange(Player player, Point t
.Where(a => !a.IsAtSafezone())
?? [];
- // Don't hit own summoned monsters with area skills.
- targetsInRange = targetsInRange.Where(a => a is not Monster { SummonedBy: { } owner } || owner != player);
+ // Allow area skills to hit own summoned monsters; their AI ignores aggro from the owner.
if (skill.AreaSkillSettings is { UseFrustumFilter: true } areaSkillSettings)
{
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index 45adc69c2..b0c2cabcc 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -588,10 +588,7 @@ private async ValueTask ApplySkillAsync(Player player, IAttackable targete
{
if (skill.SkillType == SkillType.DirectHit || skill.SkillType == SkillType.CastleSiegeSkill)
{
- if (target is Monster { SummonedBy: { } owner } && owner == player)
- {
- continue;
- }
+ // Allow attacking own summons intentionally (e.g. with CTRL). Aggro against owner is blocked in SummonedMonsterIntelligence.
if (player.Attributes![Stats.AmmunitionConsumptionRate] > player.Attributes[Stats.AmmunitionAmount])
{
From 1115096ba8e3b9aa824538b2c0e5f5ef5a958d07 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Sat, 13 Sep 2025 05:54:35 -0300
Subject: [PATCH 049/190] fix sumon pvp
---
src/GameLogic/Player.cs | 28 +++++++++++--------
.../Skills/AreaSkillAttackAction.cs | 3 +-
.../Skills/TargetedSkillDefaultPlugin.cs | 7 ++++-
3 files changed, 24 insertions(+), 14 deletions(-)
diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs
index 209cb1dd9..bf80a3d7e 100644
--- a/src/GameLogic/Player.cs
+++ b/src/GameLogic/Player.cs
@@ -67,6 +67,9 @@ public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacke
private Lazy? _comboStateLazy;
+ // Stores the definition of a summon which should be recreated after a map change/warp.
+ private MonsterDefinition? _pendingSummonDefinition;
+
///
/// Initializes a new instance of the class.
/// di
@@ -1060,15 +1063,12 @@ public async ValueTask ClientReadyAfterMapChangeAsync()
await this.WarpToSafezoneAsync().ConfigureAwait(false);
}
- // Recreate summon on the new map to keep internal map references consistent, even in safezone.
- if (this.Summon?.Item1 is { IsAlive: true } summon)
+ // Recreate summon on the new map if we had one before warping (even in safezone).
+ if (this._pendingSummonDefinition is { } pending)
{
- // Recreate the summon on the new map to keep internal map references consistent.
- // The existing summon instance still references the old map in its immutable CurrentMap property.
- var definition = summon.Definition;
- await summon.DisposeAsync().ConfigureAwait(false);
- this.Summon = null;
- await this.CreateSummonedMonsterAsync(definition).ConfigureAwait(false);
+ var toSpawn = pending;
+ this._pendingSummonDefinition = null;
+ await this.CreateSummonedMonsterAsync(toSpawn).ConfigureAwait(false);
}
}
@@ -1847,6 +1847,13 @@ private async ValueTask TryRemoveFromCurrentMapAsync(bool willRespawnOnSam
return false;
}
+ // If we have a summon, remove it cleanly and remember its definition to recreate later after the map change.
+ if (this.Summon?.Item1 is { IsAlive: true } summonBeforeWarp)
+ {
+ this._pendingSummonDefinition = summonBeforeWarp.Definition;
+ await this.RemoveSummonAsync().ConfigureAwait(false);
+ }
+
if (willRespawnOnSameMap)
{
await currentMap.InitRespawnAsync(this).ConfigureAwait(false);
@@ -1860,10 +1867,7 @@ private async ValueTask TryRemoveFromCurrentMapAsync(bool willRespawnOnSam
this.IsTeleporting = false;
await this._walker.StopAsync().ConfigureAwait(false);
await this._observerToWorldViewAdapter.ClearObservingObjectsListAsync().ConfigureAwait(false);
- if (this.Summon?.Item1 is { IsAlive: true } summon)
- {
- await currentMap.RemoveAsync(summon).ConfigureAwait(false);
- }
+ // Summon (if any) was removed earlier and stored for re-creation.
return true;
}
diff --git a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
index 5517e91a1..ff1b631f8 100644
--- a/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
+++ b/src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
@@ -219,7 +219,8 @@ private static IEnumerable GetTargetsInRange(Player player, Point t
.Where(a => !a.IsAtSafezone())
?? [];
- // Allow area skills to hit own summoned monsters; their AI ignores aggro from the owner.
+ // Don't hit own summoned monsters with area skills (classic behavior; requires CTRL on direct hit only).
+ targetsInRange = targetsInRange.Where(a => a is not Monster { SummonedBy: { } owner } || owner != player);
if (skill.AreaSkillSettings is { UseFrustumFilter: true } areaSkillSettings)
{
diff --git a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
index b0c2cabcc..e8a7d56c5 100644
--- a/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
+++ b/src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
@@ -588,7 +588,12 @@ private async ValueTask ApplySkillAsync(Player player, IAttackable targete
{
if (skill.SkillType == SkillType.DirectHit || skill.SkillType == SkillType.CastleSiegeSkill)
{
- // Allow attacking own summons intentionally (e.g. with CTRL). Aggro against owner is blocked in SummonedMonsterIntelligence.
+ // Block direct hits against own summon (client requires CTRL to send such an action for "basic"),
+ // keeping classic behavior. Aggro against owner is blocked in SummonedMonsterIntelligence anyway.
+ if (target is Monster { SummonedBy: { } owner } && owner == player)
+ {
+ continue;
+ }
if (player.Attributes![Stats.AmmunitionConsumptionRate] > player.Attributes[Stats.AmmunitionAmount])
{
From e88d4024c1f7301414f9a62bba9be772b08922b1 Mon Sep 17 00:00:00 2001
From: emanuel catania <82476648+EmanuelCatania@users.noreply.github.com>
Date: Sat, 13 Sep 2025 06:29:27 -0300
Subject: [PATCH 050/190] add console
Console in webpanel
---
docs/CHANGES_SUMMON_AND_LOGS.md | 22 ++
.../AdminPanel/Components/LogTailWidget.razor | 149 +++++++++++++
src/Web/AdminPanel/Exports.cs | 3 +-
.../MUnique.OpenMU.Web.AdminPanel.csproj | 3 +
src/Web/AdminPanel/Pages/Index.razor | 2 +
src/Web/AdminPanel/Pages/LogTail.razor | 202 ++++++++++++++++++
src/Web/AdminPanel/Shared/NavMenu.razor | 10 +
src/Web/AdminPanel/wwwroot/js/logs.js | 33 +++
8 files changed, 423 insertions(+), 1 deletion(-)
create mode 100644 docs/CHANGES_SUMMON_AND_LOGS.md
create mode 100644 src/Web/AdminPanel/Components/LogTailWidget.razor
create mode 100644 src/Web/AdminPanel/Pages/LogTail.razor
create mode 100644 src/Web/AdminPanel/wwwroot/js/logs.js
diff --git a/docs/CHANGES_SUMMON_AND_LOGS.md b/docs/CHANGES_SUMMON_AND_LOGS.md
new file mode 100644
index 000000000..1aadbf83e
--- /dev/null
+++ b/docs/CHANGES_SUMMON_AND_LOGS.md
@@ -0,0 +1,22 @@
+# Recent changes (Summon and Admin Panel)
+
+## Summon (Elf) fixes and scaling
+
+- Summon base stats are now loaded and cloned correctly (EF types) so values are no longer zero.
+- Energy-based scaling can be configured from the Admin Panel (plug-in "Elf Summon cfg — 30..36").
+ - Formula: `scale = 1 + floor(Energy / EnergyPerStep) * PercentPerStep`.
+ - If the plug-in is not active, stored configuration values are still respected.
+- Classic behavior restored:
+ - Summon follows the owner inside safezone.
+ - Summon never aggroes the owner even if the owner hits it.
+ - Direct and area skills do not hit own summon (basic attack with CTRL is handled by the client).
+- Optional diagnostics: set environment variable `SUMMON_DIAG=1` to enable detailed server logs.
+
+## Live Logs in Admin Panel
+
+- New page: "Logs en Vivo" in the left menu (below "Archivos de Log").
+- Endpoint configurable via env var `LOG_TAIL_URL`.
+ - Default: `/api/logs/tail?take=200`
+ - Example: `https://mu.server-pups.space/api/logs/tail?take=200`
+- Includes line count, text filter and auto-refresh (3s).
+
diff --git a/src/Web/AdminPanel/Components/LogTailWidget.razor b/src/Web/AdminPanel/Components/LogTailWidget.razor
new file mode 100644
index 000000000..68c556e79
--- /dev/null
+++ b/src/Web/AdminPanel/Components/LogTailWidget.razor
@@ -0,0 +1,149 @@
+@using System.Threading
+@inject NavigationManager Nav
+@inject Microsoft.JSInterop.IJSRuntime JS
+
+