Skip to content

Commit 6accfac

Browse files
sunnamed434claude
andcommitted
docs: contrast reflection vs publicizer for non-public access
NonPublicized now reads the same non-public field (isDedicatedUGCInstalled) via reflection instead of public-only API, so the two plugins are a live reflection-vs-.Publicized comparison. README explains why publicizing wins: compile-checked, readable, and faster than string-keyed reflection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 598dc37 commit 6accfac

2 files changed

Lines changed: 51 additions & 36 deletions

File tree

NonPublicized/NonPublicizedExamplePlugin.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
1+
using System.Reflection;
12
using Rocket.Core.Logging;
23
using Rocket.Core.Plugins;
34
using SDG.Unturned;
45

56
namespace UnturnedRedistExample.NonPublicized
67
{
78
/// <summary>
8-
/// Uses the plain (non-publicized) redist. Only PUBLIC Unturned members are
9-
/// accessible. Compare with <c>../Publicized</c>, which can also read
10-
/// non-public members. No AllowUnsafeBlocks needed here.
9+
/// Uses the plain (non-publicized) redist. Public Unturned members are used
10+
/// directly; a NON-public member can only be reached via reflection — which
11+
/// is the cost of not publicizing. Compare with <c>../Publicized</c>, which
12+
/// reads the same field as a plain, compile-checked field access.
1113
/// </summary>
1214
public class NonPublicizedExamplePlugin : RocketPlugin
1315
{
1416
protected override void Load()
1517
{
16-
// Public Unturned API works fine with the plain redist:
17-
Logger.Log(
18-
$"[UnturnedRedistExample.NonPublicized] Loaded. " +
19-
$"Provider.maxPlayers = {Provider.maxPlayers} (public API).");
18+
// Public members would be trivial here (e.g. Provider.maxPlayers).
19+
// But isDedicatedUGCInstalled is NON-public: a direct
20+
// `Provider.isDedicatedUGCInstalled` does not even compile against the
21+
// plain redist (CS0117). Without the .Publicized package, reflection
22+
// is the only way in:
23+
FieldInfo field = typeof(Provider).GetField(
24+
"isDedicatedUGCInstalled",
25+
BindingFlags.NonPublic | BindingFlags.Static);
26+
bool ugcInstalled = (bool)field.GetValue(null);
2027

21-
// The line below would NOT compile here — the plain redist doesn't
22-
// expose non-public members, so the compiler can't see this field at
23-
// all. To read it, reference the .Publicized redist (see ../Publicized):
24-
//
25-
// var ugc = Provider.isDedicatedUGCInstalled;
26-
// // CS0117: 'Provider' does not contain a definition for 'isDedicatedUGCInstalled'
28+
Logger.Log(
29+
$"[UnturnedRedistExample.NonPublicized] Loaded. Read non-public " +
30+
$"isDedicatedUGCInstalled = {ugcInstalled} via REFLECTION — the name " +
31+
$"is a string (a rename breaks at runtime, not build) and every read " +
32+
$"pays a reflection + boxing cost.");
2733
}
2834

2935
protected override void Unload()

README.md

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,50 @@
11
# RocketModFix.Unturned.Redist.Example
22

3-
Two minimal [RocketMod](https://github.com/RocketModFix/RocketModFix) plugins that show how to consume the RocketModFix NuGet packages in an Unturned server plugin — and, side by side, exactly what the [`RocketModFix.Unturned.Redist`](https://github.com/RocketModFix/RocketModFix.Unturned.Redist) **`.Publicized`** package changes.
3+
Two minimal [RocketMod](https://github.com/RocketModFix/RocketModFix) plugins that read the **same non-public Unturned field two ways** — via reflection (the plain redist) and via the [`RocketModFix.Unturned.Redist`](https://github.com/RocketModFix/RocketModFix.Unturned.Redist) **`.Publicized`** package (a plain field access) — so you can see *why* publicizing is worth it.
44

55
> These are example **consumer** plugins — their namespaces (`UnturnedRedistExample.*`) are what *you'd* write in your own plugin; they just **reference** the RocketModFix packages.
66
77
## The two plugins
88

9-
| Project | Redist package referenced | `AllowUnsafeBlocks` | Reads non-public members? |
10-
| --- | --- | --- | --- |
11-
| [`NonPublicized/`](NonPublicized) | `RocketModFix.Unturned.Redist.Server` | no | ❌ public API only |
12-
| [`Publicized/`](Publicized) | `RocketModFix.Unturned.Redist.Server.Publicized` | **yes** ||
9+
Both reference the **same game build** (`3.26.3.3`) and both read `SDG.Unturned.Provider.isDedicatedUGCInstalled` — a field that is **non-public** in the stock `Assembly-CSharp.dll`. The only difference is *how*:
1310

14-
Both reference the **same game build** (`3.26.3.3`) and are otherwise identical RocketMod plugins. The redist package is the only meaningful difference — that's the whole lesson.
11+
| Project | Redist package | How it reads the non-public field |
12+
| --- | --- | --- |
13+
| [`NonPublicized/`](NonPublicized) | `RocketModFix.Unturned.Redist.Server` | **Reflection** — string-keyed, not compile-checked, slower |
14+
| [`Publicized/`](Publicized) | `RocketModFix.Unturned.Redist.Server.Publicized` (+ `AllowUnsafeBlocks`) | **Direct field access** — compile-checked, fast |
1515

16-
### `NonPublicized` — public API only
16+
## Why publicize instead of reflection?
1717

18-
Reads a **public** member and logs it. The non-public member is shown commented out, because it does **not** compile against the plain redist:
18+
A direct `Provider.isDedicatedUGCInstalled` **doesn't compile** against the plain redist — the package simply doesn't expose non-public members:
1919

20-
```csharp
21-
Logger.Log($"... Provider.maxPlayers = {Provider.maxPlayers} (public API).");
22-
23-
// var ugc = Provider.isDedicatedUGCInstalled;
24-
// // CS0117: 'Provider' does not contain a definition for 'isDedicatedUGCInstalled'
20+
```
21+
CS0117: 'Provider' does not contain a definition for 'isDedicatedUGCInstalled'
2522
```
2623

27-
The plain redist doesn't expose non-public members, so the compiler can't see the field at all.
24+
So without publicizing, your only option is **reflection** ([`NonPublicized/`](NonPublicized/NonPublicizedExamplePlugin.cs)):
2825

29-
### `Publicized` — non-public members too
26+
```csharp
27+
FieldInfo field = typeof(Provider).GetField(
28+
"isDedicatedUGCInstalled", BindingFlags.NonPublic | BindingFlags.Static);
29+
bool ugcInstalled = (bool)field.GetValue(null);
30+
```
31+
32+
Downsides:
33+
- **Not compile-checked.** The member name is a string — a rename or typo compiles fine and throws at **runtime** (`NullReferenceException` on the missing `field`), on your live server.
34+
- **Harder to read.** No IntelliSense, casts everywhere, three lines for one read.
35+
- **Slower.** Every access pays a reflection lookup + boxing; even caching the `FieldInfo` stays indirect.
3036

31-
Reads `SDG.Unturned.Provider.isDedicatedUGCInstalled`**non-public** in the stock `Assembly-CSharp.dll`:
37+
The **`.Publicized`** package ([`Publicized/`](Publicized/PublicizedExamplePlugin.cs)) rewrites that member to public, so the same read is one normal line:
3238

3339
```csharp
34-
var ugcInstalled = Provider.isDedicatedUGCInstalled; // compiles via the .Publicized package
35-
Logger.Log($"... isDedicatedUGCInstalled = {ugcInstalled} ...");
40+
bool ugcInstalled = Provider.isDedicatedUGCInstalled;
3641
```
3742

38-
This compiles only because the `.Publicized` package rewrites that member to public, and runs only because the `.csproj` sets `<AllowUnsafeBlocks>true</AllowUnsafeBlocks>` (which lets Unturned's Mono runtime skip the access check).
43+
- **Compile-checked** — a rename breaks the *build*, not your server.
44+
- **Reads like ordinary code** — full IntelliSense, no casts.
45+
- **Plain field-access speed.**
46+
47+
The one requirement: set `<AllowUnsafeBlocks>true</AllowUnsafeBlocks>` in the `.csproj`, which lets Unturned's Mono runtime skip the access check on the originally-private member.
3948

4049
## Packages used (both plugins)
4150

@@ -59,14 +68,14 @@ Builds both projects (via `UnturnedRedistExample.sln`); the DLLs land in each pr
5968

6069
1. Install [RocketModFix](https://github.com/RocketModFix/RocketModFix#installation) on an Unturned dedicated server (copy the `Rocket.Unturned` folder into `Modules/`) and start it once so it generates `Servers/<ServerID>/Rocket/`.
6170
2. Copy either built DLL into `Servers/<ServerID>/Rocket/Plugins/` (DLLs go directly in `Plugins/`, not a subfolder).
62-
3. Start the server and watch the console:
71+
3. Start the server and watch the console — both read the same value, the hard way and the easy way:
6372

6473
```
65-
[UnturnedRedistExample.NonPublicized] Loaded. Provider.maxPlayers = 24 (public API).
66-
[UnturnedRedistExample.Publicized] Loaded. Read NON-public Provider.isDedicatedUGCInstalled = False via the .Publicized redist.
74+
[UnturnedRedistExample.NonPublicized] Loaded. Read non-public isDedicatedUGCInstalled = False via REFLECTION ...
75+
[UnturnedRedistExample.Publicized] Loaded. Read NON-public Provider.isDedicatedUGCInstalled = False via the .Publicized redist.
6776
```
6877

69-
The `Publicized` line is the key one: it proves the non-public member was read **at runtime** (so `AllowUnsafeBlocks` did its job). If publicization were broken you'd get a `FieldAccessException` ("is inaccessible") instead.
78+
Seeing those lines confirms each plugin **loaded** and read the non-public member **at runtime**. (For the publicized one, that also proves `AllowUnsafeBlocks` did its job — if publicization were broken you'd get a `FieldAccessException` instead.)
7079

7180
## License
7281

0 commit comments

Comments
 (0)