Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion docs-website/docs/admin-panel/map-editor.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Map editor
sidebar_position: 10
description: Edit monster spawn areas and gates of a map graphically.
description: Edit monster spawn areas, gates and the terrain of a map graphically.
---

# Map editor
Expand Down Expand Up @@ -67,3 +67,55 @@ This is how you

Import replaces the objects of the currently selected map, so export first if you
want to be able to go back.

## Terrain editor

**Navigation:** *Configuration → Terrain editor* — route `/terrain-editor`.

The terrain defines for every tile of a map whether it can be walked on, is a
safezone, a wall, a hole or water. The terrain editor lets you paint these
attributes directly on the map.

Select the map, and in the second drop-down the terrain to edit: the **default
terrain**, or one of the **terrain variants** which replace it in certain states
of the map, e.g. the peace, occupied and war terrains of the Crywolf fortress.

![The terrain editor showing the war terrain of the Crywolf fortress](/img/admin-panel/terrain-editor.png)

The tools:

| Tool | Edits |
|---|---|
| **Brush** | The tiles under the cursor while the left mouse button is pressed; the brush size sets the width |
| **Rectangle** | All tiles of the dragged rectangle |
| **Fill** | All connected tiles which have the same attributes as the clicked one |

The **mode** decides what happens to the selected **attributes**: *add* them to
the tiles, *remove* them, or *replace* all attributes of the tiles with them.
Replace without any selected attribute makes the tiles plain walkable ground.

![A zoomed terrain with the tile grid, while drawing a rectangle](/img/admin-panel/terrain-editor-zoomed.png)

Use the mouse wheel or the zoom buttons to zoom, and drag with the right mouse
button to pan. Below the terrain you see the coordinates and the attributes of
the tile under the cursor. **Undo** and **Redo** work per terrain until you
switch to another one; **Save** writes the changes of all terrains to the
database.

### Terrain files

* **Import terrain file** reads a terrain file into the selected terrain. It
accepts both the `*.att` file of the original server and the encrypted
`EncTerrain*.att` file of the game client. You can undo an import.
* **Server terrain file** downloads the terrain in the `*.att` format of the
original server.
* **Client terrain file** downloads the encrypted terrain file of the game
client. The client loads it from `Data\World<N>\EncTerrain<N>.att`, where
`<N>` is the map number plus one.

