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
18 changes: 16 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,38 @@ Open `FivePRS.sln` and build with Visual Studio, or run:

```
dotnet build FivePRS.sln -c Release
dotnet test FivePRS.Tests/FivePRS.Tests.csproj -c Release --no-build
```

Compiled outputs land in `bin/client/` and `bin/server/` as configured in each project file.
Database tests use a temporary SQLite file. To run the civilian store tests against MySQL instead, set `FIVEPRS_TEST_MYSQL` to a connection string for an empty test database, for example a throwaway container:

```
docker run -d --name fiveprs-mysql-test -e MYSQL_ROOT_PASSWORD=fiveprs -e MYSQL_DATABASE=fiveprs -p 33306:3306 mysql:8.4
FIVEPRS_TEST_MYSQL="Server=127.0.0.1;Port=33306;Database=fiveprs;User=root;Password=fiveprs;" dotnet test FivePRS.Tests/FivePRS.Tests.csproj -c Release
```

Compiled outputs land in `bin/client/` and `bin/server/`.

**Testing locally**

Copy the relevant DLLs from `bin/` to your FiveM server's `resources/FivePRS/` directory and restart the resource. There is no automated test suite yet; manual in-server testing is the current validation path.
Run `publish.ps1` from the repository root. It builds the core and writes `dist/fiveprs/` and `dist/fiveprs.zip`, ready to copy into your FiveM server's `resources/` directory. Restart the resource after copying.

---

## Project Structure

| Path | Purpose |
|---|---|
| `FivePRS.*/`, `config/`, `nui/`, `fxmanifest.lua` | Everything that ships in the `fiveprs` resource |
| `publish.ps1` | Builds the core and packages it into `dist/` |

| Project | Purpose |
|---|---|
| `FivePRS.Core` | Shared models, events, interfaces, and config POCOs. No FiveM API dependency. |
| `FivePRS.Client` | Client-side scripting: agency management, callout dispatch, vehicle spawning, loadouts. |
| `FivePRS.Server` | Server-side scripting: player data, XP, database abstraction. |
| `FivePRS.Police` | Police department implementation: callouts, vehicles, loadouts. |
| `FivePRS.Tests` | xUnit tests for config, territories and client assembly compatibility with FiveM's runtime. |

Callout packs and plugin extensions are built as separate class libraries and dropped into the `callouts/` or `plugins/` folders at runtime. They reference `FivePRS.Core` and `FivePRS.Client` but are never compiled into the core solution.

Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ on:

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

Expand Down
48 changes: 0 additions & 48 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"

on:
Expand All @@ -22,20 +11,10 @@ on:
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write

# required to fetch internal or private CodeQL packs
packages: read

# only required for workflows in private repositories
actions: read
contents: read

Expand All @@ -45,43 +24,16 @@ jobs:
include:
- language: csharp
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4

# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.

# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality

# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Release

on:
push:
tags: [ "v*" ]

permissions:
contents: write

jobs:
release:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Check version
shell: pwsh
run: |
$tag = "${{ github.ref_name }}".TrimStart("v")
$manifest = Select-String -Path fxmanifest.lua -Pattern "^version '(.+)'" | ForEach-Object { $_.Matches[0].Groups[1].Value }
if ($manifest -ne $tag) { throw "Tag v$tag does not match fxmanifest.lua version $manifest." }
"VERSION=$tag" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8

- name: Build
shell: pwsh
run: ./publish.ps1 -Configuration Release

- name: Test
run: dotnet test FivePRS.Tests/FivePRS.Tests.csproj -c Release --no-build

- name: Release notes
shell: pwsh
run: |
$lines = Get-Content CHANGELOG.md
$start = [Array]::FindIndex($lines, [Predicate[string]]{ param($l) $l.StartsWith("## [$env:VERSION]") })
if ($start -lt 0) { throw "CHANGELOG.md has no section for $env:VERSION." }
$notes = $lines[($start + 1)..($lines.Length - 1)] | ForEach-Object -Begin { $done = $false } -Process {
if ($done -or $_.StartsWith("## [") -or $_ -match "^\[.+\]: ") { $done = $true } else { $_ }
}
($notes -join "`n").Trim() | Out-File -FilePath release-notes.md -Encoding utf8

