Skip to content
Open
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
6 changes: 5 additions & 1 deletion src/NosCore.Data/Enumerations/I18N/LanguageKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ public enum LanguageKey
OBJECT_IN_BAZAAR,
ITEM_GIFTED,
NOT_IN_FRIENDLIST,
SPEAKER
SPEAKER,
FAMILY_AUTHORITY_HEAD,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting so those don't have a internationalized version on official?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do, and I should have checked before adding four keys. conststring carries the four ranks at 10915-10918, with the parentheses already in the string, in every language the client ships:

UK  (Familyhead)  (Familydeputy)  (Familykeeper)  (Member)
FR  (Tête)        (Assistant)     (Gardien)       (Membre)
DE  (Oberhaupt)   (Vertreter)     (Verwalter)     (Mitglied)
IT  (Capo)        (Vicecapo)      (Guardiano)     (Membro)
ES  (Cabecilla)   (Representante) (Administrador) (Miembro)
CZ  (Hlava)       (Zástupce)      (Správce)       (Člen)
PL  (Szef)        (Reprezentant)  (Zarządca)      (Członek)
TR  (Ailereisi)   (Temsilci)      (Yönetici)      (Üye)

A captured line, gidx 1 626114 5052 -Nemesis-(Membre) 8, matches the French row — so the server sends the rank as text, and the client is not resolving a number here the way it does for Game18NConstString.

I have put the client's own wording into the resources rather than our translations, which fixed eighteen of the thirty-two values. What I have not done is move the source: they are still four LanguageKeys rather than rows imported from conststring. ConstStringParser reads only the UK file today, on the stated grounds that the other languages wrap the same numbers in translated prose — which is true for the ladders but not for these, where the text is the payload and differs per language by design. Turning that into a per-language import is a change to the parser's shape, so I would rather do it as its own PR if you want it. Say the word.

🤖 Addressed by Claude Code

FAMILY_AUTHORITY_ASSISTANT,
FAMILY_AUTHORITY_MANAGER,
FAMILY_AUTHORITY_MEMBER
}
}
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.cs.resx
Original file line number Diff line number Diff line change
Expand Up @@ -557,4 +557,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>Načteno skriptovaných instancí: {0}!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Hlava</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Zástupce</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Správce</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Člen</value>
</data>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -580,4 +580,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>{0} Skript-Instanzen geladen!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Oberhaupt</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Vertreter</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Verwalter</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Mitglied</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.es.resx
Original file line number Diff line number Diff line change
Expand Up @@ -484,4 +484,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>¡{0} instancias de script analizadas!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Cabecilla</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Representante</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Administrador</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Miembro</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -555,4 +555,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>{0} instances scriptées analysées !</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Tête</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Assistant</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Gardien</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Membre</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.it.resx
Original file line number Diff line number Diff line change
Expand Up @@ -601,4 +601,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>{0} istanze scriptate caricate!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all those are not translated we shouldnt add a value and not translate them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Translated, and then corrected — the first pass invented them, which is worse than leaving them in English because nobody would have looked again.

The client already has these four. conststring holds them at 10915-10918, parentheses included, in every language it ships:

UK  (Familyhead)  (Familydeputy)  (Familykeeper)  (Member)
FR  (Tête)        (Assistant)     (Gardien)       (Membre)
DE  (Oberhaupt)   (Vertreter)     (Verwalter)     (Mitglied)
IT  (Capo)        (Vicecapo)      (Guardiano)     (Membro)
ES  (Cabecilla)   (Representante) (Administrador) (Miembro)
CZ  (Hlava)       (Zástupce)      (Správce)       (Člen)
PL  (Szef)        (Reprezentant)  (Zarządca)      (Członek)
TR  (Ailereisi)   (Temsilci)      (Yönetici)      (Üye)

and a captured line reads gidx 1 626114 5052 -Nemesis-(Membre) 8, which is the French row exactly.

So the values are the client's wording now, not a translation of ours. Eighteen of the thirty-two were wrong — the first French one said Chef de famille where the client says Tête, the English one said Head against Familyhead. Russian keeps ours: the client ships no Russian file, so there is nothing to read.

🤖 Addressed by Claude Code

<value>Capo</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Vicecapo</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Guardiano</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Membro</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.pl.resx
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>Wczytano {0} instancji skryptowanych!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Szef</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Reprezentant</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Zarządca</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Członek</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -631,4 +631,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>{0} Scripted Instances Parsed!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Familyhead</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Familydeputy</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Familykeeper</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Member</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.ru.resx
Original file line number Diff line number Diff line change
Expand Up @@ -582,4 +582,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>Загружено скриптовых инстансов: {0}!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Глава семьи</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Помощник</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Управляющий</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Участник</value>
</data>
</root>
12 changes: 12 additions & 0 deletions src/NosCore.Data/Resource/LocalizedResources.tr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -573,4 +573,16 @@
<data name="TIMESPACES_PARSED" xml:space="preserve">
<value>{0} betikli örnek yüklendi!</value>
</data>
<data name="FAMILY_AUTHORITY_HEAD" xml:space="preserve">
<value>Ailereisi</value>
</data>
<data name="FAMILY_AUTHORITY_ASSISTANT" xml:space="preserve">
<value>Temsilci</value>
</data>
<data name="FAMILY_AUTHORITY_MANAGER" xml:space="preserve">
<value>Yönetici</value>
</data>
<data name="FAMILY_AUTHORITY_MEMBER" xml:space="preserve">
<value>Üye</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
//