:::warning[The client needs the terrain, too]
The game servers use a changed terrain after they reloaded their configuration.
The game client, however, has its own copy of the terrain: if walls, safezones
or water changed, distribute the client terrain file to your players as well.
Otherwise characters get stuck or walk through walls on their screen.
:::
2 changes: 1 addition & 1 deletion docs-website/docs/admin-panel/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ with `-adminpanel:disabled`.
| [Game configuration](game-configuration.md) | Everything about the game world: system settings, items, monsters, skills, maps, drops, … |
| [Plugins](plugins.md) | Activate, deactivate and configure plugins |
| [Chat commands](chat-commands.md) | Which in-game commands exist and who may use them |
| [Map editor](map-editor.md) | Edit monster spawn areas and gates graphically |
| [Map editor](map-editor.md) | Edit monster spawn areas, gates and the [terrain](map-editor.md#terrain-editor) graphically |
| [Live map](live-map.md) | Watch what happens on a map in real time |
| [Logs and monitoring](logs-and-monitoring.md) | Log files, Grafana with logs, metrics and traces |
| [Users](users.md) | The users which may log into the admin panel |
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
<NavLink class="dropdown-item" href="map-editor" @onclick="() => NavigationHistory.Clear()">
<span class="oi oi-globe" aria-hidden="true"></span> @Resources.MapEditor
</NavLink>
<NavLink class="dropdown-item" href="terrain-editor" @onclick="() => NavigationHistory.Clear()">
<span class="oi oi-brush" aria-hidden="true"></span> @Resources.TerrainEditor
</NavLink>
<hr class="dropdown-divider"/>
<NavLink class="dropdown-item" href="@($"edit-config/{typeof(GameConfiguration).FullName}/")" Match="NavLinkMatch.All" @onclick="() => NavigationHistory.Clear()">
<span class="oi oi-cog" aria-hidden="true"></span> @Resources.FullConfiguration
Expand Down
273 changes: 3 additions & 270 deletions src/Web/AdminPanel/Pages/EditMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,285 +4,18 @@

namespace MUnique.OpenMU.Web.AdminPanel.Pages;

using System.Reflection;
using System.Threading;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared;
using MUnique.OpenMU.Web.Shared.Components;
using MUnique.OpenMU.Web.Shared.Components.MapEditor;
using MUnique.OpenMU.Web.Shared.Components.Modal;
using MUnique.OpenMU.Web.Shared.Components.Toast;
using MUnique.OpenMU.Web.Shared.Services;

/// <summary>
/// A page, which shows an <see cref="MapEditor"/> for all <see cref="GameConfiguration.Maps"/>.
/// </summary>
[Route("/map-editor")]
[Route("/map-editor/{SelectedMapId:guid}")]
public sealed class EditMap : ComponentBase, IDisposable
public sealed class EditMap : MapEditorPageBase<MapEditor>
{
private List<GameMapDefinition>? _maps;
private CancellationTokenSource? _disposeCts;
private IContext? _context;
private IDisposable? _navigationLockDisposable;

/// <summary>
/// Gets or sets the selected map identifier.
/// </summary>
[Parameter]
public Guid SelectedMapId { get; set; }

/// <summary>
/// Gets or sets the modal service.
/// </summary>
[Inject]
private IModalService ModalService { get; set; } = null!;

/// <summary>
/// Gets or sets the loading overlay service.
/// </summary>
[Inject]
private LoadingOverlayService LoadingOverlay { get; set; } = null!;

/// <summary>
/// Gets or sets the toast service.
/// </summary>
[Inject]
private IToastService ToastService { get; set; } = null!;

/// <summary>
/// Gets or sets the game configuration source.
/// </summary>
[Inject]
private IDataSource<GameConfiguration> GameConfigurationSource { get; set; } = null!;

/// <summary>
/// Gets or sets the logger.
/// </summary>
[Inject]
private ILogger<EditMap> Logger { get; set; } = null!;

/// <summary>
/// Gets or sets the navigation manager.
/// </summary>
[Inject]
private NavigationManager NavigationManager { get; set; } = null!;

/// <summary>
/// Gets or sets the JavaScript runtime.
/// </summary>
[Inject]
private IJSRuntime JavaScript { get; set; } = null!;

/// <inheritdoc />
public void Dispose()
{
this._disposeCts?.Cancel();
this._disposeCts?.Dispose();
this._disposeCts = null;

this._navigationLockDisposable?.Dispose();
this._navigationLockDisposable = null;
}

/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (this._maps is null)
{
return;
}

builder.OpenComponent<Breadcrumb>(0);
builder.AddAttribute(1, nameof(Breadcrumb.Caption), Resources.MapEditor);
builder.CloseComponent();

builder.OpenComponent<CascadingValue<IContext>>(10);
builder.AddAttribute(11, nameof(CascadingValue<IContext>.Value), this._context);
builder.AddAttribute(12, nameof(CascadingValue<IContext>.IsFixed), false);
builder.AddAttribute(13, nameof(CascadingValue<IContext>.ChildContent), (RenderFragment)this.BuildMapEditorFragment);
builder.CloseComponent();
}

/// <inheritdoc />
protected override async Task OnParametersSetAsync()
{
await (this._disposeCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
this._disposeCts?.Dispose();
this._disposeCts = new CancellationTokenSource();

this._context = await this.GameConfigurationSource
.GetContextAsync(this._disposeCts.Token)
.ConfigureAwait(false);

await base.OnParametersSetAsync().ConfigureAwait(false);
}

/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender).ConfigureAwait(false);
if (this._maps is null)
{
this._disposeCts ??= new CancellationTokenSource();
var cts = this._disposeCts.Token;
_ = Task.Run(() => this.LoadDataAsync(cts), cts);
}
}

/// <inheritdoc />
protected override Task OnInitializedAsync()
{
this._navigationLockDisposable = this.NavigationManager.RegisterLocationChangingHandler(this.OnBeforeInternalNavigationAsync);
return base.OnInitializedAsync();
}

private async ValueTask OnBeforeInternalNavigationAsync(LocationChangingContext context)
{
if (!await this.AllowChangeAsync().ConfigureAwait(false))
{
context.PreventNavigation();
}
}

private async Task OnSelectedMapChangingAsync(MapChangingArgs eventArgs)
{
eventArgs.Cancel = !await this.AllowChangeAsync().ConfigureAwait(true);
if (!eventArgs.Cancel)
{
this.SelectedMapId = eventArgs.NextMap;
}
}

