Skip to content

Commit bc7201c

Browse files
committed
feat: add some basic examples
0 parents  commit bc7201c

29 files changed

Lines changed: 1381 additions & 0 deletions

‎.github/workflows/build.yml‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: [master]
6+
tags: ["v*"]
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: write
12+
13+
jobs:
14+
build:
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
os: [ubuntu-latest, windows-latest]
19+
runs-on: ${{ matrix.os }}
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: actions/setup-dotnet@v4
24+
with:
25+
dotnet-version: 8.0.x
26+
27+
- run: dotnet build FivePRS.Examples.sln -c Release
28+
29+
- name: Package
30+
if: matrix.os == 'ubuntu-latest'
31+
run: |
32+
cp -r resources/fiveprs_example_hud dist/
33+
cp LICENSE dist/
34+
cd dist && zip -r ../fiveprs-examples.zip .
35+
36+
- uses: actions/upload-artifact@v4
37+
if: matrix.os == 'ubuntu-latest'
38+
with:
39+
name: fiveprs-examples
40+
path: fiveprs-examples.zip
41+
42+
- name: Release
43+
if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/v')
44+
env:
45+
GH_TOKEN: ${{ github.token }}
46+
run: gh release create "${{ github.ref_name }}" fiveprs-examples.zip --title "${{ github.ref_name }}" --generate-notes

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.fiveprs/
2+
dist/
3+
bin/
4+
obj/

‎Directory.Build.props‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project>
2+
3+
<Import Project="build\FivePRS.props" />
4+
5+
<PropertyGroup>
6+
<FivePRSFolder>$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName($(MSBuildProjectDirectory)))))</FivePRSFolder>
7+
<TargetFramework>net452</TargetFramework>
8+
<LangVersion>10</LangVersion>
9+
<Nullable>enable</Nullable>
10+
<OutputType>Library</OutputType>
11+
<AssemblyName>$(MSBuildProjectName).net</AssemblyName>
12+
<RootNamespace>$(MSBuildProjectName)</RootNamespace>
13+
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
14+
<OutputPath>$(MSBuildThisFileDirectory)dist\fiveprs\$(FivePRSFolder)\</OutputPath>
15+
<CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>
16+
<GenerateDependencyFile>false</GenerateDependencyFile>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="CitizenFX.Core.Client" Version="1.0.26803" ExcludeAssets="runtime" />
21+
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
22+
<Reference Include="FivePRS.Core" HintPath="$(FivePRSClientDir)FivePRS.Core.dll" Private="false" />
23+
<Reference Include="FivePRS.Client.net" HintPath="$(FivePRSClientDir)FivePRS.Client.net.dll" Private="false" />
24+
<Reference Include="Newtonsoft.Json" HintPath="$(FivePRSClientDir)Newtonsoft.Json.dll" Private="false" />
25+
</ItemGroup>
26+
27+
</Project>

‎Directory.Build.targets‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project>
2+
3+
<Import Project="build\Fetch.targets" />
4+
5+
<Target Name="FetchFivePRSForProject" BeforeTargets="ResolveAssemblyReferences" DependsOnTargets="FetchFivePRS" />
6+
7+
<PropertyGroup>
8+
<FivePRSDeployDir Condition="'$(FIVEPRS_RESOURCE)' != ''">$([MSBuild]::EnsureTrailingSlash('$(FIVEPRS_RESOURCE)'))$(FivePRSFolder)\</FivePRSDeployDir>
9+
</PropertyGroup>
10+
11+
<Target Name="DeployToServer" AfterTargets="Build" Condition="'$(FivePRSDeployDir)' != ''">
12+
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(FivePRSDeployDir)" SkipUnchangedFiles="true" />
13+
<Copy SourceFiles="@(Content)" DestinationFiles="@(Content->'$(FivePRSDeployDir)%(RelativeDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
14+
<Message Importance="high" Text="$(TargetFileName) -> $(FivePRSDeployDir)" />
15+
</Target>
16+
17+
</Project>

‎Directory.Solution.targets‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project>
2+
3+
<Import Project="build\FivePRS.props" />
4+
<Import Project="build\Fetch.targets" />
5+
6+
<Target Name="FetchFivePRSForSolution" BeforeTargets="Build" DependsOnTargets="FetchFivePRS" />
7+
8+
</Project>