using NodaTime;
using NosCore.GameObject.Services.FamilyService;
using System.Collections.Concurrent;

namespace NosCore.GameObject.Ecs.Components;

public record struct PlayerSocialComponent(
ConcurrentDictionary<long, long> GroupRequestCharacterIds,
Instant? LastGroupRequest);
Instant? LastGroupRequest,
Family? Family);
90 changes: 90 additions & 0 deletions src/NosCore.GameObject/Ecs/Extensions/FamilyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
//

using NosCore.Algorithm.FamilyExperienceService;
using NosCore.Core.I18N;
using NosCore.Data.Enumerations.Family;
using NosCore.Data.Enumerations.I18N;
using NosCore.Packets.ServerPackets.Families;
using NosCore.Shared.Enumerations;
using System.Linq;

namespace NosCore.GameObject.Ecs.Extensions;

public static class FamilyExtensions
{
public static GidxPacket GenerateGidx(this PlayerComponentBundle player,
IGameLanguageLocalizer localizer, RegionType viewerLanguage)
{
var family = player.Family;
if (family == null)
{
return new GidxPacket
{
VisualType = VisualType.Player,
VisualId = player.VisualId,
FamilyId = null,
FamilyName = null,
FamilyLevel = 0
};
}

return new GidxPacket
{
VisualType = VisualType.Player,
VisualId = player.VisualId,
FamilyId = family.FamilyId,
FamilyName = FamilyTag(family, player.CharacterId, localizer, viewerLanguage),
FamilyLevel = family.FamilyLevel
};
}

public static GInfoPacket? GenerateGInfo(this PlayerComponentBundle player,
IFamilyExperienceService familyExperienceService)
{
var family = player.Family;
if (family == null)
{
return null;
}

return new GInfoPacket
{
FamilyName = family.Name,
CharacterName = family.HeadCharacterName,
FamilyHeadGenderType = family.FamilyHeadGender,
FamilyLevel = family.FamilyLevel,
FamilyXp = family.FamilyExperience,
MaxFamilyXp = familyExperienceService.GetFamilyExperience(family.FamilyLevel),
MembersCount = (ushort)family.Members.Count,
MembersCapacity = family.MaxSize,
CharacterFamilyAuthority = (Packets.Enumerations.FamilyAuthority)family.AuthorityOf(player.CharacterId),
FamilyManagerCanInvit = family.ManagerCanInvite,
FamilyManagerCanNotice = family.ManagerCanNotice,
FamilyManagerCanShout = family.ManagerCanShout,
FamilyManagerCanGetHistory = family.ManagerCanGetHistory,
FamilyManagerAuthorityType = family.ManagerAuthorityType,
FamilyMemberCanGetHistory = family.MemberCanGetHistory,
FamilyMemberAuthorityType = family.MemberAuthorityType,
// A serialized field keeps its own spaces, and the notice is free text.
FamilyMessage = family.FamilyMessage?.Replace(' ', '^')
};
}

private static string FamilyTag(Services.FamilyService.Family family, long characterId,
IGameLanguageLocalizer localizer, RegionType viewerLanguage)
{
var rank = family.AuthorityOf(characterId) switch
{
FamilyAuthority.Head => LanguageKey.FAMILY_AUTHORITY_HEAD,
FamilyAuthority.Assistant => LanguageKey.FAMILY_AUTHORITY_ASSISTANT,
FamilyAuthority.Manager => LanguageKey.FAMILY_AUTHORITY_MANAGER,
_ => LanguageKey.FAMILY_AUTHORITY_MEMBER
};

return $"{family.Name}({localizer[rank, viewerLanguage]})".Replace(' ', '^');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ await player.SendPacketAsync(new MsgiPacket
});
}