- name: Publish release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$flags = @()
if ($env:VERSION.Contains("-")) { $flags += "--prerelease" }
gh release view "${{ github.ref_name }}" *> $null
if ($LASTEXITCODE -eq 0) {
gh release upload "${{ github.ref_name }}" dist/fiveprs.zip --clobber
} else {
gh release create "${{ github.ref_name }}" dist/fiveprs.zip --title "FivePRS $env:VERSION" --notes-file release-notes.md @flags
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ packages/

*.log
*.tmp

TESTING.md
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Changelog

All notable changes to FivePRS are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and FivePRS follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Each release on [GitHub](https://github.com/FivePRS/core/releases) is tagged `v<version>` and uses its section below as the release notes.

## [Unreleased]

## [0.9.0-ptb.1] - 2026-10-10

The first public test build.

### Added
- Police, fire and EMS departments with agencies, ranks, XP and callsigns, all defined in config
- Jurisdictions and territories, so calls and units are matched to the right agency
- Server-side dispatch with AI callouts, player 911 calls and a callout API for DLL callout packs
- Records: people, vehicles, warrants, citations and arrests
- Civilian characters with licenses and registered vehicles
- Character creator for freemode characters, saved per character
- The FivePRS terminal: a tablet with a home screen, apps and wallpapers, including server defaults and custom wallpaper URLs that servers can turn off
- Terminal apps from DLLs in `apps/` or from other resources through exports, with a page SDK for HTML apps
- Settings app, including silencing AI callouts while on duty
- Admin app with the department roster
- Stations on the map with going on duty at a station or in the field, and teleporting to a station
- Armory, locker room and garage points at stations, with uniforms, loadouts and stock police vehicles
- `fprs_coords` for copying coordinates, headings and station entries
- Players return to their last location with their last character when they rejoin
- Update checks against GitHub releases, admin notifications and `fiveprs_update` to download and unpack a release
- Server icon loading from a URL
- Notifications with the FivePRS logo and sender
- Plugin loading from `plugins/` and callout packs from `callouts/`
- Audit log of admin and record actions
- SQLite out of the box, and MySQL support
- Versioned database migrations that update the database on start, with an automatic backup first on SQLite
- Windows and Linux server support
- Per-player rate limits for dispatch, 911 calls, records, civilian actions and admin requests, configurable under `rateLimits`
- Validation of callout catalogs sent by clients, with XP rewards capped at `fiveprs_max_xp`

### Known issues
- Fire and EMS can be joined, but have no callouts or department tools yet; police is the playable department
- Only Mission Row has armory, locker room and garage points
- Back up your database before updating between test builds

[Unreleased]: https://github.com/FivePRS/core/compare/v0.9.0-ptb.1...HEAD
[0.9.0-ptb.1]: https://github.com/FivePRS/core/releases/tag/v0.9.0-ptb.1
11 changes: 11 additions & 0 deletions FivePRS.Client/Agency/BaseAgency.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Threading.Tasks;
using CitizenFX.Core;
using CitizenFX.Core.Native;
using FivePRS.Client.Stations;
using FivePRS.Client.VehicleSpawner;
using FivePRS.Core.Config;
using FivePRS.Core.Events;
using FivePRS.Core.Interfaces;
Expand All @@ -20,6 +22,12 @@ public abstract class BaseAgency : BaseScript, IAgency

public bool IsActive { get; private set; }

public static BaseAgency? Active { get; private set; }

public PatrolVehicleSpawner Vehicles { get; } = new();

public virtual StationCatalog? BuildCatalog(PlayerData player) => null;

protected static PlayerData CurrentPlayer => ClientBrain.LocalPlayerData;

protected string DisplayName =>
Expand Down Expand Up @@ -56,6 +64,8 @@ private async void OnLocalDutyChanged(bool isOnDuty, int departmentId)
if (shouldBeActive == IsActive) return;

IsActive = shouldBeActive;
if (shouldBeActive) Active = this;
else if (Active == this) Active = null;
try
{
if (shouldBeActive)
Expand Down Expand Up @@ -90,6 +100,7 @@ private void OnResourceStop(string resourceName)
if (resourceName != API.GetCurrentResourceName() || !IsActive) return;

IsActive = false;
if (Active == this) Active = null;
try { _ = OffDuty(CurrentPlayer); }
catch (Exception ex) { Debug.WriteLine($"[{AgencyName}] Cleanup on stop failed: {ex.Message}"); }
}
Expand Down
50 changes: 50 additions & 0 deletions FivePRS.Client/App/AdminPanel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FivePRS.Core.Events;
using FivePRS.Core.Models;
using Newtonsoft.Json;

namespace FivePRS.Client.App
{
internal static class AdminPanel
{
private static AdminState? _state;

public static bool IsAvailable => _state is not null;

public static void SetState(string json) => _state = JsonConvert.DeserializeObject<AdminState>(json);

public static void Request() => ClientEvents.TriggerServer(EventNames.ServerAdminRequest, _state?.Query ?? string.Empty);

public static void Search(IDictionary<string, object> data) =>
ClientEvents.TriggerServer(EventNames.ServerAdminRequest, NuiData.GetString(data, "query"));

public static void SetRoster(IDictionary<string, object> data)
{
if (!NuiData.TryGetString(data, "license", out var license) ||
!NuiData.TryGetInt(data, "departmentId", out var departmentId))
{
return;
}

ClientEvents.TriggerServer(EventNames.ServerAdminRosterSet, license, departmentId,
NuiData.GetBool(data, "granted"), _state?.Query ?? string.Empty);
}

public static object? BuildView()
{
if (_state is null) return null;

return new
{
State = _state,
Departments = Enum.GetValues(typeof(Department))
.Cast<Department>()
.Where(d => d != Department.None)
.Select(d => new { Id = (int)d, Name = d.ToString() })
.ToList(),
};
}
}
}
Loading
Loading