Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.24)

project(RakSAMP
VERSION 0.10.1
VERSION 0.11.0
DESCRIPTION "Cross-platform SA-MP 0.3.7 and 0.3DL fake client and development server"
LANGUAGES C CXX)

Expand Down Expand Up @@ -78,6 +78,12 @@ if(BUILD_TESTING)
target_include_directories(key-state-tests PRIVATE client/src common raknet/SAMP)
add_test(NAME key-state COMMAND key-state-tests)

add_executable(drive-position-tests
tests/drive_position_tests.cpp
client/src/drive_position.cpp)
target_include_directories(drive-position-tests PRIVATE client/src)
add_test(NAME drive-position COMMAND drive-position-tests)

add_executable(tinyxml-extension-tests
tests/tinyxml_extension_tests.cpp
tinyxml/tinystr.cpp
Expand Down Expand Up @@ -167,6 +173,7 @@ if(RAKSAMP_BUILD_CLIENT)
add_executable(raksamp-client
client/src/automation_protocol.cpp
client/src/cmds.cpp
client/src/drive_position.cpp
client/src/key_state.cpp
client/src/localplayer.cpp
client/src/load_mode.cpp
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ Normal input is sent as chat; input beginning with `/` is sent as a server comma
`!goto`, `!gotocp`, `!autogotocp`, `!spawn`, `!class`, `!pickup`, `!weapon`,
`!shoot`, `!shootmiss`, `!damage`, `!takedamage`, `!key`, `!pos`, `!follow`, `!selplayer`, `!selveh`, `!vlist`, `!dialogresponse`,
`!menusel`, `!seltd`, `!sendrates`, `!log`, `!logstatus`, `!teleport`,
`!change_name`, `!change_server`, `!imitate`, and `!scmevent`.
`!change_name`, `!change_server`, `!imitate`, `!driveposition`, `!driveto`,
`!drivecancel`, `!drivestatus`, and `!scmevent`.

`!shoot <player> [weapon]` emits correlated bullet sync and damage RPCs.
`!shootmiss [weapon]` emits a non-damaging bullet miss for callback testing.
Expand All @@ -174,6 +175,18 @@ damage received by the headless client. The last command is explicit because
RakSAMP has no GTA physics engine to independently simulate explosions,
collisions, drowning, or falls; the server must still validate every report.

`!driveposition <x> <y> <z>` moves the currently assigned driver vehicle and
immediately emits driver sync. Coordinates must be finite and within the supported
SA-MP world bounds. It intentionally provides an instant-position probe for testing
server containment of malicious or corrective position changes.

`!driveto <x> <y> <z> <duration-ms>` linearly interpolates the assigned driver
vehicle over a bounded 100–60,000 milliseconds while normal driver sync continues.
`!drivestatus` reports an active target and remaining duration, and `!drivecancel`
stops motion at its current position. Starting another motion, teleporting to a
checkpoint, changing seats/vehicles, or exiting cancels the prior motion. These
commands reject passengers and clients without an assigned streamed vehicle.

`!key <down|up> <action|mask> [action|mask ...]` holds or releases one or more
server-visible SA-MP controls and immediately emits the appropriate on-foot,
driver, or passenger sync. Named actions cover `action`, `crouch`/`horn`/`h`,
Expand Down
3 changes: 3 additions & 0 deletions client/bin/RakSAMPClient.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Avalable runmodes:
!rcon: send an RCON command.
!goto: go to players position.
!gotocp: go to the current checkpoint.
!driveposition <x> <y> <z>: move the assigned driver vehicle and send sync.
!driveto <x> <y> <z> <duration-ms>: interpolate assigned-driver movement.
!drivestatus / !drivecancel: inspect or cancel active driver movement.
!autogotocp: toggle automatic checkpoint teleporter.
!imitate: change imitate name.
!vlist: shows list of vehicles.
Expand Down
152 changes: 152 additions & 0 deletions client/src/drive_position.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#include "drive_position.h"

#include <cmath>
#include <sstream>

namespace
{
constexpr float MaximumCoordinateMagnitude = 20000.0f;
constexpr uint32_t MinimumDriveDurationMilliseconds = 100;
constexpr uint32_t MaximumDriveDurationMilliseconds = 60000;

bool IsFiniteAndBounded(const DriveVector &position)
{
return std::isfinite(position.x) && std::isfinite(position.y) &&
std::isfinite(position.z) &&
std::fabs(position.x) <= MaximumCoordinateMagnitude &&
std::fabs(position.y) <= MaximumCoordinateMagnitude &&
std::fabs(position.z) <= MaximumCoordinateMagnitude;
}

DriveVector Interpolate(
const DriveVector &start,
const DriveVector &target,
float progress)
{
return {
start.x + (target.x - start.x) * progress,
start.y + (target.y - start.y) * progress,
start.z + (target.z - start.z) * progress
};
}
}

DriveCommandResult ParseDriveCommand(
const std::string &command,
DriveCommand &parsed,
std::string &error)
{
std::istringstream input(command);
std::string name;
input >> name;
if(name != "!driveposition" && name != "!driveto" &&
name != "!drivecancel" && name != "!drivestatus")
return DriveCommandResult::NotDriveCommand;

DriveCommand candidate;
std::string trailing;
if(name == "!drivecancel" || name == "!drivestatus")
{
if(input >> trailing)
{
error = name + " does not accept arguments.";
return DriveCommandResult::Error;
}
candidate.kind = name == "!drivecancel"
? DriveCommandKind::Cancel
: DriveCommandKind::Status;
}
else
{
if(!(input >> candidate.target.x >> candidate.target.y >>
candidate.target.z))
{
error = name == "!driveto"
? "Usage: !driveto <x> <y> <z> <duration-ms>"
: "Usage: !driveposition <x> <y> <z>";
return DriveCommandResult::Error;
}

if(name == "!driveto")
{
unsigned long duration = 0;
if(!(input >> duration) || duration < MinimumDriveDurationMilliseconds ||
duration > MaximumDriveDurationMilliseconds)
{
error = "Drive duration must be between 100 and 60000 milliseconds.";
return DriveCommandResult::Error;
}
candidate.kind = DriveCommandKind::To;
candidate.durationMilliseconds = static_cast<uint32_t>(duration);
}
else
candidate.kind = DriveCommandKind::Position;

if(input >> trailing)
{
error = name + " received unexpected trailing input.";
return DriveCommandResult::Error;
}
if(!IsFiniteAndBounded(candidate.target))
{
error = "Drive coordinates must be finite and within +/-20000.";
return DriveCommandResult::Error;
}
}

parsed = candidate;
error.clear();
return DriveCommandResult::Parsed;
}

bool DriveMotion::Start(
const DriveVector &start,
const DriveVector &target,
uint32_t durationMilliseconds,
uint64_t nowMilliseconds,
std::string &error)
{
if(!IsFiniteAndBounded(start) || !IsFiniteAndBounded(target))
{
error = "Drive motion endpoints must be finite and within +/-20000.";
return false;
}
if(durationMilliseconds < MinimumDriveDurationMilliseconds ||
durationMilliseconds > MaximumDriveDurationMilliseconds ||
nowMilliseconds > UINT64_MAX - durationMilliseconds)
{
error = "Drive motion duration is outside the supported bounds.";
return false;
}

start_ = start;
target_ = target;
startedAtMilliseconds_ = nowMilliseconds;
finishesAtMilliseconds_ = nowMilliseconds + durationMilliseconds;
active_ = true;
error.clear();
return true;
}

DriveMotionSample DriveMotion::Sample(uint64_t nowMilliseconds)
{
if(!active_)
return { target_, false, false };
if(nowMilliseconds <= startedAtMilliseconds_)
return { start_, true, false };

const bool completed = nowMilliseconds >= finishesAtMilliseconds_;
const float progress = completed
? 1.0f
: static_cast<float>(nowMilliseconds - startedAtMilliseconds_) /
static_cast<float>(finishesAtMilliseconds_ - startedAtMilliseconds_);
const DriveVector position = Interpolate(start_, target_, progress);
if(completed)
active_ = false;
return { position, !completed, completed };
}

void DriveMotion::Cancel()
{
active_ = false;
}
68 changes: 68 additions & 0 deletions client/src/drive_position.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#pragma once

#include <cstdint>
#include <string>

struct DriveVector
{
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
};

enum class DriveCommandKind
{
Position,
To,
Cancel,
Status
};

struct DriveCommand
{
DriveCommandKind kind = DriveCommandKind::Status;
DriveVector target;
uint32_t durationMilliseconds = 0;
};

enum class DriveCommandResult
{
NotDriveCommand,
Parsed,
Error
};

DriveCommandResult ParseDriveCommand(
const std::string &command,
DriveCommand &parsed,
std::string &error);

struct DriveMotionSample
{
DriveVector position;
bool active = false;
bool completed = false;
};

class DriveMotion
{
public:
bool Start(
const DriveVector &start,
const DriveVector &target,
uint32_t durationMilliseconds,
uint64_t nowMilliseconds,
std::string &error);
DriveMotionSample Sample(uint64_t nowMilliseconds);
void Cancel();
bool IsActive() const { return active_; }
const DriveVector &Target() const { return target_; }
uint64_t FinishesAtMilliseconds() const { return finishesAtMilliseconds_; }

private:
DriveVector start_;
DriveVector target_;
uint64_t startedAtMilliseconds_ = 0;
uint64_t finishesAtMilliseconds_ = 0;
bool active_ = false;
};
Loading