public static InPacket GenerateIn(this PlayerComponentBundle player, string prefix)
{
return new InPacket
Expand Down Expand Up @@ -434,8 +433,8 @@ public static CInfoPacket GenerateCInfo(this PlayerComponentBundle player, IGame
? $"[{localizer[LanguageKey.SUPPORT, player.Account.Language]}]" + player.Name : player.Name,
Unknown1 = null,
GroupId = -1,
FamilyId = -1,
FamilyName = null,
FamilyId = (int)(player.Family?.FamilyId ?? -1),
FamilyName = player.GenerateGidx(localizer, player.AccountLanguage).FamilyName,
CharacterId = player.VisualId,
Authority = player.Authority,
Gender = player.Gender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using NosCore.GameObject.Messaging.Handlers.Nrun;
using NosCore.GameObject.Services.BroadcastService;
using NosCore.GameObject.Services.ExchangeService;
using NosCore.GameObject.Services.FamilyService;
using NosCore.GameObject.Services.GroupService;
using NosCore.GameObject.Services.MapInstanceGenerationService;
using NosCore.GameObject.Services.MateService;
Expand Down Expand Up @@ -62,6 +63,7 @@ public static void RegisterDependencies(IServiceCollection services)
services.AddSingleton<IExchangeRequestRegistry, ExchangeRequestRegistry>();
services.AddSingleton<ISessionGroupFactory, SessionGroupFactory>();
services.AddSingleton<IMateService, MateService>();
services.AddSingleton<IFamilyService, FamilyService>();

// Inter-channel hub clients — one instance per concrete HubClient, each
// exposed as all of its implemented interfaces so features that depend on
Expand Down
26 changes: 26 additions & 0 deletions src/NosCore.GameObject/Services/FamilyService/Family.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
//

using NosCore.Data.Dto;
using NosCore.Data.Enumerations.Family;
using System.Collections.Generic;
using System.Linq;

namespace NosCore.GameObject.Services.FamilyService
{
public class Family : FamilyDto
{
public IReadOnlyList<FamilyCharacterDto> Members { get; set; } = [];

/// <summary>The head's name, kept here because the head is usually offline.</summary>
public string HeadCharacterName { get; set; } = string.Empty;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need a family member name lookup as it's not the only name we need. So likely we should just rely on Member

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and it is worth settling before anything else reads a member.

FamilyCharacterDto carries CharacterId and no name — the name lives on Character, one table across. So today GetFamilyAsync loads the membership rows and then does one extra characterDao lookup for the head, because ginfo needs exactly one name and no other packet on this branch needs any.

The moment the member list window arrives that becomes N lookups, which is the wrong shape. Two ways out, and I would rather you pick than guess:

  1. project members to a small record carrying CharacterId, Authority, Rank, Experience and Name, filled by one characterDao.Where(s => ids.Contains(s.CharacterId)) for the whole family. HeadCharacterName then disappears and ginfo reads the head off Members.
  2. leave Members as the raw rows and let whoever draws the list resolve names, which keeps this PR unchanged but repeats the join at every call site.

I lean on (1) and it is a small change — the service already has the id list in hand. Happy to put it in this PR or keep it for the one that adds the window; say which.

🤖 Addressed by Claude Code


/// <summary>Not in the member list means Member: the packet cannot say "none".</summary>
public FamilyAuthority AuthorityOf(long characterId) =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would somebody not be in member list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would not — and you are right that the fallback is hiding something.

GetFamilyAsync finds the family through this character's own membership row, so by the time Family exists the character is in Members by construction. The ?? FamilyAuthority.Member is there only because AuthorityOf takes an arbitrary id and the expression has to be total; the branch is unreachable in every call the codebase makes.

The redundancy underneath it is real: the service reads the membership row, drops it, and AuthorityOf then finds it again in the list. It should either return the authority it already has, or AuthorityOf should be gone and the caller should read it off the row.

That is tangled with the other comment about Members carrying names, so I have left both alone rather than half-doing them — tell me which shape you want for Members and I will do the pair in one go.

🤖 Addressed by Claude Code

Members.FirstOrDefault(s => s.CharacterId == characterId)?.Authority
?? FamilyAuthority.Member;
}
}
52 changes: 52 additions & 0 deletions src/NosCore.GameObject/Services/FamilyService/FamilyService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
//

using Mapster;
using NosCore.Dao.Interfaces;
using NosCore.Data.Dto;
using NosCore.Data.Enumerations.Family;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NosCore.GameObject.Services.FamilyService
{
public class FamilyService(IDao<FamilyDto, long> familyDao,
IDao<FamilyCharacterDto, long> familyCharacterDao,
IDao<CharacterDto, long> characterDao) : IFamilyService
{
public async Task<Family?> GetFamilyAsync(long characterId)
{
var membership = await familyCharacterDao
.FirstOrDefaultAsync(s => s.CharacterId == characterId);
if (membership == null)
{
return null;
}

var familyDto = await familyDao
.FirstOrDefaultAsync(s => s.FamilyId == membership.FamilyId);
if (familyDto == null)
{
return null;
}

var family = familyDto.Adapt<Family>();
family.Members = familyCharacterDao.Where(s => s.FamilyId == family.FamilyId)?.ToList()
?? new List<FamilyCharacterDto>();

var head = family.Members.FirstOrDefault(s => s.Authority == FamilyAuthority.Head);
if (head != null)
{
var headCharacter = await characterDao
.FirstOrDefaultAsync(s => s.CharacterId == head.CharacterId);
family.HeadCharacterName = headCharacter?.Name ?? string.Empty;
}

return family;
}
}
}
Loading
Loading