Cleanup low-level character identification details

This consolidates how characters are identified to not depend so much on
the arbitrary values set by `CharacterNextId` (the low-level ID counter).

The `ID` value is still present for historical reasons, though every
in-game use has been ported over to use `IsPlayer()`, and the actual
unique identifier for a character is now `CharacterID`, which is either
the online account's identifier from the server, or the game-internal
string that's been used to create the character (for NPCs and simple
characters). That allows to fix the misuse of `AccountName` by private
characters, which broke the assumption that they'd set it as their
dialog identifier instead of a unique string (their dialog name plus
old character id).
This commit is contained in:
Jean-Baptiste Emmanuel Zorg 2023-08-19 10:15:27 +02:00
parent adfd66f339
commit 81eabb25c5
10 changed files with 94 additions and 83 deletions
BondageClub
Screens
Character
Appearance
InformationSheet
Login
Wardrobe
Online/ChatRoom
Room
Pandora
Private
Scripts

View file

@ -1071,7 +1071,7 @@ function AppearancePreviewBuild(C, buildCanvases) {
*/
function AppearancePreviewCleanup() {
AppearancePreviews = [];
const previews = Character.filter(c => c.AccountName.startsWith("AppearancePreview-"));
const previews = Character.filter(c => c.CharacterID.startsWith("AppearancePreview-"));
for (const preview of previews) {
CharacterDelete(preview);
}
@ -1668,7 +1668,7 @@ function CharacterAppearanceReady(C) {
CharacterAppearanceClose();
// If the character is logged in, we sync its appearance
if (C.IsPlayer() && C.AccountName != "") {
if (C.IsPlayer() && C.CharacterID != "") {
ServerPlayerAppearanceSync();
}
if (C.IsNpc()) {

View file

@ -73,8 +73,6 @@ function InformationSheetRun() {
currentY += spacingLarge;
// Some info are not available for online players
const OnlinePlayer = C.AccountName.indexOf("Online-") >= 0;
if (C.IsPlayer()) {
let memberForLine = TextGet(C.IsBirthday() ? "Birthday" : "MemberFor") + " " + (Math.floor((CurrentTime - C.Creation) / 86400000)).toString() + " " + TextGet("Days");
DrawTextFit(memberForLine, 550, currentY, 450, (C.IsBirthday() ? "Blue" : "Black"), "Gray");
@ -83,7 +81,7 @@ function InformationSheetRun() {
let moneyLine = TextGet("Money") + " " + C.Money.toString() + " $";
DrawTextFit(moneyLine, 550, currentY, 450, "Black", "Gray");
currentY += spacing;
} else if (OnlinePlayer && C.Creation != null) {
} else if (C.IsOnline() && C.Creation != null) {
let memberForLine = TextGet("MemberFor") + " " + (Math.floor((CurrentTime - C.Creation) / 86400000)).toString() + " " + TextGet("Days");
DrawTextFit(memberForLine, 550, currentY, 450, "Black", "Gray");
currentY += spacing;
@ -110,7 +108,7 @@ function InformationSheetRun() {
currentY += spacingLarge;
// For the current player or an online player
if ((C.IsPlayer()) || OnlinePlayer) {
if ((C.IsPlayer()) || C.IsOnline()) {
// Shows the difficulty level
let Days = Math.floor((CurrentTime - (((C.Difficulty == null) || (C.Difficulty.LastChange == null) || (typeof C.Difficulty.LastChange !== "number")) ? C.Creation : C.Difficulty.LastChange)) / 86400000);
@ -152,7 +150,7 @@ function InformationSheetRun() {
DrawButton(1815, 420, 90, 90, "", "White", "Icons/FriendList.png");
DrawButton(1815, 535, 90, 90, "", "White", "Icons/Introduction.png");
DrawButton(1815, 765, 90, 90, "", "White", "Icons/Next.png");
} else if (OnlinePlayer) {
} else if (C.IsOnline()) {
DrawButton(1815, 190, 90, 90, "", "White", "Icons/Introduction.png");
DrawButton(1815, 765, 90, 90, "", "White", "Icons/Next.png");
}
@ -162,7 +160,7 @@ function InformationSheetRun() {
if (InformationSheetSecondScreen) return InformationSheetSecondScreenRun();
// For player and online characters, we show the lover list (NPC or online)
if (C.IsPlayer() || OnlinePlayer) {
if (C.IsPlayer() || C.IsOnline()) {
DrawText(TextGet("Relationships"), 1200, 125, "Black", "Gray");
if (C.Lovership.length < 1) DrawText(TextGet("Lover") + " " + TextGet("LoverNone"), 1200, 200, "Black", "Gray");
for (let L = 0; L < C.Lovership.length; L++) {
@ -204,8 +202,7 @@ function InformationSheetSecondScreenRun() {
// For current player and online characters
var C = InformationSheetSelection;
var OnlinePlayer = C.AccountName.indexOf("Online-") >= 0;
if ((C.IsPlayer()) || OnlinePlayer) {
if (C.IsPlayer() || C.IsOnline()) {
const lineHeight = 55;
// Draw the reputation section
DrawText(TextGet("Reputation"), 1000, 125, "Black", "Gray");
@ -219,7 +216,7 @@ function InformationSheetSecondScreenRun() {
// Draw the skill section
DrawText(TextGet("Skill"), 1425, 125, "Black", "Gray");
if (C.AccountName.indexOf("Online-") >= 0) {
if (C.IsOnline()) {
DrawText(TextGet("Unknown"), 1425, 200, "Black", "Gray");
} else {
let skillLine = 0;
@ -271,7 +268,7 @@ function InformationSheetClick() {
if (MouseIn(1815, 420, 90, 90)) CommonSetScreen("Character", "FriendList");
if (MouseIn(1815, 535, 90, 90)) CommonSetScreen("Character", "OnlineProfile");
if (MouseIn(1815, 765, 90, 90)) InformationSheetSecondScreen = !InformationSheetSecondScreen;
} else if (C.AccountName.indexOf("Online-") >= 0) {
} else if (C.IsOnline()) {
if (MouseIn(1815, 190, 90, 90)) CommonSetScreen("Character", "OnlineProfile");
if (MouseIn(1815, 765, 90, 90)) InformationSheetSecondScreen = !InformationSheetSecondScreen;
}

View file

@ -103,8 +103,9 @@ function LoginLoad() {
// Resets the player and other characters
Character = [];
CharacterNextId = 1;
CharacterReset(0, "Female3DCG");
CharacterNextId = 0;
// Create a blank character for our player. Its actual ID will be set when LoginResponse happens
Player = /** @type {PlayerCharacter} */ (CharacterCreate("Female3DCG", CharacterType.ONLINE, ""));
CharacterAppearanceSetDefault(Player);
CharacterLoadCSVDialog(Player, { module: "Character", screen: "Player", name: "Player" });
LoginCharacter = CharacterLoadNPC("NPC_Login");

View file

@ -383,8 +383,8 @@ function WardrobeFastLoad(C, W, Update) {
PoseRefresh(C);
CharacterLoadCanvas(C);
if (Update == null || Update) {
if (C.IsPlayer() && C.CharacterID != null) ServerPlayerAppearanceSync();
if (C.IsPlayer() || C.AccountName.indexOf("Online-") == 0) ChatRoomCharacterUpdate(C);
if (C.IsPlayer()) ServerPlayerAppearanceSync();
if (C.IsPlayer() || C.IsOnline()) ChatRoomCharacterUpdate(C);
}
}
}
@ -398,7 +398,7 @@ function WardrobeFastLoad(C, W, Update) {
*/
function WardrobeFastSave(C, W, Push) {
if (Player.Wardrobe != null) {
var AddAll = C.IsPlayer() || C.AccountName.indexOf("Wardrobe-") == 0;
var AddAll = C.IsPlayer() || C.CharacterID.indexOf("Wardrobe-") == 0;
Player.Wardrobe[W] = C.Appearance
.filter(a => a.Asset.Group.Category == "Appearance")
.filter(a => WardrobeGroupAccessible(C, a.Asset.Group, { ExcludeNonCloth: AddAll }))
@ -411,7 +411,7 @@ function WardrobeFastSave(C, W, Push) {
.map(WardrobeAssetBundle));
}
WardrobeFixLength();
if (WardrobeCharacter != null && WardrobeCharacter[W] != null && C.AccountName != WardrobeCharacter[W].AccountName) WardrobeFastLoad(WardrobeCharacter[W], W);
if (WardrobeCharacter != null && WardrobeCharacter[W] != null && C.CharacterID != WardrobeCharacter[W].CharacterID) WardrobeFastLoad(WardrobeCharacter[W], W);
if ((Push == null) || Push) ServerAccountUpdate.QueueData({ Wardrobe: CharacterCompressWardrobe(Player.Wardrobe) });
}
}
@ -491,7 +491,7 @@ function WardrobeGetExpression(C) {
function WardrobeGroupAccessible(C, Group, Options) {
// You can always edit yourself.
if (C.IsPlayer() || C.AccountName.indexOf("Wardrobe-") == 0) return true;
if (C.IsPlayer() || C.CharacterID.indexOf("Wardrobe-") == 0) return true;
// You cannot always change body cosplay
if (Group.BodyCosplay && C.OnlineSharedSettings && C.OnlineSharedSettings.BlockBodyCosplay) return false;

View file

@ -2368,8 +2368,8 @@ function ChatRoomPublishCustomAction(msg, LeaveDialog, Dictionary) {
*/
function ChatRoomCharacterUpdate(C) {
if (ChatRoomAllowCharacterUpdate) {
var data = {
ID: (C.IsPlayer()) ? Player.CharacterID : C.AccountName.replace("Online-", ""),
const data = {
ID: C.CharacterID,
ActivePose: C.ActivePose,
Appearance: ServerAppearanceBundle(C.Appearance)
};

View file

@ -363,7 +363,7 @@ function PandoraMsgBox(Text) {
* @returns {NPCCharacter} - The NPC character to return
*/
function PandoraGenerateNPC(Group, Archetype, Name, AllowItem) {
const oldNPC = Character.find(c => c.AccountName === "NPC_Pandora_" + Group + Archetype);
const oldNPC = Character.find(c => c.CharacterID === "NPC_Pandora_" + Group + Archetype);
if (oldNPC) {
CharacterDelete(oldNPC);
}

View file

@ -968,10 +968,11 @@ function PrivateLoadCharacter(data) {
if (!data.Name) return updateRequired;
const C = CharacterLoadNPC("NPC_Private_Custom", "Room", "Private");
const charID = "NPC_Private_Custom_" + PrivateCharacter.length;
const C = CharacterLoadNPC(charID, "NPC_Private_Custom", "Room", "Private");
C.Name = data.Name;
C.AccountName = "NPC_Private_Custom" + C.ID.toString();
if (data.Title != null) C.Title = data.Title;
if (data.AssetFamily != null) C.AssetFamily = data.AssetFamily;
if (data.Appearance != null) {
@ -1018,9 +1019,8 @@ function PrivateLoadCharacter(data) {
* @returns {NPCCharacter} - The new private room character.
*/
function PrivateAddCharacter(Template, Archetype, CustomData) {
var C = CharacterLoadNPC("NPC_Private_Custom");
var C = CharacterLoadNPC("NPC_Private_Custom_" + PrivateCharacter.length.toString(), "NPC_Private_Custom");
C.Name = Template.Name;
C.AccountName = "NPC_Private_Custom" + PrivateCharacter.length.toString();
C.Appearance = Template.Appearance.slice();
C.AppearanceFull = Template.Appearance.slice();
C.Love = 0;
@ -1055,12 +1055,9 @@ function PrivateGetCurrentID() {
*/
function PrivateKickOut() {
var ID = PrivateGetCurrentID();
PrivateCharacter[ID] = null;
CharacterDelete(PrivateCharacter[ID]);
PrivateCharacter.splice(ID, 1);
ServerPrivateCharacterSync();
for (let P = 1; P < PrivateCharacter.length; P++)
if (PrivateCharacter[P] != null)
PrivateCharacter[P].AccountName = "NPC_Private_Custom" + P.toString();
DialogLeave();
}
@ -1100,10 +1097,9 @@ function PrivateChange(NewCloth) {
* @returns {boolean} - Returns TRUE if the player's owner is inside her private room.
*/
function PrivateOwnerInRoom() {
for (let C = 1; C < PrivateCharacter.length; C++) {
if ((PrivateCharacter[C].AccountName == null) && (PrivateCharacter[C].Name != null) && (PrivateCharacter[C].Name == Player.Owner.replace("NPC-", ""))) return true;
if ((PrivateCharacter[C].AccountName != null) && PrivateCharacter[C].IsOwner() && (CurrentCharacter != null) && (PrivateCharacter[C].ID != CurrentCharacter.ID)) return true;
if ((PrivateCharacter[C].AccountName != null) && PrivateCharacter[C].IsOwner() && (CurrentCharacter == null)) return true;
for (const character of PrivateCharacter) {
if (character.IsPlayer()) continue;
if (character.IsOwner()) return true;
}
return false;
}
@ -1114,10 +1110,10 @@ function PrivateOwnerInRoom() {
* @returns {boolean} - Returns TRUE if the player's lover is inside her private room.
*/
function PrivateLoverInRoom(L) {
for (let C = 1; C < PrivateCharacter.length; C++) {
if ((PrivateCharacter[C].AccountName == null) && (PrivateCharacter[C].Name != null) && (Player.GetLoversNumbers()[L] == "NPC-" + PrivateCharacter[C].Name)) return true;
if ((PrivateCharacter[C].AccountName != null) && (Player.GetLoversNumbers()[L] == "NPC-" + PrivateCharacter[C].Name) && (CurrentCharacter != null) && (PrivateCharacter[C].ID != CurrentCharacter.ID)) return true;
if ((PrivateCharacter[C].AccountName != null) && (Player.GetLoversNumbers()[L] == "NPC-" + PrivateCharacter[C].Name) && (CurrentCharacter == null)) return true;
const loverInfo = Player.GetLoversNumbers()[L];
for (const character of PrivateCharacter) {
if (character.IsPlayer()) continue;
if ("NPC-" + character.Name === loverInfo) return true;
}
return false;
}

View file

@ -29,7 +29,7 @@ var AnimationDataTypes = {
* @returns {string} - Contains the name of the persistent data key.
*/
function AnimationGetDynamicDataName(C, Type, Asset) {
return (Type ? Type + "__" : "") + C.AccountName + (Asset ? "__" + Asset.Group.Name + "__" + Asset.Name : "");
return (Type ? Type + "__" : "") + C.CharacterID + (Asset ? "__" + Asset.Group.Name + "__" + Asset.Name : "");
}
/**
@ -149,7 +149,7 @@ function AnimationPurge(C, IncludeAll) {
// Clear no longer needed data (Clear the subscription, then clear the asset data)
for (const key in AnimationPersistentStorage) {
const isCharDataKey = key.startsWith(AnimationDataTypes.PersistentData + "__" + C.AccountName + "__");
const isCharDataKey = key.startsWith(AnimationDataTypes.PersistentData + "__" + C.CharacterID + "__");
if (isCharDataKey && !PossibleData.includes(key)) {
const Group = AnimationPersistentStorage[key].Group;
if (Group && Array.isArray(Group.Subscriptions)) {
@ -161,7 +161,7 @@ function AnimationPurge(C, IncludeAll) {
// Clear no longer needed cached canvases
GLDrawImageCache.forEach((img, key) => {
if (key.startsWith(AnimationDataTypes.Canvas + "__" + + C.AccountName + "__") && !PossibleCanvas.includes(key)) {
if (key.startsWith(AnimationDataTypes.Canvas + "__" + + C.CharacterID + "__") && !PossibleCanvas.includes(key)) {
GLDrawImageCache.delete(key);
}
});

View file

@ -44,21 +44,23 @@ var CharacterType = {
/**
* Loads a character into the buffer, creates it if it does not exist
* @param {number} CharacterID - ID of the character
* @param {IAssetFamily} CharacterAssetFamily - Name of the asset family of the character
* @param {CharacterType} [Type=CharacterType.ONLINE] - The character type
* @param {CharacterType} Type - The character type
* @param {string} CharacterID - An unique identifier for the character
* @returns {Character} - The newly loaded character
*/
function CharacterReset(CharacterID, CharacterAssetFamily, Type = CharacterType.ONLINE) {
function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
const id = CharacterNextId++;
// Prepares the character sheet
/** @type {Character} */
var NewCharacter = {
ID: CharacterID,
const NewCharacter = {
ID: id,
CharacterID: CharacterID,
AssetFamily: CharacterAssetFamily,
AccountName: "",
Hooks: null,
Name: "",
Type,
AssetFamily: CharacterAssetFamily,
AccountName: "",
Owner: "",
Lover: "",
Money: 0,
@ -401,7 +403,7 @@ function CharacterReset(CharacterID, CharacterAssetFamily, Type = CharacterType.
return this.Type === CharacterType.ONLINE;
},
IsNpc: function () {
return (this.Type !== CharacterType.ONLINE) && (this.Type !== CharacterType.SIMPLE);
return this.Type === CharacterType.NPC;
},
IsSimple: function () {
return this.Type === CharacterType.SIMPLE;
@ -498,15 +500,11 @@ function CharacterReset(CharacterID, CharacterAssetFamily, Type = CharacterType.
}
};
// If the character doesn't exist, we create it
var CharacterIndex = Character.findIndex(c => c.ID == CharacterID);
if (CharacterIndex == -1)
Character.push(NewCharacter);
else
Character[CharacterIndex] = NewCharacter;
// Add the character to the cache
Character.push(NewCharacter);
// Creates the inventory and default appearance
if (CharacterID == 0) {
if (id === 0) {
Player = Object.assign(
NewCharacter,
{
@ -747,18 +745,19 @@ function CharacterArchetypeClothes(C, Archetype, ForceColor) {
/**
* Loads an NPC into the character array. The appearance is randomized, and a type can be provided to dress them in a given style.
* @param {string} NPCType - Archetype of the NPC
* @param {string} CharacterID - The unique identifier for the NPC
* @param {string} [NPCType] - The dialog used by the NPC. Defaults to CharacterID if not specified.
* @returns {NPCCharacter} - The randomly generated NPC
*/
function CharacterLoadNPC(NPCType, module = CurrentModule, screen = CurrentScreen) {
function CharacterLoadNPC(CharacterID, NPCType, module = CurrentModule, screen = CurrentScreen) {
if (!NPCType) NPCType = CharacterID;
// Checks if the NPC already exists and returns it if it's the case
const duplicate = Character.find(c => c.AccountName === NPCType);
const duplicate = Character.find(c => c.CharacterID === CharacterID);
if (duplicate) return duplicate;
// Randomize the new character
CharacterReset(CharacterNextId++, "Female3DCG", CharacterType.NPC);
let C = Character[Character.length - 1];
const C = CharacterCreate("Female3DCG", CharacterType.NPC, CharacterID);
C.AccountName = NPCType;
CharacterLoadCSVDialog(C, { module: module, screen: screen, name: NPCType });
CharacterRandomName(C);
@ -777,17 +776,16 @@ function CharacterLoadNPC(NPCType, module = CurrentModule, screen = CurrentScree
/**
* Create a minimal character object
* @param {string} AccName - The account name to give to the character
* @param {string} CharacterID - The account name to give to the character
* @returns {Character} - The created character
*/
function CharacterLoadSimple(AccName) {
function CharacterLoadSimple(CharacterID) {
// Checks if the character already exists and returns it if it's the case
const duplicate = Character.find(c => c.AccountName === AccName);
const duplicate = Character.find(c => c.CharacterID === CharacterID);
if (duplicate) return duplicate;
// Create the new character
const C = CharacterReset(CharacterNextId++, "Female3DCG", CharacterType.SIMPLE);
C.AccountName = AccName;
const C = CharacterCreate("Female3DCG", CharacterType.SIMPLE, CharacterID);
// Returns the new character
return C;
@ -855,14 +853,14 @@ function CharacterOnlineRefresh(Char, data, SourceMemberNumber) {
*/
function CharacterLoadOnline(data, SourceMemberNumber) {
// Checks if the character already exists and returns it if it's the case
var Char = null;
if (data.ID.toString() == Player.CharacterID)
/** @type {Character} */
let Char = null;
// Check if the character already exists to reuse it
if (data.ID.toString() == Player.CharacterID) {
Char = Player;
else
for (let C = 0; C < Character.length; C++)
if (Character[C].AccountName == "Online-" + data.ID.toString())
Char = Character[C];
} else {
Char = Character.find(c => c.CharacterID === data.ID);
}
// Decompresses data
if (typeof data.Description === "string" && data.Description.startsWith(ONLINE_PROFILE_DESCRIPTION_COMPRESSION_MAGIC)) {
@ -893,8 +891,7 @@ function CharacterLoadOnline(data, SourceMemberNumber) {
}
// Creates the new character from the online template
CharacterReset(CharacterNextId++, "Female3DCG");
Char = Character[Character.length - 1];
Char = CharacterCreate("Female3DCG", CharacterType.ONLINE, data.ID);
Char.Name = data.Name;
Char.Lover = (data.Lover != null) ? data.Lover : "";
Char.Owner = (data.Owner != null) ? data.Owner : "";
@ -969,7 +966,7 @@ function CharacterLoadOnline(data, SourceMemberNumber) {
function CharacterDelete(C) {
if (!C) return;
const idx = Character.findIndex(c => c.AccountName === C.AccountName);
const idx = Character.findIndex(c => c.ID === C.ID);
if (idx < 0) return;
// Delete the cached dialog for that NPC.
@ -1143,13 +1140,13 @@ function CharacterChangeMoney(C, Value) {
* @param {boolean} [RefreshDialog=true] - Refreshes the character dialog
* @returns {void} - Nothing
*/
function CharacterRefresh(C, Push, RefreshDialog = true) {
function CharacterRefresh(C, Push = true, RefreshDialog = true) {
AnimationPurge(C, false);
CharacterLoadEffect(C);
PoseRefresh(C);
CharacterLoadCanvas(C);
// Label often looped through checks:
C.RunScripts = (!C.AccountName.startsWith('Online-') || !(Player.OnlineSettings && Player.OnlineSettings.DisableAnimations)) && (!Player.GhostList || Player.GhostList.indexOf(C.MemberNumber) == -1);
C.RunScripts = (!C.IsOnline() || !(Player.OnlineSettings && Player.OnlineSettings.DisableAnimations)) && (!Player.GhostList || Player.GhostList.indexOf(C.MemberNumber) == -1);
C.HasScriptedAssets = !!C.Appearance.find(CA => CA.Asset.DynamicScriptDraw);
if (C.IsPlayer()) {
@ -1158,13 +1155,13 @@ function CharacterRefresh(C, Push, RefreshDialog = true) {
C.CustomBackground = customBGItem ? customBGItem.Property.CustomBlindBackground : undefined;
}
if ((C.IsPlayer()) && (C.CharacterID != null) && ((Push == null) || (Push == true))) {
if (C.IsPlayer() && Push) {
ChatRoomRefreshChatSettings();
ServerPlayerAppearanceSync();
}
// Also refresh the current dialog menu if the refreshed character is the current character.
var Current = CharacterGetCurrent();
if (Current && C.ID == Current.ID && RefreshDialog) {
const Current = CharacterGetCurrent();
if (Current && C.ID === Current.ID && RefreshDialog) {
CharacterRefreshDialog(C);
}
}

View file

@ -1151,9 +1151,29 @@ interface PrivateCharacterData {
}
interface Character {
/**
* The character's cache slot ID in the Character array
*
* Usually meaningless, except that ID 0 is always the player,
* but please use `IsPlayer()` instead of checking that.
*/
ID: number;
/**
* The unique identifier for the character
*
* A value of `""` indicates the player before the login happens
*/
CharacterID: string;
/** The type of character: online, npc, or simple */
Type: CharacterType;
/**
* The character's account name
*
* Note that it's only meaningful for the logged in player as the server never provides account names.
* Online characters will use `"Online-"` plus their character ID, NPCs will have their dialog identifier,
* and simple characters set it to CharacterID.
*/
AccountName: string;
/**
* The character's loaded dialog info
*/
@ -1164,10 +1184,10 @@ interface Character {
* @deprecated
*/
OnlineID?: string;
/** The asset family used by the character */
AssetFamily: IAssetFamily;
Name: string;
Nickname?: string;
AssetFamily: IAssetFamily;
AccountName: string;
Owner: string;
Lover: string;
Money: number;