mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-25 17:59:34 +00:00
Adds permissions & validation for script-modified items
This commit is contained in:
parent
956ddc6fb7
commit
19de988bb0
17 changed files with 497 additions and 31 deletions
BondageClub
Assets/Female3DCG
Icons
Screens
Character
Online/ChatRoom
Scripts
|
@ -7552,7 +7552,12 @@ var AssetFemale3DCG = [
|
|||
],
|
||||
Color: ["Default", "#202020", "#808080", "#bbbbbb", "#aa8080", "#80aa80", "#8080aa", "#aaaa80", "#80aaaa", "#aa80aa", "#cc3333", "#33cc33", "#3333cc", "#cccc33", "#33cccc", "#cc33cc"]
|
||||
},
|
||||
|
||||
{
|
||||
Group: "ItemScript",
|
||||
Priority: 0,
|
||||
AllowColorize: false,
|
||||
Asset: [{Name: "Script", Visible: false}],
|
||||
},
|
||||
];
|
||||
|
||||
/** 3D Custom Girl based pose
|
||||
|
|
|
@ -209,6 +209,8 @@ interface AssetDefinition {
|
|||
|
||||
AllowEffect?: EffectName[];
|
||||
AllowBlock?: AssetGroupItemName[];
|
||||
AllowHide?: AssetGroupItemName[];
|
||||
AllowHideItem?: string[];
|
||||
AllowType?: string[];
|
||||
DefaultColor?: ItemColor;
|
||||
Opacity?: number;
|
||||
|
|
BIN
BondageClub/Icons/Question_Cyan.png
Normal file
BIN
BondageClub/Icons/Question_Cyan.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 5.4 KiB |
BIN
BondageClub/Icons/Question_Yellow.png
Normal file
BIN
BondageClub/Icons/Question_Yellow.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 5.7 KiB |
BIN
BondageClub/Icons/Scripts.png
Normal file
BIN
BondageClub/Icons/Scripts.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.2 KiB |
|
@ -386,6 +386,11 @@ function CharacterAppearanceVisible(C, AssetName, GroupName, Recursive = true) {
|
|||
}
|
||||
}
|
||||
|
||||
const scriptItem = InventoryGet(C, "ItemScript");
|
||||
if (scriptItem && scriptItem.Property && scriptItem.Property.UnHide && scriptItem.Property.UnHide.includes(GroupName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const item of C.DrawAppearance) {
|
||||
if (CharacterAppearanceItemIsHidden(item.Asset.Name, item.Asset.Group.Name)) continue;
|
||||
let HidingItem = false;
|
||||
|
|
|
@ -4,7 +4,7 @@ var PreferenceMessage = "";
|
|||
var PreferenceSafewordConfirm = false;
|
||||
var PreferenceColorPick = "";
|
||||
var PreferenceSubscreen = "";
|
||||
var PreferenceSubscreenList = ["General", "Difficulty", "Restriction", "Chat", "CensoredWords", "Audio", "Arousal", "Security", "Online", "Visibility", "Immersion", "Graphics", "Controller", "Notifications", "Gender"];
|
||||
var PreferenceSubscreenList = ["General", "Difficulty", "Restriction", "Chat", "CensoredWords", "Audio", "Arousal", "Security", "Online", "Visibility", "Immersion", "Graphics", "Controller", "Notifications", "Gender", "Scripts"];
|
||||
var PreferencePageCurrent = 1;
|
||||
var PreferenceChatColorThemeList = ["Light", "Dark", "Light2", "Dark2"];
|
||||
var PreferenceChatColorThemeIndex = 0;
|
||||
|
@ -64,6 +64,32 @@ var PreferenceGraphicsAnimationQualityList = [10000, 2000, 200, 100, 50, 0];
|
|||
var PreferenceCalibrationStage = 0;
|
||||
var PreferenceCensoredWordsList = [];
|
||||
var PreferenceCensoredWordsOffset = 0;
|
||||
const PreferenceScriptPermissionProperties = ["Hide", "Block"];
|
||||
let PreferenceScriptHelp = null;
|
||||
let PreferenceScriptTimeoutHandle = null;
|
||||
let PreferenceScriptTimer = null;
|
||||
let PreferenceScriptWarningAccepted = false;
|
||||
|
||||
const ScriptPermissionLevel = Object.freeze({
|
||||
SELF: "Self",
|
||||
OWNER: "Owner",
|
||||
LOVERS: "Lovers",
|
||||
FRIENDS: "Friends",
|
||||
WHITELIST: "Whitelist",
|
||||
PUBLIC: "Public",
|
||||
});
|
||||
|
||||
const ScriptPermissionBits = Object.freeze({
|
||||
[ScriptPermissionLevel.SELF]: 1,
|
||||
[ScriptPermissionLevel.OWNER]: 2,
|
||||
[ScriptPermissionLevel.LOVERS]: 4,
|
||||
[ScriptPermissionLevel.FRIENDS]: 8,
|
||||
[ScriptPermissionLevel.WHITELIST]: 16,
|
||||
[ScriptPermissionLevel.PUBLIC]: 32,
|
||||
});
|
||||
|
||||
const maxScriptPermission = Object.values(ScriptPermissionBits)
|
||||
.reduce((sum, bit) => sum | bit, 0);
|
||||
|
||||
/**
|
||||
* An object defining which genders a setting is active for
|
||||
|
@ -505,6 +531,19 @@ function PreferenceInitPlayer() {
|
|||
}
|
||||
if (typeof C.OnlineSharedSettings.ItemsAffectExpressions !== "boolean") C.OnlineSharedSettings.ItemsAffectExpressions = true;
|
||||
|
||||
// Set up script permissions for asset properties
|
||||
if (!C.OnlineSharedSettings.ScriptPermissions || typeof C.OnlineSharedSettings.ScriptPermissions !== "object") C.OnlineSharedSettings.ScriptPermissions = {
|
||||
Hide: {permission: 0},
|
||||
Block: {permission: 0},
|
||||
};
|
||||
const ScriptPermissions = C.OnlineSharedSettings.ScriptPermissions;
|
||||
for (const property of PreferenceScriptPermissionProperties) {
|
||||
if (!ScriptPermissions[property] || typeof ScriptPermissions[property] !== "object") ScriptPermissions[property] = {};
|
||||
if (typeof ScriptPermissions[property].permission !== "number" || ScriptPermissions[property].permission < 0 || ScriptPermissions[property].permission > maxScriptPermission) {
|
||||
ScriptPermissions[property].permission = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Graphical settings
|
||||
// @ts-ignore: Individual properties validated separately
|
||||
if (!C.GraphicsSettings) C.GraphicsSettings = {};
|
||||
|
@ -874,11 +913,18 @@ function PreferenceClick() {
|
|||
|
||||
// Open the selected subscreen
|
||||
for (let A = 0; A < PreferenceSubscreenList.length; A++)
|
||||
if (MouseIn(500 + 500 * Math.floor(A / 7), 160 + 110 * (A % 7), 400, 90)) {
|
||||
if (MouseIn(500 + 420 * Math.floor(A / 7), 160 + 110 * (A % 7), 400, 90)) {
|
||||
if (typeof window["PreferenceSubscreen" + PreferenceSubscreenList[A] + "Load"] === "function")
|
||||
CommonDynamicFunction("PreferenceSubscreen" + PreferenceSubscreenList[A] + "Load()");
|
||||
PreferenceSubscreen = PreferenceSubscreenList[A];
|
||||
PreferencePageCurrent = 1;
|
||||
if (PreferenceSubscreenList[A] === "Scripts") {
|
||||
PreferenceScriptTimer = Date.now() + 5000;
|
||||
PreferenceScriptTimeoutHandle = setTimeout(() => {
|
||||
PreferenceScriptTimer = null;
|
||||
PreferenceScriptTimeoutHandle = null;
|
||||
}, 5000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -1549,6 +1595,94 @@ function PreferenceGenderDrawSetting(Left, Top, Text, Setting) {
|
|||
DrawCheckbox(Left + 950 + 155, Top - 32, 64, 64, "", Setting.Male);
|
||||
}
|
||||
|
||||
function PreferenceSubscreenScriptsRun() {
|
||||
const helpColour = "#ff8";
|
||||
|
||||
// Character, exit & help buttons
|
||||
DrawCharacter(Player, 50, 50, 0.9);
|
||||
DrawButton(1815, 75, 90, 90, "", "White", "Icons/Exit.png");
|
||||
|
||||
MainCanvas.textAlign = "left";
|
||||
|
||||
DrawText(TextGet("ScriptsPreferences"), 500, 125, "Black", "Gray");
|
||||
|
||||
if (!PreferenceScriptWarningAccepted) {
|
||||
PreferenceScriptsDrawWarningScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Slightly wacky x-coordinate because DrawTextWrap assumes text is centered (500 - 1300/2 = -150)
|
||||
DrawTextWrap(TextGet("ScriptsExplanation"), -150, 150, 1300, 120, "Black");
|
||||
DrawButton(1815, 190, 90, 90, "", PreferenceScriptHelp === "global" ? helpColour : "White", "Icons/Question.png");
|
||||
|
||||
/** @type {ScriptPermissionLevel[]} */
|
||||
const permissions = Object.values(ScriptPermissionLevel);
|
||||
|
||||
// Can be used to page properties in the future
|
||||
/** @type {ScriptPermissionProperty[]} */
|
||||
const propertiesPage = PreferenceScriptPermissionProperties;
|
||||
|
||||
MainCanvas.textAlign = "center";
|
||||
for (const [i, property] of propertiesPage.entries()) {
|
||||
DrawTextFit(TextGet(`ScriptsPermissionProperty${property}`), 850 + 300 * i, 320, 124, "Black", "Gray");
|
||||
const helpHover = MouseIn(720 + 300 * i, 296, 48, 48);
|
||||
const iconColour = PreferenceScriptHelp === property ? "_Yellow" : helpHover ? "_Cyan" : "";
|
||||
DrawImageResize(`Icons/Question${iconColour}.png`, 720 + 300 * i, 296, 48, 48);
|
||||
MainCanvas.moveTo(700 + 300 * i, 270);
|
||||
MainCanvas.strokeStyle = "rgba(0, 0, 0, 0.5)";
|
||||
MainCanvas.lineTo(700 + 300 * i, 270 + (permissions.length + 1) * 90);
|
||||
MainCanvas.stroke();
|
||||
for (const [j, permissionLevel] of permissions.entries()) {
|
||||
const disabled = permissionLevel !== ScriptPermissionLevel.PUBLIC
|
||||
&& permissionLevel !== ScriptPermissionLevel.SELF
|
||||
&& (
|
||||
ValidationHasScriptPermission(Player, property, ScriptPermissionLevel.PUBLIC)
|
||||
|| !ValidationHasScriptPermission(Player, property, ScriptPermissionLevel.SELF)
|
||||
);
|
||||
DrawCheckbox(816 + 300 * i, 386 + 90 * j, 64, 64, "", ValidationHasScriptPermission(Player, property, permissionLevel), disabled);
|
||||
}
|
||||
}
|
||||
MainCanvas.textAlign = "left";
|
||||
|
||||
MainCanvas.moveTo(500, 370);
|
||||
MainCanvas.strokeStyle = "rgba(0, 0, 0, 0.5)";
|
||||
MainCanvas.lineTo(700 + propertiesPage.length * 300, 370);
|
||||
MainCanvas.stroke();
|
||||
|
||||
for (const [i, permissionName] of permissions.entries()) {
|
||||
DrawText(TextGet(`ScriptsPermissionLevel${permissionName}`), 500, 410 + 90 * i, "Black", "Gray");
|
||||
}
|
||||
|
||||
if (PreferenceScriptHelp === "global") {
|
||||
const helpHeight = 90 + 90 * permissions.length;
|
||||
DrawRect(500, 270, 1300, helpHeight, helpColour);
|
||||
MainCanvas.strokeStyle = "Black";
|
||||
MainCanvas.strokeRect(500, 270, 1300, helpHeight);
|
||||
DrawTextWrap(TextGet("ScriptsHelpGlobal"), -110, 270, 1260, helpHeight, "Black");
|
||||
MainCanvas.textAlign = "center";
|
||||
return;
|
||||
} else if (PreferenceScriptHelp) {
|
||||
const helpHeight = 90 * permissions.length
|
||||
DrawRect(500, 370, 1300, helpHeight, helpColour);
|
||||
MainCanvas.strokeStyle = "Black";
|
||||
MainCanvas.strokeRect(500, 370, 1300, helpHeight);
|
||||
DrawTextWrap(TextGet(`ScriptsHelp${PreferenceScriptHelp}`), -110, 370, 1260, helpHeight, "Black");
|
||||
}
|
||||
|
||||
MainCanvas.textAlign = "center";
|
||||
}
|
||||
|
||||
function PreferenceScriptsDrawWarningScreen() {
|
||||
DrawText(TextGet("ScriptsWarningTitle"), 500, 220, "#c80800", "Gray");
|
||||
DrawTextWrap(TextGet("ScriptsWarning"), -140, 250, 1280, 240, "Black");
|
||||
|
||||
MainCanvas.textAlign = "center";
|
||||
const disabled = PreferenceScriptTimer != null;
|
||||
const seconds = PreferenceScriptTimer ? Math.ceil((PreferenceScriptTimer - Date.now()) / 1000) : null;
|
||||
DrawButton(500, 500, 400, 64, `${TextGet("ScriptsWarningAccept")}${seconds ? ` (${seconds})` : ""}`, disabled ? "rgba(0, 0, 0, 0.12)" : "White", null, null, disabled);
|
||||
MainCanvas.textAlign = "left";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click events for the audio preference settings. Redirected from the main Click function.
|
||||
* @returns {void} - Nothing
|
||||
|
@ -1991,6 +2125,101 @@ function PreferenceGenderClickSetting(Left, Top, Setting, MutuallyExclusive) {
|
|||
}
|
||||
}
|
||||
|
||||
function PreferenceSubscreenScriptsClick() {
|
||||
if (MouseIn(1815, 75, 90, 90)) {
|
||||
PreferenceSubscreenScriptsExitClick();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PreferenceScriptWarningAccepted) {
|
||||
PreferenceSubscreenScriptsWarningClick();
|
||||
return;
|
||||
}
|
||||
|
||||
if (PreferenceScriptHelp === "global") {
|
||||
PreferenceScriptHelp = null;
|
||||
return;
|
||||
} else if (MouseIn(1815, 190, 90, 90)) {
|
||||
PreferenceScriptHelp = "global";
|
||||
return;
|
||||
}
|
||||
|
||||
const ScriptPermissions = Player.OnlineSharedSettings.ScriptPermissions;
|
||||
|
||||
/** @type {ScriptPermissionLevel[]} */
|
||||
const permissions = Object.values(ScriptPermissionLevel);
|
||||
|
||||
// Can be used to page properties in the future
|
||||
/** @type {ScriptPermissionProperty[]} */
|
||||
const propertiesPage = PreferenceScriptPermissionProperties;
|
||||
|
||||
for (const [i, property] of propertiesPage.entries()) {
|
||||
if (MouseIn(720 + 300 * i, 296, 48, 48)) {
|
||||
if (PreferenceScriptHelp === property) {
|
||||
PreferenceScriptHelp = null;
|
||||
return;
|
||||
} else {
|
||||
PreferenceScriptHelp = property;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [j, permissionLevel] of permissions.entries()) {
|
||||
if (MouseIn(816 + 300 * i, 386 + 90 * j, 64, 64)) {
|
||||
const levelSelf = permissionLevel === ScriptPermissionLevel.SELF;
|
||||
const levelPublic = permissionLevel === ScriptPermissionLevel.PUBLIC;
|
||||
const selfAllowed = ValidationHasScriptPermission(Player, property, ScriptPermissionLevel.SELF);
|
||||
const publicAllowed = ValidationHasScriptPermission(Player, property, ScriptPermissionLevel.PUBLIC);
|
||||
if (levelSelf) {
|
||||
ScriptPermissions[property].permission = selfAllowed ? 0 : ScriptPermissionBits[permissionLevel];
|
||||
} else if (levelPublic) {
|
||||
ScriptPermissions[property].permission = publicAllowed ? 0 : maxScriptPermission;
|
||||
} else if (selfAllowed && !publicAllowed) {
|
||||
ScriptPermissions[property].permission ^= ScriptPermissionBits[permissionLevel];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PreferenceScriptHelp = null;
|
||||
}
|
||||
|
||||
function PreferenceSubscreenScriptsExitClick() {
|
||||
PreferenceSubscreen = "";
|
||||
if (PreferenceScriptTimeoutHandle != null) {
|
||||
clearTimeout(PreferenceScriptTimeoutHandle);
|
||||
PreferenceScriptTimeoutHandle = null;
|
||||
}
|
||||
PreferenceScriptTimer = null;
|
||||
const scriptItem = InventoryGet(Player, "ItemScript");
|
||||
if (scriptItem) {
|
||||
const params = ValidationCreateDiffParams(Player, Player.MemberNumber);
|
||||
const { result, valid } = ValidationResolveScriptDiff(null, scriptItem, params);
|
||||
if (!valid) {
|
||||
console.info("Cleaning script item after permissions modification");
|
||||
if (result) {
|
||||
Player.Appearance = Player.Appearance.map((item) => {
|
||||
return item.Asset.Group.Name === "ItemScript" ? result : item;
|
||||
});
|
||||
} else {
|
||||
InventoryRemove(Player, "ItemScript", false);
|
||||
}
|
||||
if (ServerPlayerIsInChatRoom()) {
|
||||
ChatRoomCharacterUpdate(Player);
|
||||
} else {
|
||||
CharacterRefresh(Player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PreferenceSubscreenScriptsWarningClick() {
|
||||
if (PreferenceScriptTimer == null && MouseIn(500, 500, 400, 64)) {
|
||||
PreferenceScriptWarningAccepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click events for the visibility settings of a player. Redirected from the main Click function.
|
||||
* @returns {void} - Nothing
|
||||
|
|
|
@ -13,6 +13,7 @@ ImmersionPreferences,- Immersion Preferences -
|
|||
ControllerPreferences,- Controller Preferences -
|
||||
NotificationsPreferences,- Notification Preferences -
|
||||
GenderPreferences,- Gender Preferences -
|
||||
ScriptsPreferences,- Script Permission Preferences -
|
||||
HomepageGeneral,General
|
||||
HomepageDifficulty,Difficulty
|
||||
HomepageRestriction,Restriction
|
||||
|
@ -28,6 +29,7 @@ HomepageGraphics,Graphics
|
|||
HomepageController,Controller
|
||||
HomepageNotifications,Notifications
|
||||
HomepageGender,Gender
|
||||
HomepageScripts,Scripts
|
||||
DifficultyTitle,The difficulty mode enforces BDSM restrictions in multi-player only. Click for details.
|
||||
DifficultyLevel0,Roleplay
|
||||
Difficulty0Text0,Roleplay - This mode is fully open to customise your experience.
|
||||
|
@ -342,3 +344,18 @@ CensorLevel1,Replace the phrase by ***
|
|||
CensorLevel2,Hide the phrase or room
|
||||
CensorWord,Censored words:
|
||||
CensorAdd,Add
|
||||
ScriptsWarningTitle,"WARNING!"
|
||||
ScriptsWarning,"Changing the settings on this page can allow people to modify your items and appearance using scripts in ways that are not possible in the core game. If you don't want this, or if you don't know what you're doing, don't touch these settings!"
|
||||
ScriptsWarningAccept,"I Accept"
|
||||
ScriptsExplanation,"Specify who can modify your worn items using scripts, and what they're allowed to do."
|
||||
ScriptsHelpGlobal,"To reduce issues and maintain a baseline level of visual coherence, the Bondage Club contains a series of measures which prevents people from modifying your items in ways that might cause clipping or graphical issues. On this screen you can choose to allow certain people to bypass those restrictions via custom scripts or addons. This can allow for a higher degree of visual customisation for items, but be aware that it can also open the door to severe visual glitches, including invisible items or characters, so use these options at your own risk. To find out more about the properties that you can assign permissions for and what each might do and the associated risks, click the '?' icon next to each one. Be aware that setting permissions to public means that anyone will be able to modify that feature, even trolls, so please use this with caution. Remember that you can remove unwanted script effects at any time by removing all permissions for that effect (including 'Self')."
|
||||
ScriptsHelpHide,"Permits scripts to override the default item-hiding behaviour of Bondage Club. This can enhance your game by allowing item combinations that wouldn't normally be possible, or by allowing items to be hidden to achieve a particular look that doesn't necessarily reflect exactly what you're wearing. However, it can also be used to create severe visual glitches, including making items or even your whole character invisible. Use with caution!"
|
||||
ScriptsHelpBlock,"Permits scripts to override the default item-blocking behaviour of Bondage Club. This is the functionality that prevents you from equipping items on others, or can prevent sexual activities on blocked zones. Allowing scripts to add custom blocking behaviour can allow for additional restriction, preventing players from removing worn items or adding new ones in a given slot, but can also result in items that you can't remove yourself without the help of a script (or by removing this permission)."
|
||||
ScriptsPermissionLevelSelf,Self
|
||||
ScriptsPermissionLevelOwner,Owner
|
||||
ScriptsPermissionLevelLovers,Lovers
|
||||
ScriptsPermissionLevelFriends,Friends
|
||||
ScriptsPermissionLevelWhitelist,Whitelist
|
||||
ScriptsPermissionLevelPublic,Public
|
||||
ScriptsPermissionPropertyHide,Hide
|
||||
ScriptsPermissionPropertyBlock,Block
|
||||
|
|
|
|
@ -3489,7 +3489,7 @@ function ChatRoomSyncItem(data) {
|
|||
const previousItem = InventoryGet(ChatRoomCharacter[C], data.Item.Group);
|
||||
const newItem = ServerBundledItemToAppearanceItem(ChatRoomCharacter[C].AssetFamily, data.Item);
|
||||
|
||||
let { item, valid } = ValidationResolveAppearanceDiff(previousItem, newItem, updateParams);
|
||||
let { item, valid } = ValidationResolveAppearanceDiff(data.Item.Group, previousItem, newItem, updateParams);
|
||||
|
||||
ChatRoomAllowCharacterUpdate = false;
|
||||
|
||||
|
|
|
@ -155,6 +155,8 @@ function AssetAdd(Group, AssetDef, ExtendedConfig) {
|
|||
AllowEffect: AssetDef.AllowEffect,
|
||||
AllowBlock: AssetDef.AllowBlock,
|
||||
AllowType: AssetDef.AllowType,
|
||||
AllowHide: AssetDef.AllowHide,
|
||||
AllowHideItem: AssetDef.AllowHideItem,
|
||||
DefaultColor: AssetDef.DefaultColor,
|
||||
Opacity: AssetParseOpacity(AssetDef.Opacity),
|
||||
MinOpacity: typeof AssetDef.MinOpacity === "number" ? AssetParseOpacity(AssetDef.MinOpacity) : 1,
|
||||
|
|
|
@ -1821,7 +1821,15 @@ function CharacterClearOwnership(C) {
|
|||
}
|
||||
|
||||
C.Appearance = C.Appearance.filter(item => !item.Asset.OwnerOnly);
|
||||
C.Appearance.forEach(item => ValidationSanitizeProperties(C, item));
|
||||
C.Appearance.forEach(item => ValidationSanitizeProperties(C, item, {
|
||||
C,
|
||||
fromSelf: true,
|
||||
fromOwner: false,
|
||||
fromLover: false,
|
||||
fromFriend: false,
|
||||
fromWhitelist: false,
|
||||
sourceMemberNumber: C.MemberNumber,
|
||||
}, null));
|
||||
CharacterRefresh(C);
|
||||
}
|
||||
|
||||
|
|
|
@ -549,10 +549,11 @@ function CommonColorsEqual(C1, C2) {
|
|||
* order, as determined by === comparison
|
||||
* @param {*[]} a1 - The first array to compare
|
||||
* @param {*[]} a2 - The second array to compare
|
||||
* @param {boolean} [ignoreOrder] - Whether to ignore item order when considering equality
|
||||
* @returns {boolean} - TRUE if both arrays have the same length and contain the same items in the same order, FALSE otherwise
|
||||
*/
|
||||
function CommonArraysEqual(a1, a2) {
|
||||
return a1.length === a2.length && a1.every((item, i) => item === a2[i]);
|
||||
function CommonArraysEqual(a1, a2, ignoreOrder = false) {
|
||||
return a1.length === a2.length && a1.every((item, i) => ignoreOrder ? a2.includes(item) : item === a2[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -954,4 +955,4 @@ function CommonCensor(S) {
|
|||
// Returns the mashed string
|
||||
return S;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -875,11 +875,15 @@ function ModularItemGenerateValidationProperties(data) {
|
|||
asset.AllowEffect = Array.isArray(asset.AllowEffect) ? asset.AllowEffect.slice() : [];
|
||||
CommonArrayConcatDedupe(asset.AllowEffect, asset.Effect);
|
||||
asset.AllowBlock = Array.isArray(asset.Block) ? asset.Block.slice() : [];
|
||||
asset.AllowHide = Array.isArray(asset.Hide) ? asset.Hide.slice() : [];
|
||||
asset.AllowHideItem = Array.isArray(asset.HideItem) ? asset.HideItem.slice() : [];
|
||||
for (const module of modules) {
|
||||
for (const {Property} of module.Options) {
|
||||
if (Property) {
|
||||
if (Property.Effect) CommonArrayConcatDedupe(asset.AllowEffect, Property.Effect);
|
||||
if (Property.Block) CommonArrayConcatDedupe(asset.AllowBlock, Property.Block);
|
||||
if (Property.Hide) CommonArrayConcatDedupe(asset.AllowHide, Property.Hide);
|
||||
if (Property.HideItem) CommonArrayConcatDedupe(asset.AllowHideItem, Property.HideItem);
|
||||
if (Property.Tint && Array.isArray(Property.Tint) && Property.Tint.length > 0) asset.AllowTint = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -346,7 +346,7 @@ function ServerPlayerRelationsSync() {
|
|||
*/
|
||||
function ServerAppearanceBundle(Appearance) {
|
||||
var Bundle = [];
|
||||
for (let A = 0; A < Appearance.length; A++)
|
||||
for (let A = 0; A < Appearance.length; A++)
|
||||
if (Appearance[A].Asset != null) {
|
||||
var N = {};
|
||||
N.Group = Appearance[A].Asset.Group.Name;
|
||||
|
@ -383,9 +383,9 @@ function ServerAppearanceLoadFromBundle(C, AssetFamily, Bundle, SourceMemberNumb
|
|||
const updateParams = ValidationCreateDiffParams(C, SourceMemberNumber);
|
||||
|
||||
let { appearance, updateValid } = Object.keys(appearanceDiffs)
|
||||
.reduce(({ appearance, updateValid }, key) => {
|
||||
const diff = appearanceDiffs[key];
|
||||
const { item, valid } = ValidationResolveAppearanceDiff(diff[0], diff[1], updateParams);
|
||||
.reduce(({ appearance, updateValid }, groupName) => {
|
||||
const diff = appearanceDiffs[groupName];
|
||||
const { item, valid } = ValidationResolveAppearanceDiff(groupName, diff[0], diff[1], updateParams);
|
||||
if (item) appearance.push(item);
|
||||
updateValid = updateValid && valid;
|
||||
return { appearance, updateValid };
|
||||
|
|
|
@ -64,6 +64,7 @@ function TypedItemRegister(asset, config) {
|
|||
TypedItemGenerateAllowType(data);
|
||||
TypedItemGenerateAllowEffect(data);
|
||||
TypedItemGenerateAllowBlock(data);
|
||||
TypedItemGenerateAllowHide(data);
|
||||
TypedItemGenerateAllowTint(data);
|
||||
TypedItemGenerateAllowLockType(data);
|
||||
TypedItemRegisterSubscreens(asset, config);
|
||||
|
@ -299,6 +300,20 @@ function TypedItemGenerateAllowBlock({asset, options}) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an asset's AllowHide & AllowHideItem properties based on its typed item data.
|
||||
* @param {TypedItemData} data - The typed item's data
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function TypedItemGenerateAllowHide({asset, options}) {
|
||||
asset.AllowHide = Array.isArray(asset.Hide) ? asset.Hide.slice() : [];
|
||||
asset.AllowHideItem = Array.isArray(asset.HideItem) ? asset.HideItem.slice() : [];
|
||||
for (const option of options) {
|
||||
CommonArrayConcatDedupe(asset.AllowHide, option.Property.Hide);
|
||||
CommonArrayConcatDedupe(asset.AllowHideItem, option.Property.HideItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an asset's AllowTint property based on its typed item data.
|
||||
* @param {TypedItemData} data - The typed item's data
|
||||
|
|
20
BondageClub/Scripts/Typedef.d.ts
vendored
20
BondageClub/Scripts/Typedef.d.ts
vendored
|
@ -229,7 +229,7 @@ type AssetGroupItemName =
|
|||
'ItemMouth3' | 'ItemNeck' | 'ItemNeckAccessories' | 'ItemNeckRestraints' |
|
||||
'ItemNipples' | 'ItemNipplesPiercings' | 'ItemNose' | 'ItemPelvis' |
|
||||
'ItemTorso' | 'ItemTorso2'| 'ItemVulva' | 'ItemVulvaPiercings' |
|
||||
'ItemHandheld' |
|
||||
'ItemHandheld' | 'ItemScript' |
|
||||
|
||||
'ItemHidden' /* TODO: investigate, not a real group */
|
||||
;
|
||||
|
@ -749,6 +749,8 @@ interface Asset {
|
|||
RemoveItemOnRemove: { Name: string; Group: string; Type?: string; }[];
|
||||
AllowEffect?: EffectName[];
|
||||
AllowBlock?: AssetGroupItemName[];
|
||||
AllowHide?: AssetGroupItemName[];
|
||||
AllowHideItem?: string[];
|
||||
AllowType?: string[];
|
||||
DefaultColor?: ItemColor;
|
||||
Opacity: number;
|
||||
|
@ -975,6 +977,16 @@ interface ItemPermissions {
|
|||
Type?: string | null;
|
||||
}
|
||||
|
||||
interface ScriptPermission {
|
||||
permission: number;
|
||||
}
|
||||
|
||||
type ScriptPermissionProperty = "Hide" | "Block";
|
||||
|
||||
type ScriptPermissionLevel = "Self" | "Owner" | "Lovers" | "Friends" | "Whitelist" | "Public";
|
||||
|
||||
type ScriptPermissions = Record<ScriptPermissionProperty, ScriptPermission>;
|
||||
|
||||
interface Character {
|
||||
ID: number;
|
||||
/** Only on `Player` */
|
||||
|
@ -1133,6 +1145,7 @@ interface Character {
|
|||
DisablePickingLocksOnSelf: boolean;
|
||||
GameVersion: string;
|
||||
ItemsAffectExpressions: boolean;
|
||||
ScriptPermissions: ScriptPermissions;
|
||||
};
|
||||
Game?: {
|
||||
LARP?: GameLARPParameters,
|
||||
|
@ -1782,6 +1795,9 @@ interface ItemPropertiesCustom {
|
|||
Door?: boolean;
|
||||
/** Whether the kennel has padding */
|
||||
Padding?: boolean;
|
||||
|
||||
/** Only available as overrides on the script item */
|
||||
UnHide?: AssetGroupName[];
|
||||
}
|
||||
|
||||
interface ItemProperties extends ItemPropertiesBase, AssetDefinitionProperties, ItemPropertiesCustom { }
|
||||
|
@ -2375,6 +2391,8 @@ interface AppearanceUpdateParameters {
|
|||
* lover-only items)
|
||||
*/
|
||||
fromLover: boolean;
|
||||
/** The script permission levels that the source player has with respect to the receiver */
|
||||
permissions: ScriptPermissionLevel[];
|
||||
/** The member number of the source player */
|
||||
sourceMemberNumber: number;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,14 @@ const ValidationAllLockProperties = ValidationNonModifiableLockProperties
|
|||
.concat(ValidationTimerLockProperties)
|
||||
.concat(["MemberNumberListKeys"]);
|
||||
const ValidationModifiableProperties = ValidationAllLockProperties.concat(["Effect", "Expression"]);
|
||||
const ValidationScriptableProperties = ["Hide", "HideItem", "UnHide", "Block"];
|
||||
/** @type {Partial<Record<keyof ItemProperties, ScriptPermissionProperty>>} */
|
||||
const ValidationPropertyPermissions = {
|
||||
Hide: "Hide",
|
||||
HideItem: "Hide",
|
||||
UnHide: "Hide",
|
||||
Block: "Block",
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the appearance update parameters used to validate an appearance diff, based on the provided target character
|
||||
|
@ -38,23 +46,41 @@ function ValidationCreateDiffParams(C, sourceMemberNumber) {
|
|||
}
|
||||
fromLover = ownerCanUseLoverLocks;
|
||||
}
|
||||
return { C, fromSelf, fromOwner, fromLover, sourceMemberNumber };
|
||||
// We can't know the details about other peoples' friendlist/whitelist, so assume an update is safe - their
|
||||
// validation will correct it if not.
|
||||
const fromFriend = C.IsPlayer() ? C.FriendList.includes(sourceMemberNumber) : true;
|
||||
const fromWhitelist = C.IsPlayer() ? C.WhiteList.includes(sourceMemberNumber) : true;
|
||||
|
||||
/** @type {ScriptPermissionLevel[]} */
|
||||
const permissions = [
|
||||
fromSelf && ScriptPermissionLevel.SELF,
|
||||
fromOwner && !fromSelf && ScriptPermissionLevel.OWNER,
|
||||
fromLover && !fromOwner && !fromSelf && ScriptPermissionLevel.LOVERS,
|
||||
fromFriend && ScriptPermissionLevel.FRIENDS,
|
||||
fromWhitelist && ScriptPermissionLevel.WHITELIST,
|
||||
ScriptPermissionLevel.PUBLIC,
|
||||
].filter(Boolean);
|
||||
|
||||
return { C, fromSelf, fromOwner, fromLover, permissions, sourceMemberNumber };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an appearance diff based on the previous item, new item, and the appearance update parameters provided.
|
||||
* Returns an {@link ItemDiffResolution} object containing the final appearance item and a valid flag indicating
|
||||
* whether or not the new item had to be modified/rolled back.
|
||||
* @param {string} groupName - The name of the group for the appearance diff
|
||||
* @param {Item|null} previousItem - The previous item that the target character had equipped (or null if none)
|
||||
* @param {Item|null} newItem - The new item to equip (may be identical to the previous item, or null if removing)
|
||||
* @param {AppearanceUpdateParameters} params - The appearance update parameters that apply to the diff
|
||||
* @returns {ItemDiffResolution} - The diff resolution - a wrapper object containing the final item and a flag
|
||||
* indicating whether or not the change was valid.
|
||||
*/
|
||||
function ValidationResolveAppearanceDiff(previousItem, newItem, params) {
|
||||
function ValidationResolveAppearanceDiff(groupName, previousItem, newItem, params) {
|
||||
let result;
|
||||
if (!previousItem && !newItem) {
|
||||
result = { item: previousItem, valid: true };
|
||||
} else if (groupName === "ItemScript") {
|
||||
result = ValidationResolveScriptDiff(previousItem, newItem, params);
|
||||
} else if (!previousItem) {
|
||||
result = ValidationResolveAddDiff(newItem, params);
|
||||
} else if (!newItem) {
|
||||
|
@ -66,7 +92,105 @@ function ValidationResolveAppearanceDiff(previousItem, newItem, params) {
|
|||
}
|
||||
let { item, valid } = result;
|
||||
// If the diff has resolved to an item, sanitize its properties
|
||||
if (item) valid = !ValidationSanitizeProperties(params.C, item) && valid;
|
||||
if (item && groupName !== "ItemScript") valid = !ValidationSanitizeProperties(params.C, item) && valid;
|
||||
return { item, valid };
|
||||
}
|
||||
|
||||
function ValidationHasArrayPropertyBeenModified(oldArray, newArray) {
|
||||
if (!oldArray && !newArray) {
|
||||
return false;
|
||||
} else if (Array.isArray(oldArray) && Array.isArray(newArray) && CommonArraysEqual(oldArray, newArray, true)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Item|null} previousItem
|
||||
* @param {Item|null} newItem
|
||||
* @param {AppearanceUpdateParameters} params -
|
||||
* @return {ItemDiffResolution}
|
||||
*/
|
||||
function ValidationResolveScriptDiff(previousItem, newItem, {C, permissions, sourceMemberNumber}) {
|
||||
let valid = true;
|
||||
|
||||
/** @type {Record<ScriptPermissionProperty, boolean>} */
|
||||
const propertyPermissions = {
|
||||
Block: ValidationHasSomeScriptPermission(C, "Block", permissions),
|
||||
Hide: ValidationHasSomeScriptPermission(C, "Hide", permissions),
|
||||
};
|
||||
|
||||
const previousProperty = (previousItem && previousItem.Property) || {};
|
||||
const newProperty = (newItem && newItem.Property) || {};
|
||||
const sanitizedProperty = {};
|
||||
|
||||
/** @type {(keyof ItemProperties)[]} */
|
||||
const unpermittedPropertyModifications = [];
|
||||
|
||||
// Check for property modifications that are not permitted
|
||||
if (!propertyPermissions.Hide && ValidationHasArrayPropertyBeenModified(previousProperty.Hide, newProperty.Hide)) {
|
||||
unpermittedPropertyModifications.push("Hide");
|
||||
}
|
||||
if (!propertyPermissions.Hide && ValidationHasArrayPropertyBeenModified(previousProperty.UnHide, newProperty.UnHide)) {
|
||||
unpermittedPropertyModifications.push("UnHide");
|
||||
}
|
||||
if (!propertyPermissions.Hide && ValidationHasArrayPropertyBeenModified(previousProperty.HideItem, newProperty.HideItem)) {
|
||||
unpermittedPropertyModifications.push("HideItem");
|
||||
}
|
||||
if (!propertyPermissions.Block && ValidationHasArrayPropertyBeenModified(previousProperty.Block, newProperty.Block)) {
|
||||
unpermittedPropertyModifications.push("Block");
|
||||
}
|
||||
|
||||
let item = newItem;
|
||||
Object.assign(sanitizedProperty, newProperty);
|
||||
const propertyNames = Object.keys(sanitizedProperty);
|
||||
|
||||
// Strip out any invalid properties
|
||||
for (const propertyName of propertyNames) {
|
||||
if (!ValidationScriptableProperties.includes(propertyName)) {
|
||||
console.warn(`Stripping invalid scripted property: ${propertyName}`);
|
||||
valid = false;
|
||||
delete sanitizedProperty[propertyName];
|
||||
}
|
||||
}
|
||||
|
||||
if (unpermittedPropertyModifications.length > 0) {
|
||||
// If there were unpermitted property modifications...
|
||||
valid = false;
|
||||
console.warn(`Reverting invalid changes to scripted properties: ${JSON.stringify(unpermittedPropertyModifications)}`);
|
||||
|
||||
if (Object.keys(sanitizedProperty).length === unpermittedPropertyModifications.length) {
|
||||
// If all remaining property changes are not permitted, we can revert the whole change
|
||||
item = previousItem;
|
||||
} else {
|
||||
// Otherwise if there were unpermitted property modifications, revert them
|
||||
for (const propertyName of unpermittedPropertyModifications) {
|
||||
sanitizedProperty[propertyName] = previousProperty[propertyName];
|
||||
}
|
||||
item = Object.assign(InventoryItemCreate(C, "ItemScript", "Script"), {Property: sanitizedProperty});
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: if the player does not have permission to modify a property, then delete that property
|
||||
if (permissions.includes(ScriptPermissionLevel.SELF)) {
|
||||
for (const propertyName of ValidationScriptableProperties) {
|
||||
const propertyPermission = ValidationPropertyPermissions[propertyName];
|
||||
if (sanitizedProperty[propertyName] != null && propertyPermission && !ValidationHasScriptPermission(C, propertyPermission, ScriptPermissionLevel.SELF)) {
|
||||
delete sanitizedProperty[propertyName];
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item && item.Property) {
|
||||
// Finally, sanitize item properties
|
||||
valid = !ValidationSanitizeStringArray(item.Property, "Hide") && valid;
|
||||
valid = !ValidationSanitizeStringArray(item.Property, "UnHide") && valid;
|
||||
valid = !ValidationSanitizeStringArray(item.Property, "HideItem") && valid;
|
||||
valid = !ValidationSanitizeStringArray(item.Property, "Block") && valid;
|
||||
}
|
||||
|
||||
return { item, valid };
|
||||
}
|
||||
|
||||
|
@ -517,7 +641,9 @@ function ValidationSanitizeProperties(C, item) {
|
|||
|
||||
// Sanitize various properties
|
||||
let changed = ValidationSanitizeEffects(C, item);
|
||||
changed = ValidationSanitizeBlocks(C, item) || changed;
|
||||
changed = ValidationSanitizeAllowedPropertyArray(C, item, "Block", "AllowBlock") || changed;
|
||||
changed = ValidationSanitizeAllowedPropertyArray(C, item, "Hide", "AllowHide") || changed;
|
||||
changed = ValidationSanitizeAllowedPropertyArray(C, item, "HideItem", "AllowHideItem") || changed;
|
||||
changed = ValidationSanitizeSetPose(C, item) || changed;
|
||||
changed = ValidationSanitizeStringArray(property, "Hide") || changed;
|
||||
|
||||
|
@ -738,26 +864,30 @@ function ValidationSanitizeLock(C, item) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the `Block` array on an item's Property object, if present. This ensures that it is a valid array of
|
||||
* strings, and that each item in the array is present in the either the asset's `Block` or `AllowBlock` array.
|
||||
* Sanitizes an array on an item's Property object, if present. This ensures that it is a valid array of
|
||||
* strings, and that each item in the array is present in the either the asset's corresponding property array or the
|
||||
* "allow" array for that item.
|
||||
* @param {Character} C - The character on whom the item is equipped
|
||||
* @param {Item} item - The item whose `Block` property should be sanitized
|
||||
* @returns {boolean} - TRUE if the item's `Block` property was modified as part of the sanitization process
|
||||
* @param {Item} item - The item whose property should be sanitized
|
||||
* @param {keyof ItemProperties & keyof Asset} propertyName - The name of the property
|
||||
* @param {keyof Asset} allowPropertyName - The name of the property corresponding to the list of allowed property
|
||||
* values on the asset
|
||||
* @returns {boolean} - TRUE if the item's property was modified as part of the sanitization process
|
||||
* (indicating it was not a valid string array, or that invalid entries were present), FALSE otherwise
|
||||
*/
|
||||
function ValidationSanitizeBlocks(C, item) {
|
||||
function ValidationSanitizeAllowedPropertyArray(C, item, propertyName, allowPropertyName) {
|
||||
const property = item.Property;
|
||||
let changed = ValidationSanitizeStringArray(property, "Block");
|
||||
let changed = ValidationSanitizeStringArray(property, propertyName);
|
||||
|
||||
// If there is no Block array, no further sanitization is needed
|
||||
if (!Array.isArray(property.Block)) return changed;
|
||||
// If there is no property array, no further sanitization is needed
|
||||
if (!Array.isArray(property[propertyName])) return changed;
|
||||
|
||||
const assetBlock = item.Asset.Block || [];
|
||||
const allowBlock = item.Asset.AllowBlock || [];
|
||||
// Any Block entry must be included in the AllowBlock list to be permitted
|
||||
property.Block = property.Block.filter((block) => {
|
||||
if (!assetBlock.includes(block) && !allowBlock.includes(block)) {
|
||||
console.warn(`Filtering out invalid Block entry on ${item.Asset.Name}:`, block);
|
||||
const assetProperty = item.Asset[property] || [];
|
||||
const allowProperty = item.Asset[allowPropertyName] || [];
|
||||
// Any entry must be included in the allow list to be permitted
|
||||
property[propertyName] = property[propertyName].filter((propertyValue) => {
|
||||
if (!assetProperty.includes(propertyValue) && !allowProperty.includes(propertyValue)) {
|
||||
console.warn(`Filtering out invalid ${propertyName} entry on ${item.Asset.Name}:`, propertyValue);
|
||||
changed = true;
|
||||
return false;
|
||||
} else return true;
|
||||
|
@ -1045,3 +1175,33 @@ function ValidationGetPrerequisiteBlockingGroups(item, appearance) {
|
|||
|
||||
return blockingGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a character permits the given permission level to modify the given item property. For example,
|
||||
* passing `Player` in as the character, `"Block"` in as the property and `ScriptPermissionLevel.FRIENDS` in as the
|
||||
* permission level will check whether the player's friends are permitted to freely modify `"Block"` properties on the
|
||||
* player's worn items without strict validation.
|
||||
* @param {Character} character - The character to check
|
||||
* @param {ScriptPermissionProperty} property - The name of the property to check
|
||||
* @param {ScriptPermissionLevel} permissionLevel - The permission level to check
|
||||
* @returns {boolean} TRUE if the character permits modifications to the provided property
|
||||
*/
|
||||
function ValidationHasScriptPermission(character, property, permissionLevel) {
|
||||
const permissionBit = ScriptPermissionBits[permissionLevel];
|
||||
const propertyPermissions = character && character.OnlineSharedSettings && character.OnlineSharedSettings.ScriptPermissions && character.OnlineSharedSettings.ScriptPermissions[property];
|
||||
if (!permissionBit || !propertyPermissions || !propertyPermissions.permission) {
|
||||
return false;
|
||||
}
|
||||
return !!(propertyPermissions.permission & permissionBit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a character permits any of the given permission levels to modify the given item property.
|
||||
* @param {Character} character - The character to check
|
||||
* @param {ScriptPermissionProperty} property - The name of the property to check
|
||||
* @param {ScriptPermissionLevel[]} permissionLevels - The permission levels to check
|
||||
* @returns {boolean} TRUE if the character permits modifications to the provided property
|
||||
*/
|
||||
function ValidationHasSomeScriptPermission(character, property, permissionLevels) {
|
||||
return permissionLevels.some((permissionLevel) => ValidationHasScriptPermission(character, property, permissionLevel));
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue