Skip to content

Commit

Permalink
Support dialogue localization.
Browse files Browse the repository at this point in the history
  • Loading branch information
isadorasophia committed Dec 8, 2023
1 parent d36fafe commit f43ba6a
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 34 deletions.
7 changes: 4 additions & 3 deletions src/Murder.Editor/CustomEditors/CharacterEditor_Dialogs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Murder.Editor.CustomFields;
using Murder.Editor.ImGuiExtended;
using Murder.Editor.Utilities;
using Murder.Services;
using System.Collections.Immutable;
using System.Diagnostics;

Expand Down Expand Up @@ -243,13 +244,13 @@ private void DrawLines(ScriptInformation info, Dialog dialog)
if (line.Text is not null)
{
// Centralizes the text vertically.
string value = LocalizationServices.GetLocalizedString(line.Text);

float textHeight = (ImGui.GetItemRectSize().Y -
ImGui.CalcTextSize(line.Text, wrapWidth: ImGui.GetWindowContentRegionMax().X - ImGui.GetCursorPosX()).Y) / 2f;
ImGui.CalcTextSize(value, wrapWidth: ImGui.GetWindowContentRegionMax().X - ImGui.GetCursorPosX()).Y) / 2f;

ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textHeight);

string value = line.Text;

ImGui.TextWrapped(value);
}

