Use a PRNG to build the lock pin combination

As we were saving the lockpick combination onto the item, but not
sending the update, a concurrent update from the lockpicked character
(which wouldn't have had the property set as it wouldn't have opened the
lockpicking UI) caused a reset of the lockpicking combination,
apparently randomly.

Fix the bug by building the lockpicking combination using the PRNG, 
seeded with a combination of the lockpicked character's ID, the group
being lockpicked, and the difficulty of the lock, which itself is a
depends on the lockpicker's state (skill and blocking, mostly).

This allows removing the validation handling for that property, and
makes the lock-pick combination unique per lockpicker.
This commit is contained in:
Jean-Baptiste Emmanuel Zorg 2022-04-22 22:56:39 +02:00
parent 5fd8d9eb9d
commit 18e736d1c6
4 changed files with 46 additions and 61 deletions
BondageClub

View file

@ -5679,8 +5679,8 @@ var AssetFemale3DCG = [
Zone: [[10, 0, 90, 200]],
Asset: [
{ Name: "MetalPadlock", Value: 15, Time: 10, Wear: false, Effect: [], IsLock: true},
{ Name: "IntricatePadlock", Value: 50, Time: 30, Wear: false, Effect: [], IsLock: true, PickDifficulty: 7, ExclusiveUnlock: true, AllowType: ["LockPickSeed"]},
{ Name: "HighSecurityPadlock", Value: 60, Time: 10, Wear: false, Effect: [], IsLock: true, PickDifficulty: 10, ExclusiveUnlock: true, AllowType: ["LockPickSeed"]},
{ Name: "IntricatePadlock", Value: 50, Time: 30, Wear: false, Effect: [], IsLock: true, PickDifficulty: 7, ExclusiveUnlock: true, },
{ Name: "HighSecurityPadlock", Value: 60, Time: 10, Wear: false, Effect: [], IsLock: true, PickDifficulty: 10, ExclusiveUnlock: true, },
{ Name: "TimerPadlock", Value: 80, Wear: false, Effect: [], IsLock: true, MaxTimer: 300, RemoveTimer: 300 },
{ Name: "CombinationPadlock", Value: 100, Random: false, Wear: false, Effect: [], IsLock: true, AllowType: ["CombinationNumber"]},
{ Name: "PasswordPadlock", Value: 100, BuyGroup: "PasswordPadlock", Random: false, Wear: false, Effect: [], IsLock: true, AllowType: ["Password", "Hint", "LockSet"]},

View file

@ -1197,6 +1197,39 @@ function StruggleLockPickGetDifficulty(C, Item) {
}
}
/**
* Generate the lock pin data
*
* @param {object} Lock - the lockpicking data. See StruggleLockPickGetDifficulty.
* @param {Character} Character
* @param {string} Group
* @returns {number[]}
*/
function StruggleLockPickGenerate(Lock, Character, Group) {
if (!Lock || !Character || !Group)
return null;
// Generate a PRNG for shuffling pins around
let seed = String(Character.IsNpc() ? Character.Name : Character.MemberNumber) + Group + Lock.Difficulty + Lock.NumPins;
const hash = xmur3(seed);
const rand = sfc32(hash(), hash(), hash(), hash());
let Order = [];
for (let P = 0; P < Lock.NumPins; P++) {
Order.push(P);
}
// Randomize array in-place using Durstenfeld shuffle algorithm
// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
for (var i = Order.length - 1; i > 0; i--) {
var j = Math.floor(rand() * (i + 1));
var temp = Order[i];
Order[i] = Order[j];
Order[j] = temp;
}
return Order;
}
/**
* Starts the dialog progress bar for picking a lock
* First the challenge level is calculated based on the base lock difficulty, the skill of the rigger and the escapee
@ -1216,12 +1249,6 @@ function StruggleLockPickProgressStart(C, Item) {
if (!Lock) return;
// Prepares the progress bar and timer
StruggleLockPickOrder = [];
StruggleLockPickSet = [];
StruggleLockPickSetFalse = [];
StruggleLockPickOffset = [];
StruggleLockPickOffsetTarget = [];
StruggleLockPickImpossiblePins = [];
StruggleLockPickProgressItem = Item;
StruggleLockPickProgressOperation = StruggleLockPickProgressGetOperation(C, Item);
StruggleLockPickProgressChallenge = Lock.Difficulty;
@ -1235,39 +1262,22 @@ function StruggleLockPickProgressStart(C, Item) {
StruggleLockPickFailTime = 0;
DialogMenuButtonBuild(C);
// Generate the combination
StruggleLockPickOrder = [];
StruggleLockPickSet = [];
StruggleLockPickSetFalse = [];
StruggleLockPickOffset = [];
StruggleLockPickOffsetTarget = [];
StruggleLockPickImpossiblePins = [];
StruggleLockPickOrder = StruggleLockPickGenerate(Lock, C, C.FocusGroup.Name);
for (let P = 0; P < Lock.NumPins; P++) {
StruggleLockPickOrder.push(P);
StruggleLockPickSet.push(false);
StruggleLockPickSetFalse.push(false);
StruggleLockPickOffset.push(0);
StruggleLockPickOffsetTarget.push(0);
}
/* Randomize array in-place using Durstenfeld shuffle algorithm */
// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
for (var i = StruggleLockPickOrder.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = StruggleLockPickOrder[i];
StruggleLockPickOrder[i] = StruggleLockPickOrder[j];
StruggleLockPickOrder[j] = temp;
}
// Initialize persistent pins
if ((Item.Property == null)) Item.Property = {};
if (Item.Property != null)
if ((Item.Property.LockPickSeed == null) || (typeof Item.Property.LockPickSeed != "string")) {
Item.Property.LockPickSeed = CommonConvertArrayToString(StruggleLockPickOrder);
StruggleLockPickTotalTries = 0;
} else {
var conv = CommonConvertStringToArray(Item.Property.LockPickSeed);
for (let PP = 0; PP < conv.length; PP++) {
if (typeof conv[PP] != "number") {
Item.Property.LockPickSeed = CommonConvertArrayToString(StruggleLockPickOrder);
conv = StruggleLockPickOrder;
break;
}
}
StruggleLockPickOrder = conv;
}
if (Lock.Difficulty > 6 && Lock.Impossible) {
// if picking is impossible, then some pins will never set
@ -1275,5 +1285,4 @@ function StruggleLockPickProgressStart(C, Item) {
if (Lock.NumPins >= 6) StruggleLockPickImpossiblePins.push(StruggleLockPickOrder[StruggleLockPickOrder.length-2]);
if (Lock.NumPins >= 8) StruggleLockPickImpossiblePins.push(StruggleLockPickOrder[StruggleLockPickOrder.length-3]);
}
}

View file

@ -1072,7 +1072,6 @@ interface ItemPropertiesCustom {
RemoveTimer?: unknown;
Password?: string;
LockPickSeed?: string;
CombinationNumber?: string;
LockMemberNumber?: number | string;
MemberNumber?: number;

View file

@ -8,7 +8,7 @@ const ValidationDefaultPassword = "UNLOCK";
const ValidationRemoveTimerToleranceMs = 5000;
const ValidationNonModifiableLockProperties = ["LockedBy", "LockMemberNumber"];
const ValidationRestrictedLockProperties = [
"EnableRandomInput", "RemoveItem", "ShowTimer", "CombinationNumber", "Password", "Hint", "LockSet", "LockPickSeed",
"EnableRandomInput", "RemoveItem", "ShowTimer", "CombinationNumber", "Password", "Hint", "LockSet",
];
const ValidationTimerLockProperties = ["MemberNumberList", "RemoveTimer"];
const ValidationAllLockProperties = ValidationNonModifiableLockProperties
@ -670,29 +670,6 @@ function ValidationSanitizeLock(C, item) {
changed = true;
}
// Sanitize lockpicking seed
if (typeof property.LockPickSeed === "string") {
const seed = CommonConvertStringToArray(property.LockPickSeed);
if (!seed.length) {
console.warn("Deleting invalid lockpicking seed: ", property.LockPickSeed);
delete property.LockPickSeed;
changed = true;
} else {
// Check that every number from 0 up to the seed length is included in the seed
for (let i = 0; i < seed.length; i++) {
if (!seed.includes(i)) {
console.warn("Deleting invalid lockpicking seed: ", property.LockPickSeed);
delete property.LockPickSeed;
changed = true;
break;
}
}
}
} else if (property.LockPickSeed != null) {
delete property.LockPickSeed;
changed = true;
}
// Sanitize lock password
if (typeof property.Password === "string") {
if (!ValidationPasswordRegex.test(property.Password)) {