private async ValueTask<bool> AllowChangeAsync()
{
var cancellationToken = this._disposeCts?.Token ?? default;
var persistenceContext = await this.GameConfigurationSource
.GetContextAsync(cancellationToken)
.ConfigureAwait(true);

if (persistenceContext?.HasChanges is not true)
{
return true;
}

var isConfirmed = await this.JavaScript
.InvokeAsync<bool>("window.confirm", cancellationToken, Resources.UnsavedChangesQuestion)
.ConfigureAwait(true);

if (!isConfirmed)
{
return false;
}

await this.GameConfigurationSource.DiscardChangesAsync().ConfigureAwait(true);

IDisposable? loadingOverlay = null;
var showOverlayTask = this.InvokeAsync(() => loadingOverlay = this.LoadingOverlay.ShowLoadingIndicator());

try
{
this._context = await this.GameConfigurationSource
.GetContextAsync(cancellationToken)
.ConfigureAwait(true);

var gameConfig = await this.GameConfigurationSource
.GetOwnerAsync(Guid.Empty, cancellationToken)
.ConfigureAwait(false);

this._maps = gameConfig.Maps.OrderBy(c => c.Number).ToList();
}
finally
{
await showOverlayTask.ConfigureAwait(false);
loadingOverlay?.Dispose();
}

await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(false);

return true;
}

private async Task LoadDataAsync(CancellationToken cancellationToken)
{
IDisposable? modal = null;
var showModalTask = this.InvokeAsync(() => modal = this.LoadingOverlay.ShowLoadingIndicator());

try
{
if (!cancellationToken.IsCancellationRequested)
{
var gameConfig = await this.GameConfigurationSource
.GetOwnerAsync(Guid.Empty, cancellationToken)
.ConfigureAwait(false);

try
{
this._maps = gameConfig.Maps.OrderBy(c => c.Number).ToList();
}
catch (Exception ex)
{
this.Logger.LogError(
ex,
"Could not load game maps: {Message}{NewLine}{StackTrace}",
ex.Message,
Environment.NewLine,
ex.StackTrace);

await this.ModalService
.ShowMessageAsync(Resources.Error, Resources.CouldNotLoadMapDataCheckTheLogs)
.ConfigureAwait(false);
}

await showModalTask.ConfigureAwait(false);
modal?.Dispose();
await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(false);
}
}
catch (TargetInvocationException ex) when (ex.InnerException is ObjectDisposedException)
{
// See ObjectDisposedException.
}
catch (ObjectDisposedException)
{
// Happens when the user navigated away. The persistence layer does not
// yet have a cancellation token based async API so we swallow this.
}
}

private async Task SaveChangesAsync()
{
try
{
var context = await this.GameConfigurationSource.GetContextAsync().ConfigureAwait(true);
var success = await context.SaveChangesAsync().ConfigureAwait(true);
var text = success ? Resources.SavedChanges : Resources.NoChangesToSave;
this.ToastService.ShowSuccess(text);
}
catch (Exception ex)
{
this.Logger.LogError(ex, "Error during saving");
this.ToastService.ShowError(string.Format(Resources.UnexpectedErrorCheckLogs, ex.Message));
}
}

private void BuildMapEditorFragment(RenderTreeBuilder builder)
{
builder.OpenComponent<MapEditor>(15);
builder.AddAttribute(16, nameof(MapEditor.Maps), this._maps);
builder.AddAttribute(17, nameof(MapEditor.SelectedMapId), this.SelectedMapId);
builder.AddAttribute(18, nameof(MapEditor.OnValidSubmit), EventCallback.Factory.Create(this, this.SaveChangesAsync));
builder.AddAttribute(
19,
nameof(MapEditor.SelectedMapChanging),
EventCallback.Factory.Create<MapChangingArgs>(this, this.OnSelectedMapChangingAsync));
builder.CloseComponent();
}
}
protected override string Caption => Resources.MapEditor;
}
21 changes: 21 additions & 0 deletions src/Web/AdminPanel/Pages/EditTerrain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// <copyright file="EditTerrain.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Web.AdminPanel.Pages;

using Microsoft.AspNetCore.Components;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared.Components.TerrainEditor;

/// <summary>
/// A page, which shows a <see cref="TerrainEditor"/> for the terrains of all <see cref="GameConfiguration.Maps"/>.
/// </summary>
[Route("/terrain-editor")]
[Route("/terrain-editor/{SelectedMapId:guid}")]
public sealed class EditTerrain : MapEditorPageBase<TerrainEditor>
{
/// <inheritdoc />
protected override string Caption => Resources.TerrainEditor;
}
Loading
Loading