Adds permissions & validation for script-modified items

This commit is contained in:
Ellie 2022-11-27 00:17:49 +00:00
parent 956ddc6fb7
commit 19de988bb0
No known key found for this signature in database
GPG key ID: 5E7C12A4193CFC2B
17 changed files with 497 additions and 31 deletions

View file

@ -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

View file

@ -209,6 +209,8 @@ interface AssetDefinition {
AllowEffect?: EffectName[];
AllowBlock?: AssetGroupItemName[];
AllowHide?: AssetGroupItemName[];
AllowHideItem?: string[];
AllowType?: string[];
DefaultColor?: ItemColor;
Opacity?: number;

Binary file not shown.

After

(image error) Size: 5.4 KiB

Binary file not shown.

After

(image error) Size: 5.7 KiB

Binary file not shown.

After

(image error) Size: 1.2 KiB

View file

@ -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;

View file

@ -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

View file

@ -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

1 Preferences - Preferences -
13 ControllerPreferences - Controller Preferences -
14 NotificationsPreferences - Notification Preferences -
15 GenderPreferences - Gender Preferences -
16 ScriptsPreferences - Script Permission Preferences -
17 HomepageGeneral General
18 HomepageDifficulty Difficulty
19 HomepageRestriction Restriction
29 HomepageController Controller
30 HomepageNotifications Notifications
31 HomepageGender Gender
32 HomepageScripts Scripts
33 DifficultyTitle The difficulty mode enforces BDSM restrictions in multi-player only. Click for details.
34 DifficultyLevel0 Roleplay
35 Difficulty0Text0 Roleplay - This mode is fully open to customise your experience.
344 CensorLevel2 Hide the phrase or room
345 CensorWord Censored words:
346 CensorAdd Add
347 ScriptsWarningTitle WARNING!
348 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!
349 ScriptsWarningAccept I Accept
350 ScriptsExplanation Specify who can modify your worn items using scripts, and what they're allowed to do.
351 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').
352 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!
353 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).
354 ScriptsPermissionLevelSelf Self
355 ScriptsPermissionLevelOwner Owner
356 ScriptsPermissionLevelLovers Lovers
357 ScriptsPermissionLevelFriends Friends
358 ScriptsPermissionLevelWhitelist Whitelist
359 ScriptsPermissionLevelPublic Public
360 ScriptsPermissionPropertyHide Hide
361 ScriptsPermissionPropertyBlock Block

View file

@ -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;

View file

@ -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,

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 };

View file

@ -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

View file

@ -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;
}

View file

@ -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));
}