diff --git a/code/components/citizen-server-impl/include/state/ServerGameState.h b/code/components/citizen-server-impl/include/state/ServerGameState.h index d68ec1ea21..e9b42d5d56 100644 --- a/code/components/citizen-server-impl/include/state/ServerGameState.h +++ b/code/components/citizen-server-impl/include/state/ServerGameState.h @@ -547,18 +547,18 @@ struct CTrainGameStateDataNodeData bool isEngine; bool isCaboose; - bool unk12; + bool isMissionTrain; bool direction; - bool unk14; + bool hasPassengerCarriages; bool renderDerailed; // 2372 { - bool unk198; //unk198 - bool highPrecisionBlending; //unk224 - bool hasNoThreadId; //unk199 + bool allowRemovalByPopulation; + bool highPrecisionBlending; + bool shouldStopAtStations; // } bool forceDoorsOpen; @@ -648,17 +648,6 @@ enum ePopType POPTYPE_TOOL }; -//TODO: Probably should be moved out of fx::sync namespace -struct scrVector -{ - float x; - int pad; - float y; - int pad2; - float z; - int pad3; -}; - struct SyncTreeBase { public: @@ -1124,6 +1113,9 @@ struct ScriptGuid } }; +auto GetTrain(fx::ServerGameState* sgs, uint32_t objectId) -> fx::sync::SyncEntityPtr; +auto GetNextTrain(fx::ServerGameState* sgs, const fx::sync::SyncEntityPtr& entity) -> fx::sync::SyncEntityPtr; + struct EntityCreationState { // TODO: allow resending in case the target client disappears diff --git a/code/components/citizen-server-impl/include/state/SyncTrees_Five.h b/code/components/citizen-server-impl/include/state/SyncTrees_Five.h index 94b56d6738..72aaa07cd8 100644 --- a/code/components/citizen-server-impl/include/state/SyncTrees_Five.h +++ b/code/components/citizen-server-impl/include/state/SyncTrees_Five.h @@ -2627,22 +2627,23 @@ struct CTrainGameStateDataNode : GenericSerializeDataNode fx::sync::SyncEntityPtr + +auto GetTrain(fx::ServerGameState* sgs, uint32_t objectId) -> fx::sync::SyncEntityPtr { if (objectId != 0) { @@ -854,7 +855,7 @@ static auto GetTrain(fx::ServerGameState* sgs, uint32_t objectId) -> fx::sync::S return {}; }; -static auto GetNextTrain(fx::ServerGameState* sgs, const fx::sync::SyncEntityPtr& entity) -> fx::sync::SyncEntityPtr +auto GetNextTrain(fx::ServerGameState* sgs, const fx::sync::SyncEntityPtr& entity) -> fx::sync::SyncEntityPtr { if (auto trainState = entity->syncTree->GetTrainState()) { diff --git a/code/components/citizen-server-impl/src/state/ServerSetters.cpp b/code/components/citizen-server-impl/src/state/ServerSetters.cpp index 268cde134b..24df56eca6 100644 --- a/code/components/citizen-server-impl/src/state/ServerSetters.cpp +++ b/code/components/citizen-server-impl/src/state/ServerSetters.cpp @@ -7,11 +7,13 @@ #include +#include #include #include #include #include +#include namespace fx { @@ -156,6 +158,64 @@ std::shared_ptr MakeVehicle(uint32_t model, float posX, floa return tree; } +template +std::shared_ptr MakeTrain(uint32_t model, float posX, float posY, float posZ, float heading, uint32_t resourceHash, bool isEngine, bool direction, bool stopAtStation, float speed, uint32_t trackid, uint32_t trainConfigIndex) +{ + auto tree = std::make_shared(); + + SetupNode(tree, [model](sync::CVehicleCreationDataNode& cdn) + { + cdn.m_model = model; + cdn.m_creationToken = msec().count(); + cdn.m_needsToBeHotwired = false; + cdn.m_maxHealth = 1000; + cdn.m_popType = sync::POPTYPE_MISSION; + cdn.m_randomSeed = rand(); + cdn.m_tyresDontBurst = false; + cdn.m_vehicleStatus = 2; + cdn.m_unk5 = false; + }); + + SetupNode(tree, [isEngine, speed, direction, trackid, trainConfigIndex, stopAtStation](sync::CTrainGameStateDataNode& cdn) + { + cdn.data.linkedToBackwardId = 0; + cdn.data.linkedToForwardId = 0; + cdn.data.distanceFromEngine = 0.0f; + + cdn.data.trainState = 3; + + cdn.data.isEngine = isEngine; + cdn.data.cruiseSpeed = speed; + cdn.data.direction = direction; + cdn.data.trackId = trackid; + cdn.data.trainConfigIndex = trainConfigIndex; + + cdn.data.isCaboose = true; + + cdn.data.isMissionTrain = true; + cdn.data.hasPassengerCarriages = true; + cdn.data.renderDerailed = false; + + if (Is2372()) + { + cdn.data.allowRemovalByPopulation = false; + cdn.data.shouldStopAtStations = stopAtStation; + cdn.data.highPrecisionBlending = false; + } + }); + + SetupPosition(tree, posX, posY, posZ); + SetupHeading(tree, heading); + + SetupNode(tree, [resourceHash](sync::CEntityScriptInfoDataNode& cdn) + { + cdn.m_scriptHash = resourceHash; + cdn.m_timestamp = msec().count(); + }); + + return tree; +} + std::shared_ptr MakePed(uint32_t model, float posX, float posY, float posZ, uint32_t resourceHash, float heading = 0.0f) { auto tree = std::make_shared(); @@ -294,6 +354,38 @@ void DisownEntityScript(const fx::sync::SyncEntityPtr& entity) } } +static auto GetTrainCarriageCount(fx::ServerGameState* sgs, const fx::sync::SyncEntityPtr& engine) -> int +{ + if (engine->type != fx::sync::NetObjEntityType::Train) + { + return -1; + } + + int carriageCount = 0; + for (auto link = GetNextTrain(sgs, engine); link; link = GetNextTrain(sgs, link)) + { + carriageCount++; + } + + return carriageCount; +}; + +static auto GetTrainCabooseCarriage(fx::ServerGameState* sgs, const fx::sync::SyncEntityPtr& engine) -> fx::sync::SyncEntityPtr +{ + fx::sync::SyncEntityPtr caboose = engine; + for (auto link = GetNextTrain(sgs, engine); link; link = GetNextTrain(sgs, link)) + { + auto state = link->syncTree->GetTrainState(); + + if (state->linkedToBackwardId == 0 || state->isCaboose) + { + caboose = link; + break; + } + } + return caboose; +} + static InitFunction initFunction([]() { fx::ServerInstanceBase::OnServerCreate.Connect([](fx::ServerInstanceBase* ref) @@ -447,6 +539,162 @@ static InitFunction initFunction([]() ctx.SetResult(sgs->MakeScriptHandle(entity)); }); + fx::ScriptEngine::RegisterNativeHandler("CREATE_TRAIN", [=](fx::ScriptContext& ctx) + { + uint32_t resourceHash = 0; + + fx::OMPtr runtime; + + if (FX_SUCCEEDED(fx::GetCurrentScriptRuntime(&runtime))) + { + fx::Resource* resource = reinterpret_cast(runtime->GetParentObject()); + + if (resource) + { + resourceHash = HashString(resource->GetName().c_str()); + } + } + + uint32_t modelHash = ctx.GetArgument(0); + float x = ctx.GetArgument(1); + float y = ctx.GetArgument(2); + float z = ctx.GetArgument(3); + bool direction = ctx.GetArgument(4); + bool stopAtStations = ctx.GetArgument(5); + float speed = ctx.GetArgument(6); + uint32_t trackId = ctx.GetArgument(7); + uint32_t trainConfigIndex = ctx.CheckArgument(8); + + //Don't create train if an invalid trackid has been provided. + if (trackId != 0 && trackId != 3) + { + throw std::runtime_error(va("Tried to spawn train on invalid track id: %u", trackId)); + + ctx.SetResult(0); + return; + } + + //Don't create train if an invalid trainConfigIndex has been provided, otherwises the client crashes. + if (trainConfigIndex > (uint32_t)(Is2802() ? 26 : (Is2372() ? 25 : 24))) + { + throw std::runtime_error(va("Tried to spawn train with invalid train config index: %u", trainConfigIndex)); + + ctx.SetResult(0); + return; + } + + + auto tree = MakeTrain(modelHash, x, y, z, 0.0f, resourceHash, true, direction, stopAtStations, speed, trackId, trainConfigIndex); + auto& sgs = ref->GetComponent(); + auto entity = sgs->CreateEntityFromTree(sync::NetObjEntityType::Train, tree); + + //Edit CTrainGameStateData node to assign engineCarriage to the newly created objectID + SetupNode(tree, [entity](sync::CTrainGameStateDataNode& cdn) + { + cdn.data.engineCarriage = entity->handle; + }); + + ctx.SetResult(sgs->MakeScriptHandle(entity)); + }); + + fx::ScriptEngine::RegisterNativeHandler("CREATE_TRAIN_CARRIAGE", [=](fx::ScriptContext& ctx) + { + // get the current resource manager + auto resourceManager = fx::ResourceManager::GetCurrent(); + + // get the owning server instance + auto instance = resourceManager->GetComponent()->Get(); + + // get the server's game state + auto gameState = instance->GetComponent(); + + // parse the client ID + auto id = ctx.GetArgument(0); + + if (!id) + { + ctx.SetResult(0); + return; + } + + auto entity = gameState->GetEntity(id); + + //Make sure entity exists. + if (!entity) + { + throw std::runtime_error(va("Tried to access invalid entity: %d", id)); + + ctx.SetResult(0); + return; + } + + auto trainState = entity->syncTree->GetTrainState(); + + // Make sure entity is a train + if (entity->type != fx::sync::NetObjEntityType::Train) + { + throw std::runtime_error(va("Entity is not a train: %d", id)); + + ctx.SetResult(0); + return; + } + + // Make sure entity is the engine carriage + if (!trainState || !trainState->isEngine) + { + throw std::runtime_error(va("Tried to attach train carriage to invalid train entity: %d", id)); + + ctx.SetResult(0); + return; + } + + uint32_t resourceHash = 0; + fx::OMPtr runtime; + + if (FX_SUCCEEDED(fx::GetCurrentScriptRuntime(&runtime))) + { + fx::Resource* resource = reinterpret_cast(runtime->GetParentObject()); + + if (resource) + { + resourceHash = HashString(resource->GetName().c_str()); + } + } + + uint32_t modelHash = ctx.GetArgument(1); + float distanceFromEngine = ctx.GetArgument(2); + + float position[3]; + entity->syncTree->GetPosition(position); + + auto tree = MakeTrain(modelHash, position[0], position[1], position[2], 0.0f, resourceHash, false, trainState->direction, trainState->shouldStopAtStations, trainState->cruiseSpeed, trainState->trackId, trainState->trainConfigIndex); + auto& sgs = ref->GetComponent(); + + if (tree) + { + SetupNode(tree, [sgs, entity, distanceFromEngine](sync::CTrainGameStateDataNode& cdn){ + cdn.data.engineCarriage = entity->handle; + cdn.data.linkedToForwardId = GetTrainCabooseCarriage(sgs.GetRef(), entity)->handle; + cdn.data.carriageIndex = GetTrainCarriageCount(sgs.GetRef(), entity) + 1; + cdn.data.distanceFromEngine = distanceFromEngine; + cdn.data.isEngine = false; + cdn.data.isCaboose = true; + }); + } + + auto carriageEntity = sgs->CreateEntityFromTree(fx::sync::NetObjEntityType::Train, tree); + auto caboose = GetTrainCabooseCarriage(sgs.GetRef(), entity); + + auto trainSyncTree = std::static_pointer_cast(caboose->syncTree); + SetupNode(trainSyncTree, [carriageEntity](sync::CTrainGameStateDataNode& cdn) + { + cdn.data.linkedToBackwardId = carriageEntity->handle; + cdn.data.isCaboose = false; + }); + + ctx.SetResult(sgs->MakeScriptHandle(carriageEntity)); + }); + fx::ScriptEngine::RegisterNativeHandler("CREATE_OBJECT_NO_OFFSET", [=](fx::ScriptContext& ctx) { uint32_t resourceHash = 0; diff --git a/code/components/gta-net-five/src/CloneObjectManager.cpp b/code/components/gta-net-five/src/CloneObjectManager.cpp index 98da61de9a..dcc7453673 100644 --- a/code/components/gta-net-five/src/CloneObjectManager.cpp +++ b/code/components/gta-net-five/src/CloneObjectManager.cpp @@ -1,6 +1,13 @@ #include #include +//Required for server-spawned train blending +#ifdef GTA_FIVE +#include +#include +#include +#endif + #include #include @@ -80,6 +87,15 @@ static void netObjectMgrBase__DestroyNetworkObject(rage::netObjectMgr* manager, delete object; } } + + +#ifdef GTA_FIVE +static int TrainTrackNodeIndexOffset; +static hook::cdecl_stub _SetTrainCoord([]() +{ + return hook::pattern("41 B1 01 48 8B D9 45 8A C1 C6 44 24 ? ? FF 90 ? ? ? ? 83 CA FF").count(1).get(0).get(0x22); +}); +#endif static void(*g_orig_netObjectMgrBase__ChangeOwner)(rage::netObjectMgr*, rage::netObject*, CNetGamePlayer*, int); @@ -96,6 +112,24 @@ static void netObjectMgrBase__ChangeOwner(rage::netObjectMgr* manager, rage::net object->PostMigrate(migrationType); CloneObjectMgr->ChangeOwner(object, oldOwnerId, targetPlayer, migrationType); +#ifdef GTA_FIVE + //Check that the entity is a train and that we own the entity + if (object->objectType == (uint16_t)NetObjEntityType::Train && targetPlayer->physicalPlayerIndex() == 128) + { + //Make sure that the gameObject isn't a nullptr + if (auto pVehicle = object->GetGameObject()) + { + //Only force blend if the train track node is 0, as this is the only time we need to correct position + //this does not have any affect on trains that are actually at track node 0 + if ((int)*(int*)((char*)pVehicle + TrainTrackNodeIndexOffset) == 0) + { + _SetTrainCoord((CVehicle*)pVehicle, -1, -1); + // Force blend + object->GetBlender()->m_30(); + } + } + } +#endif } static rage::netObject* (*g_orig_netObjectMgrBase__GetNetworkObject)(rage::netObjectMgr* manager, uint16_t id, bool evenIfDeleting); @@ -148,6 +182,8 @@ static HookFunction hookFunction([]() MH_Initialize(); #if GTA_FIVE + //Taken from extra-natives-five/VehicleExtraNatives.cpp + TrainTrackNodeIndexOffset = *hook::get_pattern("E8 ? ? ? ? 40 8A F8 84 C0 75 ? 48 8B CB E8", -4); MH_CreateHook(hook::get_pattern("48 8B F2 0F B7 52 0A 41 B0 01", -0x19), netObjectMgrBase__RegisterNetworkObject, (void**)&g_orig_netObjectMgrBase__RegisterNetworkObject); // MH_CreateHook(hook::get_pattern("8A 42 4C 45 33 FF 48 8B DA C0 E8 02", -0x21), netObjectMgrBase__DestroyNetworkObject, (void**)&g_orig_netObjectMgrBase__DestroyNetworkObject); // MH_CreateHook(hook::get_pattern("44 8A 62 4B 33 DB 41 8B E9", -0x20), netObjectMgrBase__ChangeOwner, (void**)&g_orig_netObjectMgrBase__ChangeOwner); // diff --git a/ext/native-decls/server/CreateTrain.md b/ext/native-decls/server/CreateTrain.md new file mode 100644 index 0000000000..ce62f1145b --- /dev/null +++ b/ext/native-decls/server/CreateTrain.md @@ -0,0 +1,32 @@ +--- +ns: CFX +apiset: server +--- +## CREATE_TRAIN + +```c +Vehicle CREATE_TRAIN(Hash modelHash, float x, float y, float z, bool direction, bool stopsAtStations, float cruiseSpeed, int trackId, int trainConfigIndex); +``` + +Creates a functioning train using 'server setter' logic (like CREATE_VEHICLE_SERVER_SETTER and CREATE_AUTOMOBILE). + +## Parameters +* **modelHash**: The model of train to spawn. +* **x**: Spawn coordinate X component. +* **y**: Spawn coordinate Y component. +* **z**: Spawn coordinate Z component. +* **direction**: The direction in which the train will go (true or false) +* **stopsAtStations**: Should the train stop at stations on the track (>b2372) +* **trackId**: The track the train should follow (0 = Main Track, 3 = Metro track) +* **trainConfigIndex**: The train variation id, found in trains.xml + +## Return value +A script handle for the train. + +## Examples +```lua +-- Coordinates of the first node (0) in track 3 +local coordinates = vec3(193.196, -603.836, 16.7565) +local metro = CreateTrain(`metrotrain`, coordinates, true, true, 12.0, 3, 25) +print(GetTrainCarriageIndex(metro)) -- should return 0 +``` \ No newline at end of file diff --git a/ext/native-decls/server/CreateTrainCarriage.md b/ext/native-decls/server/CreateTrainCarriage.md new file mode 100644 index 0000000000..088be24452 --- /dev/null +++ b/ext/native-decls/server/CreateTrainCarriage.md @@ -0,0 +1,44 @@ +--- +ns: CFX +apiset: server +--- +## CREATE_TRAIN_CARRIAGE + +```c +Vehicle CREATE_TRAIN_CARRIAGE(Vehicle train, Hash modelHash, float distanceFromEngine); +``` + +Creates and attaches train carriage to the target train. + +## Parameters +* **train**: The target train. +* **modelHash**: The model of train to spawn. +* **distanceFromEngine**: how far the carriage should be from the engine carriage (positive value places carriage in front of engine) + +## Return value +A script handle for the train. + +## Examples +```lua +--[[ + To work out distanceFromEngine: + First Carriage: + lengthOfCarriage = (modelBoundBoxMaxY - modelBoundBoxMinY) + distanceFromEngine = lengthOfCarriage + carriageGap (found in trains.xml) + + Each Additional Carriage: add distance to previous carriage distanceFromEngine + + e.g. (metrotrain, trainConfigIndex 25) + + lengthOfCarriage = (5.611063 - -5.667999) + distanceFromEngine = lengthOfCarriage + -0.5 = 10.7791 +]] + +-- Coordinates of the first node (0) in track 3 +local coordinates = vec3(193.196, -603.836, 16.7565) +local metro = CreateTrain(`metrotrain`, coordinates, true, true, 12.0, 3, 25) + +-- This carriage would be flipped as in trains.xml 'flip_model_dir' is true for the second carriage +local metroCarriage = CreateTrainCarriage(metro, `metrotrain`, -10.7791) +print(GetTrainCarriageIndex(metro)) +``` \ No newline at end of file