Expand Down
29 changes: 20 additions & 9 deletions src/Murder.Editor/CustomEditors/LocalizationAssetEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ public override void DrawEditor()
{
GameLogger.Verify(_localization is not null);

bool isDefaultResource = _referenceResource == _localization.Guid;
LocalizationAsset? asset = _referenceResource is not null ?
Game.Data.TryGetAsset<LocalizationAsset>(_referenceResource.Value) : null;

bool fixButtonSelected = _referenceResource is null || _referenceResource == _localization.Guid ?
bool fixButtonSelected = _referenceResource is null || isDefaultResource ?
ImGuiHelpers.SelectedIconButton('\uf0f1') :
ImGuiHelpers.IconButton('\uf0f1', $"fix_{_localization.Guid}");

if (fixButtonSelected && _referenceResource is not null)
if (fixButtonSelected && !isDefaultResource && _referenceResource is not null)
{
if (asset is not null)
{
Expand All @@ -70,23 +71,23 @@ public override void DrawEditor()

ImGui.SameLine();

bool importButton = _referenceResource == _localization.Guid ?
bool importButton = isDefaultResource ?
ImGuiHelpers.SelectedIconButton('\uf56f') :
ImGuiHelpers.IconButton('\uf56f', $"import_{_localization.Guid}");

if (importButton)
if (importButton && !isDefaultResource)
{
LocalizationExporter.ImportFromCsv(_localization);
}

ImGuiHelpers.HelpTooltip("Import from .csv");
ImGui.SameLine();

bool exportButton = _referenceResource == _localization.Guid ?
bool exportButton = isDefaultResource ?
ImGuiHelpers.SelectedIconButton('\uf56e') :
ImGuiHelpers.IconButton('\uf56e', $"export_{_localization.Guid}");

if (exportButton)
if (exportButton && !isDefaultResource)
{
LocalizationExporter.ExportToCsv(_localization);
}
Expand All @@ -104,21 +105,29 @@ public override void DrawEditor()
{
Guid g = localizedStringData.Guid;

bool deleteButton = localizedStringData.IsGenerated ?
ImGuiHelpers.SelectedIconButton('\uf2ed') :
ImGuiHelpers.DeleteButton($"delete_{g}");

// == Delete button ==
if (ImGuiHelpers.DeleteButton($"delete_{g}"))
if (deleteButton && localizedStringData.IsGenerated)
{
_localization.RemoveResource(g, force: true);
_localization.FileChanged = true;
}

if (_referenceResource == _localization.Guid)
if (localizedStringData.IsGenerated)
{
ImGuiHelpers.HelpTooltip("Generated string");
}
else if (_referenceResource == _localization.Guid)
{
string plural = localizedStringData.Counter > 1 ? "s" : "";
ImGuiHelpers.HelpTooltip($"Remove resource string ({localizedStringData.Counter ?? 1} reference{plural})");
}
else
{
ImGuiHelpers.HelpTooltip($"Remove resource string");
ImGuiHelpers.HelpTooltip("Remove resource string");
}

ImGui.SameLine();
Expand Down Expand Up @@ -198,6 +207,8 @@ private void AddMissingResourcesFromAsset(LocalizationAsset? asset)
_localization.SetResource(data.Value with { Notes = referenceData.Notes });
}
}

_localization.SetAllDialogueResources(asset.DialogueResources);
}

/// <summary>
Expand Down
59 changes: 56 additions & 3 deletions src/Murder.Editor/Data/Dialogs/GumToMurderConverter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Bang.Components;
using Murder.Assets;
using Murder.Assets.Localization;
using Murder.Core.Dialogs;
using Murder.Diagnostics;
using Murder.Editor.Utilities;
using System.Collections.Immutable;

using GumData = Gum.InnerThoughts;

namespace Murder.Editor.Data
Expand Down Expand Up @@ -32,6 +32,16 @@ internal class GumToMurderConverter

private Guid _speakerOwner = Guid.Empty;

/// <summary>
/// These are the localized strings produced during this dialogue.
/// </summary>
private ImmutableArray<Guid>.Builder _localizedStrings = ImmutableArray.CreateBuilder<Guid>();

/// <summary>
/// These are all the previous localized strings that existed in the .gum file.
/// </summary>
private Dictionary<string, LocalizedString> _previousStringsInScript = new();

public void Reset()
{
_lastScriptFetched = null;
Expand All @@ -50,6 +60,11 @@ public void ReloadDialogWith(GumData.CharacterScript script, CharacterAsset asse
_matchedComponents = new();
_matchedPortraits = new();

_localizedStrings = ImmutableArray.CreateBuilder<Guid>();

LocalizationAsset localizationAsset = Game.Data.GetDefaultLocalization();
_previousStringsInScript = FetchResourcesForAsset(localizationAsset, asset.Guid);

SortedList<int, Situation> situations = new();
foreach (GumData.Situation gumSituation in script.FetchAllSituations())
{
Expand All @@ -61,6 +76,44 @@ public void ReloadDialogWith(GumData.CharacterScript script, CharacterAsset asse

// Remove all the components that have not been used in the latest sync.
asset.RemoveCustomComponents(_components.Keys.Where(t => !_matchedComponents.Contains(t)));

localizationAsset.SetResourcesForDialogue(asset.Guid, _localizedStrings.ToImmutable());
}

private Dictionary<string, LocalizedString> FetchResourcesForAsset(LocalizationAsset localizationAsset, Guid characterGuid)
{
ImmutableArray<Guid> resources = localizationAsset.FetchResourcesForDialogue(characterGuid);

Dictionary<string, LocalizedString> result = new();
foreach (Guid resource in resources)
{
LocalizedStringData? data = localizationAsset.TryGetResource(resource);
if (data is null)
{
continue;
}

result[data.Value.String] = new(data.Value.Guid);
}

return result;
}

private LocalizedString? TryGetLocalizedString(string? text)
{
if (text is null)
{
return null;
}

if (!_previousStringsInScript.TryGetValue(text, out LocalizedString data))
{
LocalizationAsset localizationAsset = Game.Data.GetDefaultLocalization();
data = localizationAsset.AddResource(text, isGenerated: true);
}

_localizedStrings.Add(data.Id);
return data;
}

private Situation ConvertSituation(GumData.Situation gumSituation)
Expand Down Expand Up @@ -211,7 +264,7 @@ private Dialog ConvertBlockToDialog(int situation, GumData.Block block)
// If this matches, it means that the user previously set the portrait with a custom value.
// We override whatever was set in the dialog.
_matchedPortraits.Add(id);
return new(info.Speaker, info.Portrait, gumLine.Text, gumLine.Delay);
return new(info.Speaker, info.Portrait, TryGetLocalizedString(gumLine.Text), gumLine.Delay);
}

string? gumSpeaker = gumLine.Speaker;
Expand Down Expand Up @@ -241,7 +294,7 @@ private Dialog ConvertBlockToDialog(int situation, GumData.Block block)

}

return new(speaker, portrait, gumLine.Text, gumLine.Delay);
return new(speaker, portrait, TryGetLocalizedString(gumLine.Text), gumLine.Delay);
}

private Guid? FindSpeaker(string name)
Expand Down
34 changes: 34 additions & 0 deletions src/Murder.Editor/Utilities/LocalizationExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,46 @@ public static bool ExportToCsv(LocalizationAsset asset)

builder.AppendLine("Guid,Original,Translated,Notes");

HashSet<Guid> dialogueResources = new();
foreach (LocalizationAsset.ResourceDataForAsset data in asset.DialogueResources)
{
foreach (Guid g in data.Resources)
{
dialogueResources.Add(g);
}
}

foreach (LocalizedStringData data in asset.Resources)
{
if (dialogueResources.Contains(data.Guid))
{
// Add this separately
continue;
}

LocalizedStringData? referenceData = reference.TryGetResource(data.Guid);
builder.AppendLine($"{data.Guid},\"{referenceData?.String}\",\"{data.String}\",\"{data.Notes}\"");
}

// Now, put all these right at the end of the document.
// Why, do you ask? Absolutely no reason. It just seemed reasonable that generated strings came later.
foreach (LocalizationAsset.ResourceDataForAsset dialogueData in asset.DialogueResources)
{
foreach (Guid g in dialogueData.Resources)
{
LocalizedStringData? data = asset.TryGetResource(g);
if (data is null)
{
continue;
}

LocalizedStringData? referenceData = reference.TryGetResource(g);

builder.AppendLine(
$"{data.Value.Guid},\"{referenceData?.String}\",\"{data.Value.String}\",\"{data.Value.Notes}\"");
}
}

string fullLocalizationPath = GetFullRawLocalizationPath(asset.Name);
FileHelper.CreateDirectoryPathIfNotExists(fullLocalizationPath);

Expand Down
73 changes: 70 additions & 3 deletions src/Murder/Assets/Localization/LocalizationAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ public class LocalizationAsset : GameAsset
private ImmutableArray<ResourceDataForAsset> _dialogueResources = ImmutableArray<ResourceDataForAsset>.Empty;

/// <summary>
/// Expose all the resources (for editor, etc.)
/// Expose all the dialogue resources (for editor, etc.).
/// </summary>
public ImmutableArray<ResourceDataForAsset> DialogueResources => _dialogueResources;

/// <summary>
/// Expose all the resources (for editor, etc.).
/// </summary>
public ImmutableArray<LocalizedStringData> Resources => _resources;

Expand Down Expand Up @@ -62,6 +67,17 @@ public void AddResource(Guid id)
_resources = _resources.SetItem(index, data with { Counter = counter });
}