‎FivePRS.Examples.sln‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "callouts", "callouts", "{5A50BBB5-F8AF-9126-8081-073D165AB07A}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleCallouts", "callouts\ExampleCallouts\ExampleCallouts.csproj", "{FC07AB05-276A-4C04-B523-A384F8BD7389}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{07D57EEB-2F50-60C4-C011-FE4FA775C9A8}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShiftTracker", "plugins\ShiftTracker\ShiftTracker.csproj", "{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}"
13+
EndProject
14+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apps", "apps", "{1787FE1D-075E-9E68-7218-25F1BD1BBEAB}"
15+
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleApps", "apps\ExampleApps\ExampleApps.csproj", "{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}"
17+
EndProject
18+
Global
19+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
20+
Debug|Any CPU = Debug|Any CPU
21+
Debug|x64 = Debug|x64
22+
Debug|x86 = Debug|x86
23+
Release|Any CPU = Release|Any CPU
24+
Release|x64 = Release|x64
25+
Release|x86 = Release|x86
26+
EndGlobalSection
27+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
28+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Debug|Any CPU.Build.0 = Debug|Any CPU
30+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Debug|x64.ActiveCfg = Debug|Any CPU
31+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Debug|x64.Build.0 = Debug|Any CPU
32+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Debug|x86.ActiveCfg = Debug|Any CPU
33+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Debug|x86.Build.0 = Debug|Any CPU
34+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Release|Any CPU.Build.0 = Release|Any CPU
36+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Release|x64.ActiveCfg = Release|Any CPU
37+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Release|x64.Build.0 = Release|Any CPU
38+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Release|x86.ActiveCfg = Release|Any CPU
39+
{FC07AB05-276A-4C04-B523-A384F8BD7389}.Release|x86.Build.0 = Release|Any CPU
40+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
42+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Debug|x64.ActiveCfg = Debug|Any CPU
43+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Debug|x64.Build.0 = Debug|Any CPU
44+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Debug|x86.ActiveCfg = Debug|Any CPU
45+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Debug|x86.Build.0 = Debug|Any CPU
46+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
47+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Release|Any CPU.Build.0 = Release|Any CPU
48+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Release|x64.ActiveCfg = Release|Any CPU
49+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Release|x64.Build.0 = Release|Any CPU
50+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Release|x86.ActiveCfg = Release|Any CPU
51+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C}.Release|x86.Build.0 = Release|Any CPU
52+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Debug|Any CPU.Build.0 = Debug|Any CPU
54+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Debug|x64.ActiveCfg = Debug|Any CPU
55+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Debug|x64.Build.0 = Debug|Any CPU
56+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Debug|x86.ActiveCfg = Debug|Any CPU
57+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Debug|x86.Build.0 = Debug|Any CPU
58+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Release|Any CPU.ActiveCfg = Release|Any CPU
59+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Release|Any CPU.Build.0 = Release|Any CPU
60+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Release|x64.ActiveCfg = Release|Any CPU
61+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Release|x64.Build.0 = Release|Any CPU
62+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Release|x86.ActiveCfg = Release|Any CPU
63+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61}.Release|x86.Build.0 = Release|Any CPU
64+
EndGlobalSection
65+
GlobalSection(SolutionProperties) = preSolution
66+
HideSolutionNode = FALSE
67+
EndGlobalSection
68+
GlobalSection(NestedProjects) = preSolution
69+
{FC07AB05-276A-4C04-B523-A384F8BD7389} = {5A50BBB5-F8AF-9126-8081-073D165AB07A}
70+
{B6D5C88B-57EF-4DBE-8C91-7881D1BA0E2C} = {07D57EEB-2F50-60C4-C011-FE4FA775C9A8}
71+
{812F5E82-BA3F-4FB4-BF0F-72C44F20FE61} = {1787FE1D-075E-9E68-7218-25F1BD1BBEAB}
72+
EndGlobalSection
73+
EndGlobal

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Tyler Hodgkin <https://codemeapixel.dev>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# FivePRS Examples
2+
3+
Complete, working examples for extending [FivePRS](https://github.com/FivePRS/core): a callout pack, a plugin, terminal apps and a Lua resource. Copy one as the starting point for your own, then [list it on the Assets page](https://fiveprs.org/docs/developers/listing-assets) when it's ready.
4+
5+
| Example | Type | Shows |
6+
|---|---|---|
7+
| [`callouts/ExampleCallouts`](callouts/ExampleCallouts) | Callout pack | Four callouts, one per pattern |
8+
| [`plugins/ShiftTracker`](plugins/ShiftTracker) | Plugin | FivePRS events, a chat command and a server setting |
9+
| [`apps/ExampleApps`](apps/ExampleApps) | Terminal apps | A drawn screen app and an app with its own page |
10+
| [`resources/fiveprs_example_hud`](resources/fiveprs_example_hud) | Resource | A duty HUD from `getState()` and events, in Lua |
11+
12+
Every example is MIT licensed, so you can copy any of it into your own work.
13+
14+
## Building
15+
16+
You need the [.NET SDK](https://dotnet.microsoft.com/download) 8 or newer, on Windows, Linux or macOS.
17+
18+
```bash
19+
git clone https://github.com/FivePRS/examples
20+
cd examples
21+
dotnet build -c Release
22+
```
23+
24+
The first build downloads the FivePRS release it compiles against into `.fiveprs/`. The version is set by `FivePRSVersion` in [`build/FivePRS.props`](build/FivePRS.props). Change it, or pass `-p:FivePRSVersion=<version>`, to build against another release.
25+
26+
Builds land in `dist/fiveprs/`, laid out like the resource:
27+
28+
```text
29+
dist/fiveprs/
30+
callouts/ExampleCallouts.net.dll
31+
plugins/ShiftTracker.net.dll
32+
apps/ExampleApps.net.dll
33+
apps/fieldlog/
34+
```
35+
36+
Copy it over your server's `resources/fiveprs` folder and restart `fiveprs`. To have every build copied there for you, set `FIVEPRS_RESOURCE` to that folder first:
37+
38+
```powershell
39+
$env:FIVEPRS_RESOURCE = "C:\server\resources\fiveprs" # PowerShell
40+
```
41+
42+
```bash
43+
export FIVEPRS_RESOURCE=~/server/resources/fiveprs # bash
44+
```
45+
46+
`resources/fiveprs_example_hud` needs no build: copy the folder into `resources/` and add `ensure fiveprs_example_hud` after `ensure fiveprs`.
47+
48+
## Callout pack
49+
50+
[`ExampleCallouts`](callouts/ExampleCallouts) builds one DLL with four callouts. Read them in this order:
51+
52+
| Callout | Department | Pattern |
53+
|---|---|---|
54+
| [Noise Complaint](callouts/ExampleCallouts/NoiseComplaintCallout.cs) | Police | The basics: a location list, night-only dispatch, spawning when the player is close, talking to a ped with the interact key, and two possible endings |
55+
| [Armed Suspect](callouts/ExampleCallouts/ArmedSuspectCallout.cs) | Police | A search area, waiting until the player can see the suspect, then surrender, foot pursuit or a fight |
56+
| [Cardiac Arrest](callouts/ExampleCallouts/CardiacArrestCallout.cs) | EMS | Building on the EMS module's `PatientCallout`: you set up the scene, FivePRS handles treatment and transport. `OnUpdate` runs every frame |
57+
| [Dumpster Fire](callouts/ExampleCallouts/DumpsterFireCallout.cs) | Fire | Spawning a prop and using the Fire module's `FireScene` to start fires and check when they're out |
58+
59+
Every entity and blip goes through `TrackEntity` or `TrackBlip`, so FivePRS removes it when the call ends however it ends. Anything else you set up, such as interaction handlers or fires, is cleaned up in `OnCleanup`.
60+
61+
## Plugin
62+
63+
[`ShiftTracker`](plugins/ShiftTracker) counts each shift's length, calls offered and arrests, using the `FivePRS:Local:DutyChanged`, `FivePRS:Local:CalloutReceived` and `FivePRS:Local:SuspectCuffed` events. Players type `/shift` for their stats and get a summary when they go off duty.
64+
65+
It reminds players to take a break every 120 minutes on shift. Server owners change that in `server.cfg` with a replicated convar, which plugins can read on the client:
66+
67+
```cfg
68+
setr shifttracker_reminder_minutes 90 # 0 turns reminders off
69+
```
70+
71+
## Terminal apps
72+
73+
[`ExampleApps`](apps/ExampleApps) builds one DLL with two apps:
74+
75+
- **[10-Codes](apps/ExampleApps/TenCodesApp.cs)** is a drawn screen: FivePRS renders a search form, group buttons and a list from an `AppScreen`, and calls `OnAction` when they're used.
76+
- **[Field Log](apps/ExampleApps/FieldLogApp.cs)** has its own [page](apps/ExampleApps/fieldlog). The page sends an action with the SDK, the C# side adds the time and the player's street, and sends the entries back. Its badge shows the number of entries.
77+
78+
Pages live under `apps/<name>/` in the resource, which `fiveprs` already serves.
79+
80+
## Resource
81+
82+
[`fiveprs_example_hud`](resources/fiveprs_example_hud) is a standalone resource that shows your callsign, status and current call while you're on duty. It reads `exports.fiveprs:getState()` every second and refreshes straight away when `FivePRS:Local:DutyChanged` fires. `/dutyhud` hides it. The same approach works in JavaScript or C#.
83+
84+
For a resource that adds a terminal app, see the official [Notes addon](https://github.com/FivePRS/addons).
85+
86+
## Client code limits
87+
88+
FiveM runs client DLLs on an old Mono runtime. These examples follow its limits: no `System.Text.RegularExpressions`, tuples, `init` accessors, `readonly struct`s or generic constraints in nullable code, and they target `net452`. See [Client code limits](https://fiveprs.org/docs/developers#client-code-limits).
89+
90+
## More
91+
92+
- [Developer docs](https://fiveprs.org/docs/developers)
93+
- [Listing your work](https://fiveprs.org/docs/developers/listing-assets) on [fiveprs.org/assets](https://fiveprs.org/assets)
94+
- [Discord](https://discord.gg/zHyzHWKuM2) for help
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<Content Include="fieldlog\**" CopyToOutputDirectory="PreserveNewest" />
5+
</ItemGroup>
6+
7+
</Project>

‎apps/ExampleApps/FieldLogApp.cs‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using CitizenFX.Core;
5+
using CitizenFX.Core.Native;
6+
using FivePRS.Client.Terminal;
7+
using FivePRS.Core.Models;
8+
9+
namespace ExampleApps
10+
{
11+
public class FieldLogApp : TerminalApp
12+
{
13+
private const int MaxEntries = 50;
14+
private const int MaxNoteLength = 200;
15+
16+
private readonly List<Dictionary<string, object>> _entries = new();
17+
18+
public override string Id => "fieldlog";
19+
public override string Label => "Field Log";
20+
public override string Color => "#0d9488";
21+
public override string Icon => "M4 20h16 M6 16l9-9 3 3-9 9H6z";
22+
public override int Order => 710;
23+
public override string? Page => "apps/fieldlog/index.html";
24+
public override string? Badge => _entries.Count > 0 ? _entries.Count.ToString() : null;
25+
26+
public override bool IsAvailable(PlayerData player) => player.IsOnDuty;
27+
28+
public override void OnOpened() => Send("entries", _entries);
29+
30+
public override void OnAction(string action, IDictionary<string, object> data)
31+
{
32+
switch (action)
33+
{
34+
case "add":
35+
var note = data.TryGetValue("note", out var value) ? Convert.ToString(value)?.Trim() ?? string.Empty : string.Empty;
36+
if (note.Length == 0) return;
37+
38+
_entries.Insert(0, new Dictionary<string, object>
39+
{
40+
["time"] = DateTime.Now.ToString("HH:mm"),
41+
["location"] = CurrentLocation(),
42+
["note"] = note.Length > MaxNoteLength ? note.Substring(0, MaxNoteLength) : note,
43+
});
44+
45+
if (_entries.Count > MaxEntries) _entries.RemoveAt(_entries.Count - 1);
46+
break;
47+
48+
case "clear":
49+
_entries.Clear();
50+
break;
51+
52+
default:
53+
return;
54+
}
55+
56+
Refresh();
57+
Send("entries", _entries);
58+
}
59+
60+
private static string CurrentLocation()
61+
{
62+
var position = Game.PlayerPed.Position;
63+
uint street = 0, crossing = 0;
64+
API.GetStreetNameAtCoord(position.X, position.Y, position.Z, ref street, ref crossing);
65+
66+
var zone = API.GetLabelText(API.GetNameOfZone(position.X, position.Y, position.Z));
67+
var parts = new[] { API.GetStreetNameFromHashKey(street), zone }.Where(part => !string.IsNullOrEmpty(part) && part != "NULL");
68+
return string.Join(", ", parts);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)