I’m trying to recreate the stores from the base game and am currently struggling at getting the synchronized scenes for the robbing part to align. Besides that I currently replace the till before the scene, which feels like the wrong approach but the existing till is the wrong prop for “mp_am_holdup”?
The problem:
Here is the code snippet of relevance:
private async Task StartRobbery(Store store)
{
store.isBeingRobbed = true;
if (await ReplaceTill(new Vector3(store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z)))
{
store.hasReplacedTill = true;
}
Ped clerk = GetClerk();
if (store == null || clerk == null) { throw new ArgumentNullException("Could not get store or coordinates!"); };
Model bagModel = new Model("p_poly_bag_01_s");
bagModel.Request();
while (!bagModel.IsLoaded) { await Delay(0); }
Entity bag = await World.CreateProp(bagModel, clerk.Position - new Vector3(0, 0, 1.0f), false, false);
Entity till = new Prop(GetClosestObjectOfType(store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z, 1.0f, 892543765, false, false, false));
RequestAnimDict("mp_am_hold_up");
while (!HasAnimDictLoaded("mp_am_hold_up")) { await Delay(0); }
Vector3 initAnimPos = GetAnimInitialOffsetPosition("mp_am_hold_up", "holdup_victim_20s", store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z, 0, 0, store.AnimationOrigin.W, 0, 2);
Vector3 initAnimHeading = GetAnimInitialOffsetRotation("mp_am_hold_up", "holdup_victim_20s", store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z, 0, 0, store.AnimationOrigin.W, 0, 2);
clerk.Task.ClearAllImmediately();
int netScene = NetworkCreateSynchronisedScene(initAnimPos.X, initAnimPos.Y, initAnimPos.Z, initAnimHeading.X, initAnimHeading.Y, initAnimHeading.Z, 1, false, false, 1, 0, 1);
NetworkAddPedToSynchronisedScene(clerk.Handle, netScene, "mp_am_hold_up", "holdup_victim_20s", 4, -8, 16, 1, 2, 0);
NetworkAddEntityToSynchronisedScene(till.Handle, netScene, "mp_am_holdup", "holdup_victim_20s_till", 8, -8, 0);
NetworkAddEntityToSynchronisedScene(bag.Handle, netScene, "mp_am_holdup", "holdup_victim_20s_bag", 8, -8, 1);
NetworkStartSynchronisedScene(netScene);
int localId = -1;
while (localId == -1)
{
await Delay(0);
localId = NetworkGetLocalSceneFromNetworkId(netScene);
}
while (GetSynchronizedScenePhase(localId) <= 0.9)
{
await Delay(0);
}
Pickup bagPickup = await World.CreatePickup(PickupType.MoneyPaperBag, bag.Position, bagModel, 1000);
bag.Delete();
NetworkStopSynchronisedScene(netScene);
clerk.Task.ReactAndFlee(Game.PlayerPed);
store.isBeingRobbed = false;
store.hasBeenRobbed = true;
}
Here is the code in its entirety:
using System;
using System.Threading.Tasks;
using CitizenFX.Core;
using static CitizenFX.Core.Native.API;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Stores.Client
{
public class Stores : BaseScript
{
private List<Store> stores = new List<Store>();
private List<int> interiorIds = new List<int>();
private Dictionary<int, int> IdxPedHandles = new Dictionary<int, int>();
private int storeNum = 1;
public Stores()
{
Tick += OnTick;
RegisterExports();
}
//----------------------------------- Store Loading / Initialization -------------------
private async Task LoadStores()
{
try
{
string storeString = LoadResourceFile("stores", "stores.json");
if (string.IsNullOrEmpty(storeString))
{
throw new ArgumentException("stores.json not found or is empty");
}
var data = JsonConvert.DeserializeObject<StoreData>(storeString);
if (data.Stores == null)
{
throw new ArgumentException("No 'stores' in JSON data");
}
foreach (var store in data.Stores)
{
var parsedStore = new Store
{
Location = new Vector3(store.Location.X, store.Location.Y, store.Location.Z),
AnimationOrigin = new Vector4(store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z, store.AnimationOrigin.W),
PedOrigin = new Vector4(store.PedOrigin.X, store.PedOrigin.Y, store.PedOrigin.Z, store.PedOrigin.W),
defaultName = store.defaultName,
Name = store.defaultName ? "Store" : store.Name
};
AddStore(parsedStore);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error loading stores: {ex.Message}");
}
}
private void ValidateStore(Store store)
{
if (float.IsNaN(store.Location.X) || float.IsNaN(store.Location.Y) || float.IsNaN(store.Location.Z))
{
throw new ArgumentException("Invalid store location");
}
if (store.Name == null && !store.defaultName)
{
throw new ArgumentException("Store name is required if not default used");
}
}
private int AddStore(Store store)
{
ValidateStore(store);
store.Idx = storeNum++;
stores.Add(store);
return store.Idx;
}
public async void StartStores()
{
await LoadStores();
CreateStoreBlips();
CreateStoreClerks();
}
private class Store
{
public Vector3 Location { get; set; }
public Vector4 AnimationOrigin { get; set; }
public Vector4 PedOrigin { get; set; }
public bool defaultName { get; set; }
public string Name { get; set; }
public bool isBeingRobbed { get; set; }
public bool hasBeenRobbed { get; set; }
public bool hasReplacedTill { get; set; }
public int Idx { get; set; }
}
private class StoreData
{
public List<Store> Stores { get; set; }
}
//----------------------------------- Store Blips / Store Clerks --------------------------------------
private void CreateStoreBlips()
{
foreach (Store store in stores)
{
Blip blip = World.CreateBlip(new Vector3(store.Location.X, store.Location.Y, store.Location.Z));
blip.Sprite = BlipSprite.Store;
blip.Scale = 1f;
blip.IsShortRange = true;
blip.Name = store.defaultName ? "Store" : store.Name;
interiorIds.Add(GetInteriorAtCoords(store.Location.X, store.Location.Y, store.Location.Z));
}
}
private async void CreateStoreClerks()
{
foreach (Store store in stores)
{
Ped storePed = await World.CreatePed(PedHash.ShopKeep01, GetOffsetFromCoordAndHeadingInWorldCoords(store.PedOrigin.X, store.PedOrigin.Y, store.PedOrigin.Z, store.PedOrigin.W, 0f, -0.1f, 0f), store.PedOrigin.W);
storePed.BlockPermanentEvents = true;
storePed.IsPersistent = true;
IdxPedHandles.Add(store.Idx, storePed.Handle);
}
}
[EventHandler("createStoreBlips")]
//----------------------------------- Store Logic --------------------------------------2
private async Task OnTick()
{
await IsStoreBeingRobbedThisFrame();
}
private Store GetStore()
{
foreach (var store in stores)
{
if (GetInteriorAtCoords(store.Location.X, store.Location.Y, store.Location.Z) == GetInteriorFromEntity(Game.PlayerPed.Handle))
{
return store;
}
}
return null;
}
private Ped GetClerk()
{
if (IsPlayerInStore())
{
Store store = GetStore();
if (IdxPedHandles.TryGetValue(store.Idx, out int pedHandle))
{
return new Ped(pedHandle);
}
}
return null;
}
private bool IsPlayerArmed()
{
return !(new Player(Game.Player.Handle).Character.Weapons.Current.Hash == WeaponHash.Unarmed);
}
private bool IsPlayerInStore()
{
if (interiorIds.Contains(GetInteriorFromEntity(Game.PlayerPed.Handle)))
{
return true;
}
return false;
}
private bool IsClerkAimedAt()
{
Ped clerk = GetClerk();
if (clerk != null)
{
return IsPlayerFreeAimingAtEntity(Game.Player.Handle, clerk.Handle);
}
return false;
}
private async Task<bool> ReplaceTill(Vector3 animOrigin)
{
int entity = GetClosestObjectOfType(animOrigin.X, animOrigin.Y, animOrigin.Z, 1.0f, 303280717, false, false, false);
if (DoesEntityExist(entity))
{
Entity till;
Model tillModel = new Model("p_till_01_s");
till = new Prop(entity);
Vector3 position = till.Position;
Vector3 rotation = till.Rotation;
till.Delete();
tillModel.Request();
while (!tillModel.IsLoaded)
{
await Delay(0);
}
till = await World.CreateProp(tillModel, new Vector3(position.X, position.Y, position.Z - 0.12f), false, false); // -0.12f is a hardcoded value to make sure the till is placed correctly
till.Rotation = rotation;
return true;
}
return false;
}
private async Task IsStoreBeingRobbedThisFrame()
{
if (IsClerkAimedAt() && IsPlayerArmed())
{
Store store = GetStore();
if (!store.isBeingRobbed && !store.hasBeenRobbed)
{
store.isBeingRobbed = true;
await StartRobbery(store);
}
}
}
private async Task StartRobbery(Store store)
{
store.isBeingRobbed = true;
if (await ReplaceTill(new Vector3(store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z)))
{
store.hasReplacedTill = true;
}
Ped clerk = GetClerk();
if (store == null || clerk == null) { throw new ArgumentNullException("Could not get store or coordinates!"); };
Model bagModel = new Model("p_poly_bag_01_s");
bagModel.Request();
while (!bagModel.IsLoaded) { await Delay(0); }
Entity bag = await World.CreateProp(bagModel, clerk.Position - new Vector3(0, 0, 1.0f), false, false);
Entity till = new Prop(GetClosestObjectOfType(store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z, 1.0f, 892543765, false, false, false));
RequestAnimDict("mp_am_hold_up");
while (!HasAnimDictLoaded("mp_am_hold_up")) { await Delay(0); }
Vector3 initAnimPos = GetAnimInitialOffsetPosition("mp_am_hold_up", "holdup_victim_20s", store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z, 0, 0, store.AnimationOrigin.W, 0, 2);
Vector3 initAnimHeading = GetAnimInitialOffsetRotation("mp_am_hold_up", "holdup_victim_20s", store.AnimationOrigin.X, store.AnimationOrigin.Y, store.AnimationOrigin.Z, 0, 0, store.AnimationOrigin.W, 0, 2);
clerk.Task.ClearAllImmediately();
int netScene = NetworkCreateSynchronisedScene(initAnimPos.X, initAnimPos.Y, initAnimPos.Z, initAnimHeading.X, initAnimHeading.Y, initAnimHeading.Z, 1, false, false, 1, 0, 1);
NetworkAddPedToSynchronisedScene(clerk.Handle, netScene, "mp_am_hold_up", "holdup_victim_20s", 4, -8, 16, 1, 2, 0);
NetworkAddEntityToSynchronisedScene(till.Handle, netScene, "mp_am_holdup", "holdup_victim_20s_till", 8, -8, 0);
NetworkAddEntityToSynchronisedScene(bag.Handle, netScene, "mp_am_holdup", "holdup_victim_20s_bag", 8, -8, 1);
NetworkStartSynchronisedScene(netScene);
int localId = -1;
while (localId == -1)
{
await Delay(0);
localId = NetworkGetLocalSceneFromNetworkId(netScene);
}
while (GetSynchronizedScenePhase(localId) <= 0.9)
{
await Delay(0);
}
Pickup bagPickup = await World.CreatePickup(PickupType.MoneyPaperBag, bag.Position, bagModel, 1000);
bag.Delete();
NetworkStopSynchronisedScene(netScene);
clerk.Task.ReactAndFlee(Game.PlayerPed);
store.isBeingRobbed = false;
store.hasBeenRobbed = true;
}
//----------------------------------- Exports ------------------------------------------
private void RegisterExports()
{
Exports.Add("createStores", new Action(StartStores));
}
}
}
If needed here’s also the .json:
{
"stores": [
{
"Location": {
"X": -711.6182,
"Y": -915.7814,
"Z": 19.2156
},
"AnimationOrigin": {
"X": -706.6382,
"Y": -913.6887,
"Z": 19.32968,
"W": 90.00
},
"defaultName": true,
"Name": "1"
},
{
"Location": {
"X": -52.2386,
"Y": -1755.9198,
"Z": 29.421
},
"AnimationOrigin": {
"X": -47.19872,
"Y": -1757.67,
"Z": 29.53509,
"W": 50.00
},
"defaultName": true,
"Name": "2"
},
{
"Location": {
"X": 1159.5792,
"Y": -325.6018,
"Z": 69.2052
},
"AnimationOrigin": {
"X": 1164.206,
"Y": -322.8901,
"Z": 69.31918,
"W": 100.00
},
"defaultName": true,
"Name": "3"
},
{
"Location": {
"X": 1699.6136,
"Y": 4928.5015,
"Z": 42.0637
},
"AnimationOrigin": {
"X": 1698.307,
"Y": 4923.371,
"Z": 42.17774,
"W": 325.00
},
"defaultName": true,
"Name": "4"
},
{
"Location": {
"X": -1822.7968,
"Y": 788.9506,
"Z": 138.1877
},
"AnimationOrigin": {
"X": -1820.465,
"Y": 793.8166,
"Z": 138.2128,
"W": 132.50
},
"defaultName": true,
"Name": "5"
},
{
"Location": {
"X": 1166.4976,
"Y": 2704.6975,
"Z": 38.1695
},
"AnimationOrigin": {
"X": 1165.958,
"Y": 2710.201,
"Z": 38.26217,
"W": 178.85
},
"defaultName": true,
"Name": "6"
},
{
"Location": {
"X": -2972.6047,
"Y": 390.827,
"Z": 15.0551
},
"AnimationOrigin": {
"X": -2967.027,
"Y": 390.9038,
"Z": 15.14779,
"W": 85.25
},
"defaultName": true,
"Name": "7"
},
{
"Location": {
"X": -1226.0824,
"Y": -903.3079,
"Z": 12.3382
},
"AnimationOrigin": {
"X": -1222.331,
"Y": -907.8233,
"Z": 12.43084,
"W": 32.70
},
"defaultName": true,
"Name": "8"
},
{
"Location": {
"X": 1140.3356,
"Y": -980.9802,
"Z": 46.4276
},
"AnimationOrigin": {
"X": 1134.812,
"Y": -982.3615,
"Z": 46.52031,
"W": 276.70
},
"defaultName": true,
"Name": "9"
},
{
"Location": {
"X": -1490.2791,
"Y": -382.8716,
"Z": 40.1752
},
"AnimationOrigin": {
"X": -1486.673,
"Y": -378.4638,
"Z": 40.26789,
"W": 133.75
},
"defaultName": true,
"Name": "10"
},
{
"Location": {
"X": -3240.8726,
"Y": 1004.6915,
"Z": 12.8307
},
"AnimationOrigin": {
"X": -3244.573,
"Y": 1000.658,
"Z": 12.94538,
"W": 355.00
},
"defaultName": true,
"Name": "11"
},
{
"Location": {
"X": -3039.5024,
"Y": 589.4763,
"Z": 7.9089
},
"AnimationOrigin": {
"X": -3041.357,
"Y": 584.2665,
"Z": 8.023597,
"W": 17.75
},
"defaultName": true,
"Name": "12"
},
{
"Location": {
"X": 544.2382,
"Y": 2671.7927,
"Z": 42.1565
},
"AnimationOrigin": {
"X": 548.9015,
"Y": 2668.941,
"Z": 42.27118,
"W": 97.50
},
"defaultName": true,
"Name": "13"
},
{
"Location": {
"X": 2558.3494,
"Y": 385.5382,
"Z": 108.6229
},
"AnimationOrigin": {
"X": 2554.875,
"Y": 381.3858,
"Z": 108.7376,
"W": 357.75
},
"defaultName": true,
"Name": "14"
},
{
"Location": {
"X": 2681.396,
"Y": 3283.1792,
"Z": 55.2411
},
"AnimationOrigin": {
"X": 2676.212,
"Y": 3280.969,
"Z": 55.35582,
"W": 330.85
},
"defaultName": true,
"Name": "15"
},
{
"Location": {
"X": 1731.7385,
"Y": 6412.0957,
"Z": 35.0372
},
"AnimationOrigin": {
"X": 1729.329,
"Y": 6417.123,
"Z": 35.15191,
"W": 243.65
},
"defaultName": true,
"Name": "16"
},
{
"Location": {
"X": 1964.8656,
"Y": 3741.5476,
"Z": 32.3437
},
"AnimationOrigin": {
"X": 1959.323,
"Y": 3742.289,
"Z": 32.45843,
"W": 300.00
},
"defaultName": true,
"Name": "17"
},
{
"Location": {
"X": 29.3065,
"Y": -1348.3275,
"Z": 29.497
},
"AnimationOrigin": {
"X": 24.94562,
"Y": -1344.954,
"Z": 29.6117,
"W": 270.00
},
"defaultName": true,
"Name": "18"
},
{
"Location": {
"X": 376.8703,
"Y": 324.1652,
"Z": 103.5665
},
"AnimationOrigin": {
"X": 373.5954,
"Y": 328.5892,
"Z": 103.6811,
"W": 255.85
},
"defaultName": true,
"Name": "19"
},
{
"Location": {
"X": 1393.825,
"Y": 3600.7297,
"Z": 34.981
},
"AnimationOrigin": {
"X": 1393.072,
"Y": 3605.959,
"Z": 35.11384,
"W": 200.00
},
"defaultName": true,
"Name": "20"
}
]
}
I’d really appreciate help of any kind. Even if you are only familiar with the lua equivalents please do respond!!!