public LocalizedString AddResource(string text, bool isGenerated)
{
LocalizedStringData data = new LocalizedStringData(
Guid.NewGuid()) with { String = text, IsGenerated = isGenerated };

_resources = _resources.Add(data);
_guidToIndexCache = null;

return new LocalizedString(data.Guid);
}

public void RemoveResource(Guid id, bool force = false)
{
if (!GuidToResourceIndex.TryGetValue(id, out int index))
Expand Down Expand Up @@ -125,13 +141,64 @@ public void UpdateOrSetResource(Guid id, string translated, string? notes)

public bool HasResource(Guid id) => GuidToResourceIndex.ContainsKey(id);

/// <summary>
/// Used when setting data from a reference data.
/// </summary>
public void SetAllDialogueResources(ImmutableArray<ResourceDataForAsset> resources)
{
_dialogueResources = resources;
}

public void SetResourcesForDialogue(Guid guid, ImmutableArray<Guid> resources)
{
for (int i = 0; i < _dialogueResources.Length; ++i)
{
ResourceDataForAsset data = _dialogueResources[i];
if (data.DialogueResourceGuid == guid)
{
HashSet<Guid> newResources = resources.ToHashSet();
foreach (Guid previousResource in data.Resources)
{
if (newResources.Contains(previousResource))
{
continue;
}

RemoveResource(previousResource);
}

_dialogueResources = _dialogueResources.SetItem(i, data with { Resources = resources });
return;
}
}

_dialogueResources = _dialogueResources.Add(
new ResourceDataForAsset() with { DialogueResourceGuid = guid, Resources = resources });
}

/// <summary>
/// Expose all resources tied to a particular dialogue.
/// </summary>
public ImmutableArray<Guid> FetchResourcesForDialogue(Guid guid)
{
foreach (ResourceDataForAsset data in _dialogueResources)
{
if (data.DialogueResourceGuid == guid)
{
return data.Resources;
}
}

return ImmutableArray<Guid>.Empty;
}

public readonly struct ResourceDataForAsset
{
/// <summary>
/// Which asset originated this resource and its respective strings.
/// </summary>
public readonly Guid Guid;
public readonly Guid DialogueResourceGuid { get; init; }

public readonly ImmutableArray<Guid> Resources;
public readonly ImmutableArray<Guid> Resources { get; init; }
}
}
8 changes: 8 additions & 0 deletions src/Murder/Assets/Localization/LocalizedString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ public readonly struct LocalizedString
{
public readonly Guid Id = Guid.Empty;

/// <summary>
/// Used when, for whatever reason, we need to override the data for a localized string
/// with actual text (usually built in runtime).
/// </summary>
public readonly string? OverrideText = null;

public LocalizedString() { }

public LocalizedString(Guid id) => Id = id;

public LocalizedString(string overrideText) => OverrideText = overrideText;
}
2 changes: 2 additions & 0 deletions src/Murder/Assets/Localization/LocalizedStringData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public readonly struct LocalizedStringData
/// </summary>
public readonly string? Notes { get; init; } = null;

public readonly bool IsGenerated { get; init; } = false;

public LocalizedStringData() { }

public LocalizedStringData(Guid guid) => Guid = guid;
Expand Down
Loading

0 comments on commit f43ba6a

Please sign in to comment.