Fresh start: replace with naxIO/netfox-cs-sample foundation
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Architecture Overview
|
||||
|
||||
See PROJECT_PLAN.md for the full build plan.
|
||||
This document will be expanded as the project progresses.
|
||||
@@ -1,170 +0,0 @@
|
||||
# Tactical Shooter — Art Style Guide
|
||||
|
||||
> **Phase 3 deliverable · Modular wall/floor kit, PBR materials at 1K**
|
||||
> Valorant-direction competitive FPS art direction · Godot 4 Forward+ renderer
|
||||
|
||||
---
|
||||
|
||||
## 1. Visual Direction
|
||||
|
||||
### Target: Valorant-Tier, Not AAA
|
||||
|
||||
- **Clean silhouettes** — geometry reads instantly at combat ranges. Avoid noisy trim/paneling.
|
||||
- **Team-color zones** — CT (blue) and T (red/orange) territories are visually distinct at a glance. Color is used for gameplay read, not decorative variety.
|
||||
- **Low-fi surface detail** — we suggest roughness and normal variation rather than carving it into geometry. 1K textures carry the detail budget.
|
||||
- **No photorealism** — stylised realism with clear material categories: concrete, metal, tile, painted surfaces. Each category is immediately identifiable by its roughness/specular signature.
|
||||
|
||||
### What This Game Looks Like
|
||||
|
||||
| Aspect | Choice | Rationale |
|
||||
|--------|--------|-----------|
|
||||
| Lighting | Baked lightmaps + reflection probes | No real-time GI cost |
|
||||
| Shadows | Baked for static, single cascade for dynamic | 60fps target on GTX 1050 |
|
||||
| Reflections | Reflection probes at junctions | No SSR/SSAO cost |
|
||||
| Post-processing | Tonemap only (ACES), no bloom, no DoF | Competitive clarity |
|
||||
| Fog | Minimal distance fog | Don't hide silhouette readability |
|
||||
| Textures | 1K PBR (1024 max), no 2K+ | VRAM budget: 2GB texture pool |
|
||||
|
||||
---
|
||||
|
||||
## 2. Modular Grid & Dimensions
|
||||
|
||||
The kit is built on a **2.56m (256cm) base grid** — close to 8ft imperial, powers-of-two friendly for texture tiling.
|
||||
|
||||
### Standard Dimensions (meters)
|
||||
|
||||
| Piece | Width | Height | Depth | Notes |
|
||||
|-------|-------|--------|-------|-------|
|
||||
| Wall straight | 2.56 | 2.56 | 0.16 | Full wall panel |
|
||||
| Wall corner (L) | 2.56 × 2 leg | 2.56 | 0.16 | 90° outside corner |
|
||||
| Wall doorway | 2.56 | 2.56 | 0.16 | 0.80m × 2.00m door cutout |
|
||||
| Wall window | 2.56 | 2.56 | 0.16 | 1.20m × 0.80m window cutout |
|
||||
| Wall endcap | 0.32 | 2.56 | 0.16 | Edge trim / termination |
|
||||
| Floor slab | 2.56 | 0.08 | 2.56 | Thin slab |
|
||||
| Pillar (square) | 0.64 | 2.56 | 0.64 | Structural column |
|
||||
| Ceiling beam | 3.84 | 0.32 | 0.32 | Metal beam, 1.5U length |
|
||||
| Accent panel | 1.28 | 0.64 | 0.08 | Team-color wall accent |
|
||||
|
||||
### Grid Alignment
|
||||
|
||||
- All pieces snap to a 1.28m (half-unit) grid in the map editor.
|
||||
- Wall thickness is always 0.16m. Floors are 0.08m.
|
||||
- Doorway bottom is flush with floor level. Window center is 1.58m from floor (eye level).
|
||||
- Pillars occupy 0.64m × 0.64m on the floor grid (¼ of a full tile).
|
||||
|
||||
---
|
||||
|
||||
## 3. Material Library
|
||||
|
||||
### Material Standards
|
||||
|
||||
Every material is a **StandardMaterial3D** with PBR textures at **1024×1024 (1K)**:
|
||||
|
||||
| Texture Map | Format | Notes |
|
||||
|-------------|--------|-------|
|
||||
| Base Color | sRGB PNG | Diffuse/albedo with subtle colour variation |
|
||||
| Normal | Linear PNG | From height map via Sobel, strength scaled per material |
|
||||
| Roughness | Linear PNG (grayscale) | White = rough (255), Black = smooth (0) |
|
||||
| Metallic | Linear PNG (grayscale) | White = metal (255), Black = dielectric (0) |
|
||||
|
||||
Texture filter: **Linear** (no mipmaps for modular kit; mipmap cost is unnecessary at 1K on a 2.56m tile with the camera ≤40m from most surfaces).
|
||||
|
||||
### Material Palette (7 materials)
|
||||
|
||||
| # | Name | Roughness | Metallic | Normal Scale | UV Tiling | Use Case |
|
||||
|---|------|-----------|----------|--------------|-----------|----------|
|
||||
| 1 | `wall_concrete_01` | 0.85 | 0.0 | 0.5 | 1.0 | Primary wall — light grey concrete |
|
||||
| 2 | `wall_concrete_02` | 0.75 | 0.0 | 0.4 | 1.0 | Accent wall — darker grey |
|
||||
| 3 | `floor_tile_01` | 0.60 | 0.0 | 1.0 | 2.0 | Interior — ceramic checkered tile |
|
||||
| 4 | `floor_concrete_01` | 0.90 | 0.0 | 0.8 | 2.0 | Industrial — dark warehouse floor |
|
||||
| 5 | `metal_structural_01` | 0.30 | 1.0 | 0.3 | 1.0 | Beams, vents, supports |
|
||||
| 6 | `accent_team_blue` | 0.35 | 0.5 | 0.3 | 1.0 | CT territory — blue painted panels |
|
||||
| 7 | `accent_team_red` | 0.35 | 0.5 | 0.3 | 1.0 | T territory — red/orange painted panels |
|
||||
|
||||
### Team Color Usage
|
||||
|
||||
- **CT (Blue)**: RGB ~(40, 80, 200) — deep blue, low saturation. Applied to accent panels, not primary walls.
|
||||
- **T (Red/Orange)**: RGB ~(200, 60, 30) — burnt red-orange. Applied to accent panels, not primary walls.
|
||||
- Neutral zones (uncontested) use `wall_concrete_01/02` and `floor_concrete_01`.
|
||||
- Spike plant zones / bomb sites get team-colour floor accents.
|
||||
|
||||
---
|
||||
|
||||
## 4. Technical Specifications
|
||||
|
||||
### Texture Budget
|
||||
|
||||
| Category | Count | Resolution | VRAM (approx) |
|
||||
|----------|-------|------------|---------------|
|
||||
| Wall materials (2) | 8 maps | 1K | ~21 MB |
|
||||
| Floor materials (2) | 8 maps | 1K | ~21 MB |
|
||||
| Metal (1) | 4 maps | 1K | ~10 MB |
|
||||
| Team accents (2) | 8 maps | 1K | ~21 MB |
|
||||
| **Total (7 materials)** | **28 maps** | **1K** | **~75 MB** |
|
||||
|
||||
VRAM headroom for lighting data, shadow maps, and models: ~1.9 GB of the 2 GB budget.
|
||||
|
||||
### Performance Targets (per the visuals architecture)
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Frame time | 16ms (60fps) |
|
||||
| GPU budget | 8ms |
|
||||
| CPU budget | 6ms (6ms headroom for gameplay) |
|
||||
| Draw calls | 1500–2000/frame |
|
||||
| LOD0 tris/wall | 500–2000 |
|
||||
|
||||
### LOD Strategy (for future mesh pass)
|
||||
|
||||
When modular CSG pieces are replaced with custom meshes:
|
||||
|
||||
| LOD | Distance | Triangle Budget | Notes |
|
||||
|-----|----------|-----------------|-------|
|
||||
| LOD0 | 0–15m | 100% | Full detail |
|
||||
| LOD1 | 15–40m | 50% | Remove panel gaps, merge edges |
|
||||
| LOD2 | 40–80m | 25% | Planar collapse, decimate |
|
||||
| LOD3 | 80m+ | Cull | Not visible at competitive sightlines |
|
||||
|
||||
---
|
||||
|
||||
## 5. Lighting Guidelines (for t_p3_lighting)
|
||||
|
||||
- **LightmapGI** on all static geometry. Texel density: 4–8 per unit on walls, 8–16 on focal surfaces.
|
||||
- **Reflection probes** at corridor junctions, open plaza areas, chokepoints. Probe box size: 4U (10.24m) for corridors, 8U+ for open areas.
|
||||
- **No SDFGI**, no real-time GI. 1-2 dynamic directional lights maximum (sun + maybe a single fill).
|
||||
- Lightmap UV2: all modular CSG pieces export lightmap UVs automatically.
|
||||
|
||||
---
|
||||
|
||||
## 6. Mapping Guidelines (for map makers)
|
||||
|
||||
- **Start every map** by placing a `KitDemo` scene to see the palette.
|
||||
- Snap walls to the 1.28m half-grid. Full grid is 2.56m.
|
||||
- Doorway walls are exactly 2.56m wide × 2.56m tall — stack a wall piece on top if headroom is needed.
|
||||
- Use `wall_concrete_02` for interior partitions, `wall_concrete_01` for exterior-facing walls.
|
||||
- Cover all floor areas with `floor_tile_01` (interiors) or `floor_concrete_01` (exteriors/warehouses).
|
||||
- Team accent panels are decorative only — they do not affect gameplay collision or hitbox.
|
||||
|
||||
---
|
||||
|
||||
## 7. Export & Build Pipeline
|
||||
|
||||
- Client project lives in `client/` with its own `project.godot`.
|
||||
- Modular kit scenes are under `client/assets/scenes/modular/`.
|
||||
- Materials under `client/assets/materials/`.
|
||||
- Textures under `client/assets/textures/<material_name>/`.
|
||||
- Textures are generated by `client/assets/textures/generate_textures.py`.
|
||||
- Materials are generated by `client/assets/materials/generate_materials.py`.
|
||||
- Scenes are generated by `client/assets/scenes/modular/generate_scenes.py`.
|
||||
|
||||
### Adding a New Material
|
||||
|
||||
1. Add a generator function in `generate_textures.py`
|
||||
2. Run it to produce 1K PBR maps
|
||||
3. Add a `.tres` entry in `generate_materials.py`
|
||||
4. Run it to produce the material resource
|
||||
5. Optionally create a scene using the new material
|
||||
|
||||
---
|
||||
|
||||
*This guide is a living document — update as the visual direction evolves.*
|
||||
@@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="384"
|
||||
height="256"
|
||||
viewBox="0 0 101.6 67.733332"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-1st-anniversary-banner.svg"
|
||||
inkscape:export-filename="netfox-anniversary-banner.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="126.57211"
|
||||
inkscape:cy="92.630986"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<pattern
|
||||
inkscape:collect="always"
|
||||
xlink:href="#pattern6-7"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern31"
|
||||
patternTransform="translate(190,350)" />
|
||||
<pattern
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="29"
|
||||
height="20"
|
||||
patternTransform="translate(190,350)"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
style="fill:#000000"
|
||||
id="pattern6-7"
|
||||
inkscape:label="Abstract 1"
|
||||
inkscape:collect="always"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-dasharray:none;paint-order:stroke markers fill;stop-color:#000000"
|
||||
d="M 0,5 C 19,5 10,15 29,15"
|
||||
id="path4-2"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</pattern>
|
||||
<linearGradient
|
||||
id="swatch25"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#8700ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop25" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Logo"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:0.822685;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
d="M 16.933326,25.374936 V 42.358395 L 33.816533,59.241602 50.799992,42.258144 42.333321,25.374936 Z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
d="m 33.866666,25.374936 -8.46667,16.933341 -8.46667,-16.933341 z"
|
||||
fill="#5fff97"
|
||||
id="path1"
|
||||
style="stroke-width:4.23333" />
|
||||
<path
|
||||
d="M 50.799992,42.308277 42.333322,25.374936 33.866666,42.308277 Z"
|
||||
fill="#ff5f5f"
|
||||
id="path2"
|
||||
style="stroke-width:4.23333" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect2"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="47.87698"
|
||||
y="11.955959"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect3"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="59.850655"
|
||||
y="-0.017714709"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect4"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="62.175529"
|
||||
y="14.351729"
|
||||
transform="rotate(45)" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:11.2889px;fill:#ff8100;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
x="54.283672"
|
||||
y="46.337658"
|
||||
id="text1"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan1"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.2889px;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';stroke-width:0.529167"
|
||||
x="54.283672"
|
||||
y="46.337658">netfox</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Hat"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path16"
|
||||
style="display:inline;fill:#8700ff;fill-opacity:1;stroke:#8800ff;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 36.380864,56.677274 8.967201,-0.02336"
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:label="party-blower-handle" />
|
||||
<path
|
||||
sodipodi:type="spiral"
|
||||
style="display:inline;fill:none;fill-rule:evenodd;stroke:#c380ff;stroke-width:4.23333;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path17"
|
||||
sodipodi:cx="42.308262"
|
||||
sodipodi:cy="56.665592"
|
||||
sodipodi:expansion="1"
|
||||
sodipodi:revolution="3"
|
||||
sodipodi:radius="3.7953792"
|
||||
sodipodi:argument="-18.849556"
|
||||
sodipodi:t0="0"
|
||||
d="m 42.308262,56.665592 c 0.190294,0 0.09388,0.256513 0,0.316282 -0.254421,0.161969 -0.554562,-0.07123 -0.632563,-0.316282 -0.139526,-0.438332 0.214595,-0.860149 0.632563,-0.948845 0.613385,-0.130164 1.171185,0.358597 1.265126,0.948845 0.125209,0.786708 -0.502655,1.484327 -1.265126,1.581408 -0.959492,0.122166 -1.798514,-0.646699 -1.89769,-1.581408 -0.120115,-1.132057 0.790727,-2.113297 1.89769,-2.213971 1.304517,-0.11864 2.428455,0.934743 2.530253,2.213971 0.117529,1.47692 -1.078751,2.743862 -2.530253,2.846535 -1.649289,0.116663 -3.059443,-1.222754 -3.162816,-2.846535 -0.115969,-1.821636 1.366753,-3.375151 3.162816,-3.479097 1.99397,-0.115401 3.690955,1.51075 3.795379,3.479097"
|
||||
transform="rotate(90,45.731692,56.281965)"
|
||||
inkscape:label="party-blower-tongue" />
|
||||
<path
|
||||
id="path14"
|
||||
style="display:inline;mix-blend-mode:normal;fill:#8700ff;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 16.933314,25.374936 29.633317,0.050124 42.333321,25.374936 Z"
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:label="hat" />
|
||||
<path
|
||||
id="path29"
|
||||
style="opacity:0.25;mix-blend-mode:normal;fill:url(#pattern31);fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 16.933314,25.374936 29.633317,0.050124 42.333321,25.374936 Z"
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:label="hat-pattern" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Candle"
|
||||
style="display:none">
|
||||
<path
|
||||
id="path7"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linejoin:round"
|
||||
d="m 33.854122,21.154135 -4.220801,-4.220802 4.233335,-8.4416035 4.233332,8.4416035 -4.220805,4.220802 z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
id="path8"
|
||||
style="fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linejoin:round"
|
||||
d="m 33.860388,21.160399 -2.1104,-2.110401 2.116667,-4.220801 2.116666,4.220801 -2.110402,2.110401 z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 67.733332 67.733332"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-1st-anniversary.svg"
|
||||
inkscape:export-filename="netfox-candle.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="2"
|
||||
inkscape:cx="106"
|
||||
inkscape:cy="122"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer3" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<pattern
|
||||
inkscape:collect="always"
|
||||
xlink:href="#pattern6-7"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="pattern31"
|
||||
patternTransform="translate(190,350)" />
|
||||
<pattern
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="29"
|
||||
height="20"
|
||||
patternTransform="translate(190,350)"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
style="fill:#000000"
|
||||
id="pattern6-7"
|
||||
inkscape:label="Abstract 1"
|
||||
inkscape:collect="always"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-dasharray:none;paint-order:stroke markers fill;stop-color:#000000"
|
||||
d="M 0,5 C 19,5 10,15 29,15"
|
||||
id="path4-2"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</pattern>
|
||||
<linearGradient
|
||||
id="swatch25"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#8700ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop25" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Logo"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:0.822685;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
d="M 16.933326,25.374936 V 42.358395 L 33.816533,59.241602 50.799992,42.258144 42.333321,25.374936 Z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
d="m 33.866666,25.374936 -8.46667,16.933341 -8.46667,-16.933341 z"
|
||||
fill="#5fff97"
|
||||
id="path1"
|
||||
style="stroke-width:4.23333" />
|
||||
<path
|
||||
d="M 50.799992,42.308277 42.333322,25.374936 33.866666,42.308277 Z"
|
||||
fill="#ff5f5f"
|
||||
id="path2"
|
||||
style="stroke-width:4.23333" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect2"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="47.87698"
|
||||
y="11.955959"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect3"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="59.850655"
|
||||
y="-0.017714709"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect4"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="62.175529"
|
||||
y="14.351729"
|
||||
transform="rotate(45)" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Hat"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path16"
|
||||
style="display:inline;fill:#8700ff;fill-opacity:1;stroke:#8800ff;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 36.380864,56.677274 8.967201,-0.02336"
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:label="party-blower-handle" />
|
||||
<path
|
||||
sodipodi:type="spiral"
|
||||
style="display:inline;fill:none;fill-rule:evenodd;stroke:#c380ff;stroke-width:4.23333;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path17"
|
||||
sodipodi:cx="42.308262"
|
||||
sodipodi:cy="56.665592"
|
||||
sodipodi:expansion="1"
|
||||
sodipodi:revolution="3"
|
||||
sodipodi:radius="3.7953792"
|
||||
sodipodi:argument="-18.849556"
|
||||
sodipodi:t0="0"
|
||||
d="m 42.308262,56.665592 c 0.190294,0 0.09388,0.256513 0,0.316282 -0.254421,0.161969 -0.554562,-0.07123 -0.632563,-0.316282 -0.139526,-0.438332 0.214595,-0.860149 0.632563,-0.948845 0.613385,-0.130164 1.171185,0.358597 1.265126,0.948845 0.125209,0.786708 -0.502655,1.484327 -1.265126,1.581408 -0.959492,0.122166 -1.798514,-0.646699 -1.89769,-1.581408 -0.120115,-1.132057 0.790727,-2.113297 1.89769,-2.213971 1.304517,-0.11864 2.428455,0.934743 2.530253,2.213971 0.117529,1.47692 -1.078751,2.743862 -2.530253,2.846535 -1.649289,0.116663 -3.059443,-1.222754 -3.162816,-2.846535 -0.115969,-1.821636 1.366753,-3.375151 3.162816,-3.479097 1.99397,-0.115401 3.690955,1.51075 3.795379,3.479097"
|
||||
transform="rotate(90,45.731692,56.281965)"
|
||||
inkscape:label="party-blower-tongue" />
|
||||
<path
|
||||
id="path14"
|
||||
style="display:inline;mix-blend-mode:normal;fill:#8700ff;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 16.933314,25.374936 29.633317,0.050124 42.333321,25.374936 Z"
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:label="hat" />
|
||||
<path
|
||||
id="path29"
|
||||
style="opacity:0.25;mix-blend-mode:normal;fill:url(#pattern31);fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 16.933314,25.374936 29.633317,0.050124 42.333321,25.374936 Z"
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:label="hat-pattern" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Candle"
|
||||
style="display:none">
|
||||
<path
|
||||
id="path7"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linejoin:round"
|
||||
d="m 33.854122,21.154135 -4.220801,-4.220802 4.233335,-8.4416035 4.233332,8.4416035 -4.220805,4.220802 z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
id="path8"
|
||||
style="fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:0.79375;stroke-linejoin:round"
|
||||
d="m 33.860388,21.160399 -2.1104,-2.110401 2.116667,-4.220801 2.116666,4.220801 -2.110402,2.110401 z"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 896 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="272"
|
||||
height="128"
|
||||
viewBox="0 0 71.966666 33.866666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-banner-hor-alt.svg"
|
||||
inkscape:export-filename="netfox-banner.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#bfbfbf"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="2.8284272"
|
||||
inkscape:cx="112.78353"
|
||||
inkscape:cy="52.3259"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g1-5" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g1">
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:0.822685;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
d="m 0,0 12.009119,12.009119 h 23.87646 v -24.018238 l -17.92507,-5.95139 z"
|
||||
transform="rotate(45)"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
d="M 16.93334,0 8.4666699,16.93334 0,0 Z"
|
||||
fill="#5fff97"
|
||||
id="path1"
|
||||
style="stroke-width:4.23333" />
|
||||
<path
|
||||
d="M 33.866666,16.93334 25.399996,0 16.93334,16.93334 Z"
|
||||
fill="#ff5f5f"
|
||||
id="path2"
|
||||
style="stroke-width:4.23333" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect2"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="17.96052"
|
||||
y="5.9868398"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect3"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="29.934195"
|
||||
y="-5.9868345"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect4"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="32.259071"
|
||||
y="8.3826094"
|
||||
transform="rotate(45)" />
|
||||
</g>
|
||||
<g
|
||||
id="g1-5"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,-190.26843,-380.00464)">
|
||||
<text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="851.6261"
|
||||
y="1516.0247"
|
||||
style="font-weight:300;font-size:42.6667px;font-family:Oxanium;fill:#ff8100">net<tspan
|
||||
class="cls-2"
|
||||
id="tspan1"
|
||||
style="font-size:42.6667px;fill:#000000;fill-opacity:1">fox</tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="272"
|
||||
height="128"
|
||||
viewBox="0 0 71.966666 33.866666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-banner-hor-black.svg"
|
||||
inkscape:export-filename="netfox-banner.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#bfbfbf"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="2.8284272"
|
||||
inkscape:cx="125.1579"
|
||||
inkscape:cy="44.901279"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><g
|
||||
id="g1-5"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,-190.26843,-380.00464)"
|
||||
style="fill:#000000;fill-opacity:1"><text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="851.6261"
|
||||
y="1516.0247"
|
||||
style="font-weight:300;font-size:42.6667px;font-family:Oxanium;fill:#000000;fill-opacity:1">net<tspan
|
||||
class="cls-2"
|
||||
id="tspan1"
|
||||
style="font-size:42.6667px;fill:#000000;fill-opacity:1">fox</tspan></text></g><g
|
||||
id="g2"><path
|
||||
id="rect1-3"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.80983;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 0.3180025,0.21523374 V 16.933327 l 14.0952875,14.095287 2.524119,-2.524627 2.524118,2.524627 14.144631,-14.144631 h -8.285032 l 2.524119,2.52412 -2.524119,2.524627 -2.524118,-2.524627 2.524118,-2.52412 H 16.986751 L 25.321127,0.21523374 H 16.986752 L 8.6523775,16.883984 11.176497,19.408103 8.6523776,21.932731 6.1282584,19.408103 8.6523775,16.883984 Z m 25.0031245,0 8.318096,16.63568426 0.01628,-0.01628 z" /><path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.521026;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6;stroke-opacity:1"
|
||||
d="M 16.88398,33.606161 0.2605135,16.982693 V 0.26051574 H 25.269743 L 33.606158,16.883983 Z"
|
||||
id="path3" /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="272"
|
||||
height="128"
|
||||
viewBox="0 0 71.966666 33.866666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-banner-hor-white.svg"
|
||||
inkscape:export-filename="netfox-banner.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#404040"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="2.8284272"
|
||||
inkscape:cx="125.1579"
|
||||
inkscape:cy="44.901279"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><g
|
||||
id="g1-5"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,-190.26843,-380.00464)"
|
||||
style="fill:#ffffff;fill-opacity:1"><text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="851.6261"
|
||||
y="1516.0247"
|
||||
style="font-weight:300;font-size:42.6667px;font-family:Oxanium;fill:#ffffff;fill-opacity:1">net<tspan
|
||||
class="cls-2"
|
||||
id="tspan1"
|
||||
style="font-size:42.6667px;fill:#ffffff;fill-opacity:1">fox</tspan></text></g><g
|
||||
id="g2"
|
||||
transform="translate(-3.439584,27.781252)"><g
|
||||
inkscape:label="Layer 1"
|
||||
id="layer1-5"
|
||||
transform="translate(3.4395835,-27.781249)"><path
|
||||
id="rect1-3"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.80983;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 0.3180025,0.21523374 V 16.933327 l 14.0952875,14.095287 2.524119,-2.524627 2.524118,2.524627 14.144631,-14.144631 h -8.285032 l 2.524119,2.52412 -2.524119,2.524627 -2.524118,-2.524627 2.524118,-2.52412 H 16.986751 L 25.321127,0.21523374 H 16.986752 L 8.6523775,16.883984 11.176497,19.408103 8.6523776,21.932731 6.1282584,19.408103 8.6523775,16.883984 Z m 25.0031245,0 8.318096,16.63568426 0.01628,-0.01628 z" /><path
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.521026;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6;stroke-opacity:1"
|
||||
d="M 16.88398,33.606161 0.2605135,16.982693 V 0.26051574 H 25.269743 L 33.606158,16.883983 Z"
|
||||
id="path3" /></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="272"
|
||||
height="128"
|
||||
viewBox="0 0 71.966666 33.866666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-banner-hor.svg"
|
||||
inkscape:export-filename="netfox-banner.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#404040"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="2.8284272"
|
||||
inkscape:cx="118.08683"
|
||||
inkscape:cy="30.759144"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g1">
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:0.822685;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
d="m 0,0 12.009119,12.009119 h 23.87646 v -24.018238 l -17.92507,-5.95139 z"
|
||||
transform="rotate(45)"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
d="M 16.93334,0 8.4666699,16.93334 0,0 Z"
|
||||
fill="#5fff97"
|
||||
id="path1"
|
||||
style="stroke-width:4.23333" />
|
||||
<path
|
||||
d="M 33.866666,16.93334 25.399996,0 16.93334,16.93334 Z"
|
||||
fill="#ff5f5f"
|
||||
id="path2"
|
||||
style="stroke-width:4.23333" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect2"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="17.96052"
|
||||
y="5.9868398"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect3"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="29.934195"
|
||||
y="-5.9868345"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect4"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="32.259071"
|
||||
y="8.3826094"
|
||||
transform="rotate(45)" />
|
||||
</g>
|
||||
<g
|
||||
id="g1-5"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,-190.26843,-380.00464)">
|
||||
<text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="851.6261"
|
||||
y="1516.0247"
|
||||
style="font-weight:300;font-size:42.6667px;font-family:Oxanium;fill:#ff8100">net<tspan
|
||||
class="cls-2"
|
||||
id="tspan1"
|
||||
style="font-size:42.6667px;fill:#ffffff;fill-opacity:1">fox</tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="netfox-banner-ver-alt.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:x="adobe:ns:meta/"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#bfbfbf"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="3.3046876"
|
||||
inkscape:cx="102.27896"
|
||||
inkscape:cy="132.8416"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><metadata
|
||||
id="metadata1"><?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta
|
||||
x:xmptk="Adobe XMP Core 5.6-c142 79.160924, 2017/07/13-01:06:39 "><rdf:RDF><rdf:Description
|
||||
rdf:about="" /></rdf:RDF></x:xmpmeta><?xpacket end="w"?></metadata><defs
|
||||
id="defs1"><style
|
||||
id="style1">
|
||||
.cls-1 {
|
||||
font-size: 250px;
|
||||
fill: #ff8100;
|
||||
font-family: Oxanium;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style></defs><text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="62.911915"
|
||||
y="211.78668"
|
||||
style="font-size:42.6667px">net<tspan
|
||||
style="font-size:42.6667px;fill:#000000;fill-opacity:1"
|
||||
id="tspan2">fox</tspan></text><g
|
||||
inkscape:label="Layer 1"
|
||||
id="layer1"
|
||||
transform="matrix(3.7795276,0,0,3.7795276,64,44.213318)"><path
|
||||
id="rect1"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:0.822685;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
d="m 0,0 12.009119,12.009119 h 23.87646 v -24.018238 l -17.92507,-5.95139 z"
|
||||
transform="rotate(45)"
|
||||
sodipodi:nodetypes="cccccc" /><path
|
||||
d="M 16.93334,0 8.4666699,16.93334 0,0 Z"
|
||||
fill="#5fff97"
|
||||
id="path1"
|
||||
style="stroke-width:4.23333" /><path
|
||||
d="M 33.866666,16.93334 25.399996,0 16.93334,16.93334 Z"
|
||||
fill="#ff5f5f"
|
||||
id="path2"
|
||||
style="stroke-width:4.23333" /><rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect2"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="17.96052"
|
||||
y="5.9868398"
|
||||
transform="rotate(45)" /><rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect3"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="29.934195"
|
||||
y="-5.9868345"
|
||||
transform="rotate(45)" /><rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect4"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="32.259071"
|
||||
y="8.3826094"
|
||||
transform="rotate(45)" /></g></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="netfox-banner-ver-black.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:x="adobe:ns:meta/"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#bfbfbf"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="2.336767"
|
||||
inkscape:cx="93.291287"
|
||||
inkscape:cy="138.22516"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1-6" /><metadata
|
||||
id="metadata1"><?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta
|
||||
x:xmptk="Adobe XMP Core 5.6-c142 79.160924, 2017/07/13-01:06:39 "><rdf:RDF><rdf:Description
|
||||
rdf:about="" /></rdf:RDF></x:xmpmeta><?xpacket end="w"?></metadata><defs
|
||||
id="defs1"><style
|
||||
id="style1">
|
||||
.cls-1 {
|
||||
font-size: 250px;
|
||||
fill: #ff8100;
|
||||
font-family: Oxanium;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style></defs><text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="62.911915"
|
||||
y="211.78668"
|
||||
style="font-size:42.6667px;fill:#000000;fill-opacity:1">net<tspan
|
||||
style="font-size:42.6667px;fill:#000000;fill-opacity:1"
|
||||
id="tspan2">fox</tspan></text><g
|
||||
inkscape:label="Layer 1"
|
||||
id="layer1-6"
|
||||
transform="matrix(3.7795276,0,0,3.7795276,63.798132,44.399864)"><path
|
||||
id="rect1-2"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.80983;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 0.3180025,0.21523374 V 16.933327 l 14.0952875,14.095287 2.524119,-2.524627 2.524118,2.524627 14.144631,-14.144631 h -8.285032 l 2.524119,2.52412 -2.524119,2.524627 -2.524118,-2.524627 2.524118,-2.52412 H 16.986751 L 25.321127,0.21523374 H 16.986752 L 8.6523775,16.883984 11.176497,19.408103 8.6523776,21.932731 6.1282584,19.408103 8.6523775,16.883984 Z m 25.0031245,0 8.318096,16.63568426 0.01628,-0.01628 z" /><path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.521026;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6;stroke-opacity:1"
|
||||
d="M 16.88398,33.606161 0.2605135,16.982693 V 0.26051574 H 25.269743 L 33.606158,16.883983 Z"
|
||||
id="path3" /></g></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="netfox-banner-ver-white.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:x="adobe:ns:meta/"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#404040"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="2.336767"
|
||||
inkscape:cx="93.291287"
|
||||
inkscape:cy="138.22516"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><metadata
|
||||
id="metadata1"><?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta
|
||||
x:xmptk="Adobe XMP Core 5.6-c142 79.160924, 2017/07/13-01:06:39 "><rdf:RDF><rdf:Description
|
||||
rdf:about="" /></rdf:RDF></x:xmpmeta><?xpacket end="w"?></metadata><defs
|
||||
id="defs1"><style
|
||||
id="style1">
|
||||
.cls-1 {
|
||||
font-size: 250px;
|
||||
fill: #ff8100;
|
||||
font-family: Oxanium;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style></defs><text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="62.911915"
|
||||
y="211.78668"
|
||||
style="font-size:42.6667px;fill:#ffffff;fill-opacity:1">net<tspan
|
||||
style="font-size:42.6667px;fill:#ffffff;fill-opacity:1"
|
||||
id="tspan2">fox</tspan></text><g
|
||||
inkscape:label="Layer 1"
|
||||
id="layer1-6"
|
||||
transform="matrix(3.7795276,0,0,3.7795276,63.798132,44.399864)"><path
|
||||
id="rect1-2"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.80983;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 0.3180025,0.21523374 V 16.933327 l 14.0952875,14.095287 2.524119,-2.524627 2.524118,2.524627 14.144631,-14.144631 h -8.285032 l 2.524119,2.52412 -2.524119,2.524627 -2.524118,-2.524627 2.524118,-2.52412 H 16.986751 L 25.321127,0.21523374 H 16.986752 L 8.6523775,16.883984 11.176497,19.408103 8.6523776,21.932731 6.1282584,19.408103 8.6523775,16.883984 Z m 25.0031245,0 8.318096,16.63568426 0.01628,-0.01628 z" /><path
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.521026;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6;stroke-opacity:1"
|
||||
d="M 16.88398,33.606161 0.2605135,16.982693 V 0.26051574 H 25.269743 L 33.606158,16.883983 Z"
|
||||
id="path3" /></g></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="netfox-banner-ver.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:x="adobe:ns:meta/"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#404040"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="2.336767"
|
||||
inkscape:cx="97.142762"
|
||||
inkscape:cy="139.50899"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><metadata
|
||||
id="metadata1"><?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta
|
||||
x:xmptk="Adobe XMP Core 5.6-c142 79.160924, 2017/07/13-01:06:39 "><rdf:RDF><rdf:Description
|
||||
rdf:about="" /></rdf:RDF></x:xmpmeta><?xpacket end="w"?></metadata><defs
|
||||
id="defs1"><style
|
||||
id="style1">
|
||||
.cls-1 {
|
||||
font-size: 250px;
|
||||
fill: #ff8100;
|
||||
font-family: Oxanium;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style></defs><text
|
||||
id="netfox"
|
||||
class="cls-1"
|
||||
x="62.911915"
|
||||
y="211.78668"
|
||||
style="font-size:42.6667px">net<tspan
|
||||
style="font-size:42.6667px;fill:#ffffff;fill-opacity:1"
|
||||
id="tspan2">fox</tspan></text><g
|
||||
inkscape:label="Layer 1"
|
||||
id="layer1"
|
||||
transform="matrix(3.7795276,0,0,3.7795276,64,44.213318)"><path
|
||||
id="rect1"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:0.822685;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
d="m 0,0 12.009119,12.009119 h 23.87646 v -24.018238 l -17.92507,-5.95139 z"
|
||||
transform="rotate(45)"
|
||||
sodipodi:nodetypes="cccccc" /><path
|
||||
d="M 16.93334,0 8.4666699,16.93334 0,0 Z"
|
||||
fill="#5fff97"
|
||||
id="path1"
|
||||
style="stroke-width:4.23333" /><path
|
||||
d="M 33.866666,16.93334 25.399996,0 16.93334,16.93334 Z"
|
||||
fill="#ff5f5f"
|
||||
id="path2"
|
||||
style="stroke-width:4.23333" /><rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect2"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="17.96052"
|
||||
y="5.9868398"
|
||||
transform="rotate(45)" /><rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect3"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="29.934195"
|
||||
y="-5.9868345"
|
||||
transform="rotate(45)" /><rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect4"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="32.259071"
|
||||
y="8.3826094"
|
||||
transform="rotate(45)" /></g></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-icon-black.svg"
|
||||
inkscape:export-filename="netfox.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#bfbfbf"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="-262"
|
||||
inkscape:cy="90"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.80983;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 0.3180025,0.21523374 V 16.933327 l 14.0952875,14.095287 2.524119,-2.524627 2.524118,2.524627 14.144631,-14.144631 h -8.285032 l 2.524119,2.52412 -2.524119,2.524627 -2.524118,-2.524627 2.524118,-2.52412 H 16.986751 L 25.321127,0.21523374 H 16.986752 L 8.6523775,16.883984 11.176497,19.408103 8.6523776,21.932731 6.1282584,19.408103 8.6523775,16.883984 Z m 25.0031245,0 8.318096,16.63568426 0.01628,-0.01628 z" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.521026;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6;stroke-opacity:1"
|
||||
d="M 16.88398,33.606161 0.2605135,16.982693 V 0.26051574 H 25.269743 L 33.606158,16.883983 Z"
|
||||
id="path3" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
sodipodi:docname="netfox-icon-white.svg"
|
||||
inkscape:export-filename="netfox.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#404040"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="4"
|
||||
inkscape:cx="72.75"
|
||||
inkscape:cy="77"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.80983;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 0.3180025,0.21523374 V 16.933327 l 14.0952875,14.095287 2.524119,-2.524627 2.524118,2.524627 14.144631,-14.144631 h -8.285032 l 2.524119,2.52412 -2.524119,2.524627 -2.524118,-2.524627 2.524118,-2.52412 H 16.986751 L 25.321127,0.21523374 H 16.986752 L 8.6523775,16.883984 11.176497,19.408103 8.6523776,21.932731 6.1282584,19.408103 8.6523775,16.883984 Z m 25.0031245,0 8.318096,16.63568426 0.01628,-0.01628 z" />
|
||||
<path
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.521026;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6;stroke-opacity:1"
|
||||
d="M 16.88398,33.606161 0.2605135,16.982693 V 0.26051574 H 25.269743 L 33.606158,16.883983 Z"
|
||||
id="path3" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866666"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="netfox.svg"
|
||||
inkscape:export-filename="netfox.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="-30"
|
||||
inkscape:cy="-3.5"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
id="rect1"
|
||||
style="fill:#ff8100;fill-opacity:1;stroke:none;stroke-width:0.822685;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
d="m 0,0 12.009119,12.009119 23.87646,0 0,-24.018238 -17.92507,-5.95139 z"
|
||||
transform="rotate(45)"
|
||||
sodipodi:nodetypes="cccccc" />
|
||||
<path
|
||||
d="M 16.93334,0 8.4666699,16.93334 0,0 Z"
|
||||
fill="#5fff97"
|
||||
id="path1"
|
||||
style="stroke-width:4.23333" />
|
||||
<path
|
||||
d="M 33.866666,16.93334 25.399996,0 16.93334,16.93334 Z"
|
||||
fill="#ff5f5f"
|
||||
id="path2"
|
||||
style="stroke-width:4.23333" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect2"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="17.96052"
|
||||
y="5.9868398"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect3"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="29.934195"
|
||||
y="-5.9868345"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.33"
|
||||
id="rect4"
|
||||
width="3.6265092"
|
||||
height="3.6265092"
|
||||
x="32.259071"
|
||||
y="8.3826094"
|
||||
transform="rotate(45)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,29 @@
|
||||
# Contributors
|
||||
|
||||
Over time, *netfox* has received multiple important contributions from its
|
||||
community. Huge kudos to all of our contributors!
|
||||
|
||||
## Special thanks
|
||||
|
||||
We'd like to highlight some of our contributors, in no particular order:
|
||||
|
||||
* [albertok](https://github.com/albertok)
|
||||
* For being one of the first active members of the community
|
||||
* For contributing multiple features to netfox, including window tiling
|
||||
* [DustieDog](https://github.com/DustieDog)
|
||||
* For creating [NetfoxSharp](https://github.com/CyFurStudios/NetfoxSharp)
|
||||
* For contributing to netfox's code quality both with PRs and design
|
||||
discussions
|
||||
* [krazyjakee](https://github.com/krazyjakee)
|
||||
* For contributing the rollback-aware state machine and FPS example
|
||||
* [TheYellowArchitect](https://github.com/TheYellowArchitect)
|
||||
* For suggesting and contributing to multiple important features such as
|
||||
diff states and input delay
|
||||
|
||||
## All contributors
|
||||
|
||||
<a href="https://github.com/foxssake/netfox/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=foxssake/netfox" />
|
||||
</a>
|
||||
|
||||
Image generated by [contrib.rocks](https://contrib.rocks).
|
||||
@@ -0,0 +1,4 @@
|
||||
span.twemoji {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#version-selector {
|
||||
display: inline !important;
|
||||
border-radius: 2em;
|
||||
padding: 0.25em 0.5em !important;
|
||||
font-family: Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{%- block extrahead %}
|
||||
<script src="https://giscus.app/client.js"
|
||||
data-repo="foxssake/netfox"
|
||||
data-repo-id="R_kgDOKBON2A"
|
||||
data-category="Docs"
|
||||
data-category-id="DIC_kwDOKBON2M4Clib_"
|
||||
data-mapping="title"
|
||||
data-strict="0"
|
||||
data-reactions-enabled="1"
|
||||
data-emit-metadata="0"
|
||||
data-input-position="top"
|
||||
data-theme="light"
|
||||
data-lang="en"
|
||||
data-loading="lazy"
|
||||
crossorigin="anonymous"
|
||||
async>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{%- block footer %}
|
||||
<hr>
|
||||
<h2>User discussion</h2>
|
||||
<div class="giscus" ></div>
|
||||
<hr>
|
||||
{%- if config.copyright %}
|
||||
<p>{{ config.copyright }}</p>
|
||||
{%- endif %}
|
||||
<p>{% trans mkdocs_link='<a href="https://www.mkdocs.org/">MkDocs</a>' %}Documentation built with {{ mkdocs_link }}.{% endtrans %}</p>
|
||||
{%- endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
# External tutorials
|
||||
|
||||
While these docs aim to cover everything netfox-related, we are happy to be
|
||||
promoting other creators who also create content that helps understanding
|
||||
netfox.
|
||||
|
||||
## BatteryAcidDev
|
||||
|
||||
In-depth tutorials explaining networking from the basics. Excellent content
|
||||
both for newcomers and seasoned developers. Make sure to check the YouTube
|
||||
channel as well as these videos!
|
||||
|
||||
- [Godot Multiplayer Lag Compensation with Netfox](https://youtu.be/GqHTNmRspjU?si=--fKtAOvJiYd5RTd)
|
||||
- [Start Your Multiplayer Game with this Template](https://youtu.be/AplKwFtBMjU?si=LBwW4X5D_A4gL33U)
|
||||
- [Complete Game Walkthrough](https://youtu.be/tWLZNCJISYU?si=1ewd1fgd5vx-3HI1)
|
||||
- [Run Your Own P2P Server with Noray!](https://youtu.be/9pEBq6hLCzQ?si=OlyK-NXyxsnJ2A8A)
|
||||
|
||||
[:simple-youtube: YouTube](https://www.youtube.com/@BatteryAcidDev/) |
|
||||
[:simple-discord: Discord](https://discord.gg/psjbBDvNBK)
|
||||
|
||||
## Your content here!
|
||||
|
||||
Are you making a video about netfox? A tutorial series? A blog post? Something
|
||||
else?
|
||||
|
||||
Let us know, we'd love to feature it! Reach out on [Github discussions], on
|
||||
[Discord], or in the comments at the bottom of this page.
|
||||
|
||||
|
||||
[Github discussions]: https://github.com/foxssake/netfox/discussions/
|
||||
[Discord]: https://discord.gg/xWGh4GskG5
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,119 @@
|
||||

|
||||
|
||||
[](https://github.com/foxssake/netfox/blob/main/LICENSE)
|
||||
[](https://github.com/foxssake/netfox/releases)
|
||||
[](https://foxssake.github.io/netfox/)
|
||||
[](https://discord.gg/xWGh4GskG5)
|
||||
[](https://ko-fi.com/T6T8WZD0W)
|
||||
|
||||
A set of addons for responsive online games with the [Godot engine].
|
||||
|
||||
## Features
|
||||
|
||||
* ⏲️ Consistent timing across multiple machines
|
||||
* 🖥️ Supports client-server architecture
|
||||
* 🧈 Smooth motion with easy-to-use interpolation
|
||||
* 💨 Lag compensation with Client-side Prediction and Server-side Reconciliation
|
||||
* 🛜 Bullet-proof connectivity with [noray] integration
|
||||
|
||||
## Overview
|
||||
|
||||
The package consists of multiple addons, each with different features:
|
||||
|
||||
* [netfox]
|
||||
* The core package, implements timing, rollback and other multiplayer
|
||||
features
|
||||
* *Start here*
|
||||
* [netfox.noray]
|
||||
* Implements [noray] integration to establish connection between players
|
||||
* *Useful for online games*
|
||||
* [netfox.extras]
|
||||
* Provides high-level, game-specific, convenience features built on top of
|
||||
netfox, like base classes for input management or weapons
|
||||
* *Check for reusable components for your game*
|
||||
* [netfox.internals]
|
||||
* Shared utilities for the other addons
|
||||
* Included as dependency, no need to install separately
|
||||
|
||||
## C# Support
|
||||
For experimental C# support, see the [Netfox Sharp guide](netfox/guides/netfox-sharp.md).
|
||||
|
||||
## Install
|
||||
|
||||
### Releases
|
||||
|
||||
Find the latest netfox under
|
||||
[Releases](https://github.com/foxssake/netfox/releases)
|
||||
|
||||
Each release contains the addons, and a build of [Forest Brawl] for Windows and
|
||||
Linux. Each addon has its dependencies packed with it - e.g.
|
||||
*"netfox.extras.vx.y.z.zip"* also contains both *netfox* and
|
||||
*netfox.internals*.
|
||||
|
||||
> Note: For releases before v1.1.1, a separate *".with-deps.zip"* version
|
||||
> contains the addon and its dependencies, while the regular zips contain only
|
||||
> the addon itself.
|
||||
|
||||
### Asset Library
|
||||
|
||||
Search for the addon name in Godot's AssetLib or download from the site:
|
||||
|
||||
* [netfox](https://godotengine.org/asset-library/asset/2375)
|
||||
* [netfox.noray](https://godotengine.org/asset-library/asset/2376)
|
||||
* [netfox.extras](https://godotengine.org/asset-library/asset/2377)
|
||||
|
||||
### Source
|
||||
|
||||
Download the [source] and copy the addons of your choice to your Godot project.
|
||||
|
||||
### Enable the addons
|
||||
|
||||
After adding *netfox* to your project, make sure to enable the addons in your
|
||||
project settings. Otherwise, Godot will present you with errors about
|
||||
undeclared identifiers.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Godot 4.x is supported by netfox. If you find any issue using any supported
|
||||
version, please [open an issue].
|
||||
|
||||
Forest Brawl and the other examples are written against Godot 4.1. They may or
|
||||
may not work when opened with any other version.
|
||||
|
||||
## Prototyping
|
||||
|
||||
To try your game online with [noray], a free to use instance is hosted at
|
||||
`tomfol.io:8890`, the same instance used by [Forest Brawl].
|
||||
|
||||
You can use this [noray] instance to quickly test your games online, but is not
|
||||
recommended for shipping games. The instance has configured limits, and no
|
||||
uptime guarantees are made.
|
||||
|
||||
## About this documentation
|
||||
|
||||
These pages assume that you are familiar with both Godot, its multiplayer
|
||||
capabilities, and building multiplayer games in general. Missing any of these
|
||||
might make your reading experience more difficult than preferred.
|
||||
|
||||
Some links to get you up to speed:
|
||||
|
||||
* [Godot Engine docs](https://docs.godotengine.org/en/stable/index.html)
|
||||
* [Godot Engine High-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html)
|
||||
* [Networking for Physics Programmers](https://www.gdcvault.com/play/1022195/Physics-for-Game-Programmers-Networking)
|
||||
|
||||
## About the tutorials
|
||||
|
||||
The tutorials are intended to get you started fast, but don't explain much of
|
||||
how things work. For that, refer to the guides.
|
||||
|
||||
[netfox]: https://github.com/foxssake/netfox/tree/main/addons/netfox
|
||||
[netfox.noray]: https://github.com/foxssake/netfox/tree/main/addons/netfox.noray
|
||||
[netfox.extras]: https://github.com/foxssake/netfox/tree/main/addons/netfox.extras
|
||||
[netfox.internals]: https://github.com/foxssake/netfox/tree/main/addons/netfox.internals
|
||||
[Forest Brawl]: https://github.com/foxssake/netfox/tree/main/examples/forest-brawl
|
||||
|
||||
[open an issue]: https://github.com/foxssake/netfox/issues
|
||||
|
||||
[Godot engine]: https://godotengine.org/
|
||||
[noray]: https://github.com/foxssake/noray
|
||||
[source]: https://github.com/foxssake/netfox/archive/refs/heads/main.zip
|
||||
@@ -0,0 +1,78 @@
|
||||
# Made with netfox
|
||||
|
||||
## Bubble Battle
|
||||
|
||||
<a href="https://store.steampowered.com/app/3180520/Bubble_Battle/" markdown>
|
||||

|
||||
</a>
|
||||
|
||||
Bubble Battle is a retro online/local multiplayer fighting game inspired by
|
||||
arcade classics. Players fly around with one simple objective: burst opponents’
|
||||
bubbles before they burst yours. Collect powerups and out-maneuver opponents
|
||||
for fast-paced, addictive fun!
|
||||
|
||||
[:simple-steam: Steam](https://store.steampowered.com/app/3180520/Bubble_Battle/)
|
||||
|
||||
## PL4no-B
|
||||
|
||||
<a href="https://store.steampowered.com/app/3423700/PL4noB/" markdown>
|
||||

|
||||
</a>
|
||||
|
||||
The best and most modern 2D tactical shooter, inspired by Counter-Strike and
|
||||
Hotline Miami. Play online with friends or meet new ones in diverse game modes.
|
||||
Unlock a huge variety of guns and utilities, earn points to build your base,
|
||||
unlock features, and invade other players’ bases for endless fun!
|
||||
|
||||
[:simple-steam: Steam](https://store.steampowered.com/app/3423700/PL4noB/)
|
||||
|
||||
## Chrome Carnage
|
||||
|
||||
<a href="https://store.steampowered.com/app/3204000/Chrome_Carnage/">
|
||||

|
||||
</a>
|
||||
|
||||
Machines took all jobs but one - urban blood sport. Chrome Carnage is a
|
||||
multiplayer top down shooter set in vibrant chaotic cyberpunk cities.
|
||||
|
||||
*Chrome Carnage* is a top-down shooter drenched in the glow of cyberpunk
|
||||
dystopia, where hope is scarcer than a working vending machine. Rising sea
|
||||
levels have forced humanity into overcrowded platform cities.
|
||||
|
||||
With everyone on basic welfare a mysterious underground app has the desperate
|
||||
masses hooked: win brutal street tournaments, score hectic amounts of crypto,
|
||||
and maybe—just maybe—eat something that doesn’t squeak.
|
||||
|
||||
[:simple-steam: Steam](https://store.steampowered.com/app/3204000/Chrome_Carnage/)
|
||||
|
||||
## Nitro Turtles
|
||||
|
||||
<a href="https://store.steampowered.com/app/3952070/Nitro_Turtles/" markdown>
|
||||

|
||||
</a>
|
||||
|
||||
Nitro Turtles is a racing game about super fast turtles! Reach insane speeds
|
||||
with awesome shell-attached gadgets, compete in crazy races with friends, and
|
||||
sprint across the finish line to claim ultimate turtle victory! To be the
|
||||
fastest turtle around, you'll need to master shell gadget usage, learn every
|
||||
shortcut imaginable, and maybe even consider using performance-enhancing
|
||||
seaweed\*.
|
||||
|
||||
!!!note
|
||||
Performance-enhancing seaweed has not been tested for safety and likely has
|
||||
numerous side effects
|
||||
|
||||
[:simple-steam: Steam](https://store.steampowered.com/app/3952070/Nitro_Turtles/)
|
||||
|
||||
## Your game here!
|
||||
|
||||
Building something cool with *netfox*? Whether it's a game, an app, tool, or
|
||||
anything else, we'd be happy to showcase it!
|
||||
|
||||
Reach out on [Github discussions], on [Discord], or in the comments at the
|
||||
bottom of this page.
|
||||
|
||||
|
||||
[Github discussions]: https://github.com/foxssake/netfox/discussions/207
|
||||
[Discord]: https://discord.gg/xWGh4GskG5
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# Tactical Shooter — Mapmaking SDK
|
||||
|
||||
Everything you need to build, validate, package, and ship competitive maps for
|
||||
Tactical Shooter. This SDK is designed for the community — no C++, no GDextension
|
||||
tinkering required. You only need Godot 4 (any 4.x) and a text editor.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Copy the template project
|
||||
cp -r client/map_template/ maps/my_first_map/
|
||||
|
||||
# Rename the demo scene
|
||||
mv maps/my_first_map/template_map.tscn maps/my_first_map/de_my_map.tscn
|
||||
|
||||
# Open in Godot
|
||||
godot maps/my_first_map/project.godot
|
||||
```
|
||||
|
||||
Then read the [Getting Started guide](01-getting-started.md).
|
||||
|
||||
## What You Can Build
|
||||
|
||||
- **Competitive defuse maps** — A-site, B-site, CT/T spawns, buy zones
|
||||
- **Custom game modes** — The template is gameplay-agnostic; your map logic
|
||||
is in Godot groups, not hardcoded
|
||||
- **Community servers** — Ship maps as `.pck` files via the registry server
|
||||
|
||||
## SDK Components
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| [Template project](01-getting-started.md) | `client/map_template/` | Standalone Godot project with CSG prefabs and demo scene |
|
||||
| [CSG Prefabs](02-building-geometry.md) | `client/map_template/assets/prefabs/` | 6 drop-in node types (spawns, sites, zones) |
|
||||
| [Editor Validator](04-validation.md) | `client/map_template/template_map.gd` | Auto-runs when scene opened in editor |
|
||||
| [Headless Validator](04-validation.md) | `client/tools/validate_map.gd` | 4-module CLI validator for CI/CD |
|
||||
| [Packaging](05-packaging-and-shipping.md) | `scripts/map_packaging/pack_map.gd` | Exports `.tscn` → `.pck` addon pack |
|
||||
| [Registry Server](05-packaging-and-shipping.md) | `scripts/map_packaging/map_registry_server.py` | Serves `.pck` files over HTTP |
|
||||
| [Client Downloader](05-packaging-and-shipping.md) | `client/scripts/map_downloader.gd` | Autoload that downloads + loads `.pck` at runtime |
|
||||
|
||||
## Map Authoring Workflow
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ 1. Build │ ──→ │ 2. Validate │ ──→ │ 3. Package │
|
||||
│ - CSG geometry │ │ - Editor check │ │ - pack_map.gd │
|
||||
│ - Prefab nodes │ │ - CLI validator │ │ - → .pck file │
|
||||
│ - Lighting │ │ - Fix warnings │ │ │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ 6. Load & Play │ ←── │ 5. Download │ ←── │ 4. Upload │
|
||||
│ - Auto-download │ │ - Registry HTTP │ │ - map_registry │
|
||||
│ - changelevel │ │ - Cache to disk │ │ - .pck + .json │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Performance Targets
|
||||
|
||||
Maps built with the SDK must meet these budgets:
|
||||
|
||||
| Metric | Target | Enforced by |
|
||||
|------------------|-------------|--------------------------------|
|
||||
| Triangle count | ≤ 50,000 | `validate_polycount.gd` |
|
||||
| Texture size | ≤ 1024×1024 | `validate_textures.gd` |
|
||||
| Dynamic lights | ≤ 4 | `validate_lights.gd` |
|
||||
| LightmapGI | ✓ Baked | `validate_lights.gd` |
|
||||
| ReflectionProbe | ≥ 1 | `validate_lights.gd` (warning) |
|
||||
| FPS target | 60+ | Project rendering settings |
|
||||
|
||||
## File Structure Reference
|
||||
|
||||
```
|
||||
tactical-shooter/
|
||||
├── client/
|
||||
│ ├── project.godot # Main game project
|
||||
│ ├── map_template/ # ← YOU ARE HERE (template project)
|
||||
│ │ ├── project.godot # Standalone Godot project
|
||||
│ │ ├── template_map.tscn # Demo scene (rename me)
|
||||
│ │ ├── template_map.gd # Editor validation script
|
||||
│ │ └── assets/prefabs/ # CSG prefab nodes
|
||||
│ ├── tools/
|
||||
│ │ ├── validate_map.gd # CLI validator entry point
|
||||
│ │ └── validate_map/ # 4 validator modules
|
||||
│ │ ├── validate_scene.gd
|
||||
│ │ ├── validate_polycount.gd
|
||||
│ │ ├── validate_textures.gd
|
||||
│ │ └── validate_lights.gd
|
||||
│ └── scripts/
|
||||
│ └── map_downloader.gd # Client autoload for .pck loading
|
||||
├── scripts/
|
||||
│ └── map_packaging/
|
||||
│ ├── pack_map.gd # Godot editor → .pck exporter
|
||||
│ └── map_registry_server.py # Python HTTP registry server
|
||||
└── docs/mapmaking/
|
||||
├── 00-index.md # This document
|
||||
├── 01-getting-started.md # Template project setup
|
||||
├── 02-building-geometry.md # CSG & prefab guide
|
||||
├── 03-lighting-and-env.md # Lighting & baking
|
||||
├── 04-validation.md # Validator usage
|
||||
├── 05-packaging-and-shipping.md# Publishing pipeline
|
||||
└── 06-faq-and-troubleshooting.md
|
||||
```
|
||||
@@ -1,135 +0,0 @@
|
||||
# Getting Started — Your First Map
|
||||
|
||||
This guide walks you through setting up the map template project and creating
|
||||
a basic playable map.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Godot 4.0 or later (any 4.x stable)
|
||||
- The Tactical Shooter repository cloned
|
||||
- Familiarity with Godot's editor (scene tree, inspector, 3D viewport)
|
||||
|
||||
## 1. Copy the Template
|
||||
|
||||
The template project lives at `client/map_template/`. Copy it somewhere outside
|
||||
the main project (or keep it adjacent — it's a standalone Godot project):
|
||||
|
||||
```bash
|
||||
# From the tactical-shooter repository root
|
||||
cp -r client/map_template/ maps/my_first_map/
|
||||
cd maps/my_first_map/
|
||||
mv template_map.tscn de_dust2.tscn
|
||||
```
|
||||
|
||||
## 2. Open in Godot
|
||||
|
||||
Launch Godot and click **Import** (not Open). Navigate to your copied directory
|
||||
and select `project.godot`. The map template project will open.
|
||||
|
||||
> **Why "Import"?** Because template is a standalone project with its own
|
||||
> `project.godot`. You're importing a new project into the Godot project list,
|
||||
> not opening a scene within an existing project.
|
||||
|
||||
## 3. Explore the Demo Scene
|
||||
|
||||
Open `de_dust2.tscn` (the renamed `template_map.tscn`). You'll see:
|
||||
|
||||
- **Floor**: A green CSGBox3D pad (16×20 units)
|
||||
- **Walls**: Orange CSGBox3D walls forming a rough box
|
||||
- **Spawn pads**: Green (CT) and red (T) Marker3D spawn points
|
||||
- **Bomb sites**: Semi-transparent orange floor panels
|
||||
- **Buy zones**: Semi-transparent yellow trigger areas
|
||||
- **Lighting**: DirectionalLight3D (sun), OmniLight3D fill, LightmapGI
|
||||
- **Environment**: WorldEnvironment with sky
|
||||
|
||||
The scene also contains a `template_map.gd` editor tool script. When you open
|
||||
this scene, the **Output** panel (bottom dock) shows auto-validation results
|
||||
for the map structure.
|
||||
|
||||
## 4. Build Your First Room
|
||||
|
||||
1. **Select the floor** — Click `Floor` in the scene tree
|
||||
2. **Scale it** — With the scale tool (R), drag the red/green/blue handles
|
||||
to resize the floor to roughly 12×16 units
|
||||
3. **Add walls** — Select `Walls` → right-click → **Instantiate Child Scene** →
|
||||
select `assets/prefabs/map_bounds.tscn`. Size them to enclose the floor
|
||||
4. **Add a ceiling** — Duplicate (Ctrl+D) the floor, move it up 4 units,
|
||||
set **Operation** to Union (0) so it acts as a solid ceiling
|
||||
|
||||
> **Tip:** Use **View → Perspective** and rotate around to check your geometry
|
||||
> from all angles. Walls should overlap slightly to prevent light leaks.
|
||||
|
||||
## 5. Place Spawn Points
|
||||
|
||||
Spawn points tell the game where players appear at round start.
|
||||
|
||||
**CT Spawns:**
|
||||
- Drag `assets/prefabs/ct_spawn.tscn` from the FileSystem dock into the scene
|
||||
- Position it on the floor at one end of the map
|
||||
- The green pad's +Z arrow shows the direction players face on spawn
|
||||
- Duplicate for 5 players (competitive standard)
|
||||
|
||||
**T Spawns:**
|
||||
- Same process with `assets/prefabs/t_spawn.tscn` (red pads)
|
||||
- Place at the opposite end of the map
|
||||
|
||||
## 6. Define Bomb Sites
|
||||
|
||||
A competitive defuse map needs two bomb sites, A and B.
|
||||
|
||||
1. Drag `assets/prefabs/bomb_site.tscn` into the scene
|
||||
2. Rename it to `BombsiteA`
|
||||
3. Resize the CollisionShape3D to cover the intended site area
|
||||
4. In the **Node** dock → **Groups** tab, ensure it has:
|
||||
- `bomb_site` (required — game logic finds sites by this group)
|
||||
- `bombsite_a` (informational — helps identify which site)
|
||||
5. Repeat for BombsiteB with group `bombsite_b`
|
||||
|
||||
## 7. Add Buy Zones
|
||||
|
||||
Buy zones mark areas where players can purchase equipment.
|
||||
|
||||
1. Drag `assets/prefabs/buy_zone.tscn` into the scene
|
||||
2. Place one at the CT spawn area (group `buy_zone` stays)
|
||||
3. Resize the yellow CollisionShape3D to cover the spawn room
|
||||
4. Duplicate for T spawn — add subgroup `ct_buy` or `t_buy` on each
|
||||
|
||||
## 8. Check the Editor Validator
|
||||
|
||||
Open the **Output** (bottom dock). The `template_map.gd` script runs
|
||||
automatically. You should see something like:
|
||||
|
||||
```
|
||||
=== Map Template: Validate Scene ===
|
||||
+ CT Spawn points (5 found)
|
||||
+ T Spawn points (5 found)
|
||||
+ Buy zones (2 found)
|
||||
+ Bomb sites (2 found)
|
||||
+ Cubemap capture origins (1 found)
|
||||
+ Map boundary walls (8 found)
|
||||
+ CSG Floor geometry (1 nodes)
|
||||
+ CSG Wall geometry (4 walls)
|
||||
+ LightmapGI configured ✓
|
||||
+ LightmapGI: NOT YET BAKED
|
||||
+ ReflectionProbe present
|
||||
+ WorldEnvironment present
|
||||
+ DirectionalLight3D (sun) present
|
||||
```
|
||||
|
||||
The "NOT YET BAKED" warning is expected — you'll bake lighting after geometry
|
||||
is finalised (see [Lighting guide](03-lighting-and-env.md)).
|
||||
|
||||
## 9. Save and Package
|
||||
|
||||
When your map is ready for testing:
|
||||
|
||||
1. Save the scene (`Ctrl+S`)
|
||||
2. Read the [Validation guide](04-validation.md) for headless CI checks
|
||||
3. Read the [Packaging guide](05-packaging-and-shipping.md) to ship it
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Building Geometry with CSG](02-building-geometry.md) — detailed prefab reference
|
||||
- [Lighting & Baking](03-lighting-and-env.md) — how to light your map properly
|
||||
- [Validator Reference](04-validation.md) — CLI validator exit codes and CI
|
||||
- [Packaging & Shipping](05-packaging-and-shipping.md) — publishing as .pck
|
||||
@@ -1,245 +0,0 @@
|
||||
# Building Geometry with CSG
|
||||
|
||||
Tactical Shooter maps are built with Godot's **CSG** (Constructive Solid
|
||||
Geometry) system — the same tool you'd use in a 3D modeller, but inside the
|
||||
Godot editor. CSG nodes let you cut, join, and extrude geometry in real-time
|
||||
with instant visual feedback.
|
||||
|
||||
## CSG Basics
|
||||
|
||||
### CSG Node Types
|
||||
|
||||
| Node | Use | Shape |
|
||||
|------|-----|-------|
|
||||
| `CSGBox3D` | Walls, floors, ceilings, pillars | Rectangular prism |
|
||||
| `CSGSphere3D` | Domes, curved edges | Sphere |
|
||||
| `CSGCylinder3D` | Columns, pipes | Cylinder |
|
||||
| `CSGCombiner3D` | Group container, non-collidable parent | — |
|
||||
| `CSGPolygon3D` | Extruded 2D path | Custom shape |
|
||||
|
||||
### CSG Operations
|
||||
|
||||
Each CSG node has an **Operation** property that determines how it combines
|
||||
with sibling CSG nodes in the same parent:
|
||||
|
||||
| Value | Operation | Effect |
|
||||
|-------|-----------|--------|
|
||||
| 0 | **Union** | Adds to existing geometry (default) |
|
||||
| 1 | **Subtraction** | Cuts a hole through parent |
|
||||
| 2 | **Intersection** | Keeps only overlapping volume |
|
||||
|
||||
**Example — a window cutout:**
|
||||
|
||||
```
|
||||
Wall (CSGBox3D, Union)
|
||||
└── WindowCutout (CSGBox3D, Subtraction)
|
||||
```
|
||||
|
||||
This subtracts the window shape from the wall. Move/resize the subtraction
|
||||
child to reposition the window.
|
||||
|
||||
### Key Properties
|
||||
|
||||
- **`use_collision`** (bool) — Must be `true` on all floor/wall/ceiling CSG
|
||||
nodes so players and bullets collide with them
|
||||
- **`use_shadows`** (bool) — Enable shadow-casting on geometry
|
||||
- **`material`** — Assign placeholder materials during construction; final
|
||||
materials come from the main game project library
|
||||
- **`snap_mode`** — Controls vertex snapping (useful for aligning geometry)
|
||||
|
||||
## Building a Room
|
||||
|
||||
A basic room needs 3 CSG layers:
|
||||
|
||||
```
|
||||
Room (CSGCombiner3D)
|
||||
├── Floor (CSGBox3D, Union, use_collision=true)
|
||||
├── Walls (CSGCombiner3D)
|
||||
│ ├── Wall_North (CSGBox3D, Union)
|
||||
│ ├── Wall_South (CSGBox3D, Union)
|
||||
│ ├── Wall_East (CSGBox3D, Union)
|
||||
│ └── Wall_West (CSGBox3D, Union)
|
||||
└── Ceiling (CSGBox3D, Union, use_collision=true)
|
||||
```
|
||||
|
||||
### CSG Building Tips
|
||||
|
||||
1. **Overlap slightly** — Walls should extend past the floor edge by ~0.1 units
|
||||
to prevent light leaks (thin gaps let light bleed through)
|
||||
2. **Keep CSG simple** — Use as few CSG nodes as possible. A wall panel is one
|
||||
CSGBox3D, not 4 extruded edges
|
||||
3. **Convert to Mesh when done** — Once geometry is final, select all CSG
|
||||
nodes → right-click → **Convert CSG to Mesh**. This bakes them into static
|
||||
MeshInstance3D nodes that perform much better
|
||||
4. **Avoid curved CSG for floor/walls** — CSGSphere3D and CSGCylinder3D are
|
||||
expensive. Use them sparingly
|
||||
|
||||
## Prefab Reference
|
||||
|
||||
The template provides 6 drag-and-drop prefab nodes. Each is an `assets/prefabs/*.tscn`
|
||||
file that you instantiate into your scene.
|
||||
|
||||
### ct_spawn.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Marker3D` (green pad + arrow) |
|
||||
| Group | `ct_spawn` |
|
||||
| Purpose | CT team spawn position |
|
||||
| Quantity | 5 (one per player) |
|
||||
|
||||
The +Z arrow (blue axis) shows the direction players face when they spawn.
|
||||
Position the pad at floor level — make it flush with the floor surface.
|
||||
|
||||
**Customisation:** Duplicate instances rather than editing the prefab. Adjust
|
||||
only position and rotation. The green pad is cosmetic.
|
||||
|
||||
### t_spawn.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Marker3D` (red pad + arrow) |
|
||||
| Group | `t_spawn` |
|
||||
| Purpose | Terrorist team spawn position |
|
||||
| Quantity | 5 |
|
||||
|
||||
Same usage as `ct_spawn` but red. Place at the opposite end of the map.
|
||||
|
||||
### buy_zone.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Area3D` with `CollisionShape3D` |
|
||||
| Group | `buy_zone` |
|
||||
| Purpose | Purchase-eligible zone |
|
||||
| Quantity | 1–2 |
|
||||
|
||||
The game monitors `body_entered` / `body_exited` on these areas to show/hide
|
||||
the buy menu. Resize the child CollisionShape3D to match the room shape.
|
||||
|
||||
**Additional groups (optional):**
|
||||
- `ct_buy` — Only CT team can buy in this zone
|
||||
- `t_buy` — Only T team can buy in this zone
|
||||
|
||||
If omitted, any player inside a `buy_zone` can buy (suitable for shared mid areas).
|
||||
|
||||
### bomb_site.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Area3D` with `CollisionShape3D` |
|
||||
| Group | `bomb_site` |
|
||||
| Purpose | Bomb plant zone |
|
||||
| Quantity | 2 (A and B) |
|
||||
|
||||
Rename instances to `BombsiteA` and `BombsiteB`. Resize the CollisionShape3D
|
||||
to cover the plantable area. Visual: semi-transparent orange floor panel.
|
||||
|
||||
**Additional groups (strongly recommended):**
|
||||
- `bombsite_a` — Identifies this as site A
|
||||
- `bombsite_b` — Identifies this as site B
|
||||
|
||||
### cubemap_origin.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Node3D` (blue sphere marker) |
|
||||
| Group | `cubemap_origin` |
|
||||
| Purpose | Marks ReflectionProbe capture position |
|
||||
| Quantity | 1 |
|
||||
|
||||
Place at eye height ≈ 1.6 units above the floor in the most visually prominent
|
||||
area (usually mid-map or the central choke point). The ReflectionProbe is a
|
||||
separate node that should be placed at this marker's position after geometry
|
||||
is finalised.
|
||||
|
||||
### map_bounds.tscn
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Type | `Area3D` with `CollisionShape3D` |
|
||||
| Group | `map_bounds` |
|
||||
| Purpose | Out-of-bounds kill wall |
|
||||
| Quantity | 4–8 (enclose the perimeter) |
|
||||
|
||||
**Usage:**
|
||||
- Place one instance per perimeter face (north, south, east, west)
|
||||
- Resize each CollisionShape3D to form a closed box around the playable area
|
||||
- Make the floor-bound wall tall enough that players can't jump over it
|
||||
- The game detects `body_exited` from these areas and teleports/kills
|
||||
out-of-bounds players
|
||||
|
||||
> **Performance tip:** `map_bounds` nodes should be as thin as practical —
|
||||
> these are just detection triggers, not visual geometry. A thickness of 0.5
|
||||
> units is sufficient.
|
||||
|
||||
## Advanced Geometry Techniques
|
||||
|
||||
### Corridors and Hallways
|
||||
|
||||
Build a corridor as a CSGCombiner3D with union floor + walls + ceiling and
|
||||
subtraction nodes for doors/windows:
|
||||
|
||||
```
|
||||
Corridor (CSGCombiner3D)
|
||||
├── Floor (CSGBox3D, Union)
|
||||
├── LeftWall (CSGBox3D, Union)
|
||||
│ └── Door1 (CSGBox3D, Subtraction)
|
||||
├── RightWall (CSGBox3D, Union)
|
||||
│ └── Window1 (CSGBox3D, Subtraction)
|
||||
└── Ceiling (CSGBox3D, Union)
|
||||
```
|
||||
|
||||
### Ramps and Stairs
|
||||
|
||||
Use multiple CSGBox3D nodes at increasing heights. Stack them like steps and
|
||||
wrap in a CSGCombiner3D for a clean hierarchy:
|
||||
|
||||
```
|
||||
Stairs (CSGCombiner3D)
|
||||
├── Step1 (CSGBox3D, Union, pos=(0, 0, 0))
|
||||
├── Step2 (CSGBox3D, Union, pos=(0, 0.5, 1))
|
||||
├── Step3 (CSGBox3D, Union, pos=(0, 1.0, 2))
|
||||
└── Step4 (CSGBox3D, Union, pos=(0, 1.5, 3))
|
||||
```
|
||||
|
||||
For ramps, rotate a CSGBox3D or use CSGPolygon3D with an extruded triangle shape.
|
||||
|
||||
### Map Scale Reference
|
||||
|
||||
| Element | Size (units) | Notes |
|
||||
|---------|-------------|-------|
|
||||
| Player height | 1.8 | Eye level ≈ 1.6 |
|
||||
| Player width | 0.5 | Collision capsule radius |
|
||||
| Doorway | 1.5 × 2.5 | Wide enough for 2 players |
|
||||
| Corridor | 3–4 wide | Comfortable movement |
|
||||
| Bombsite | 8×8 minimum | Playable site area |
|
||||
| Spawn room | 6×8 | 5 players need space |
|
||||
| Mid area | 6–8 wide | Main chokepoint |
|
||||
|
||||
## From CSG to MeshInstance3D (Baking)
|
||||
|
||||
When your geometry is **final** (no more changes expected):
|
||||
|
||||
1. Select all CSG nodes in the scene tree
|
||||
2. Right-click → **Convert CSG to Mesh**
|
||||
3. This replaces CSG nodes with static `MeshInstance3D` nodes
|
||||
4. The mesh instances are much more performant
|
||||
|
||||
**Don't convert until you're done** — CSG is editable, MeshInstance3D is not.
|
||||
Keep working in CSG mode; convert only for final optimisation before packaging.
|
||||
|
||||
## Performance Budget
|
||||
|
||||
See the [Validator guide](04-validation.md) for detailed numbers, but the key
|
||||
geometry rules are:
|
||||
|
||||
- **≤ 50,000 triangles** total across all meshes
|
||||
- **≤ 5,000 triangles** per individual mesh (recommended; enforced as warning)
|
||||
- Use CSG for blockout only — convert to mesh before shipping
|
||||
- Avoid CSGSphere3D in visible areas; use hand-modelled meshes
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Lighting & Environment Setup](03-lighting-and-env.md) — light your map
|
||||
- [Map Validation](04-validation.md) — run the validator on your scene
|
||||
@@ -1,159 +0,0 @@
|
||||
# Lighting & Environment Setup
|
||||
|
||||
Proper lighting is the difference between a map that looks professional and one
|
||||
that looks like a grey box. Tactical Shooter uses Godot's **LightmapGI** system
|
||||
for baked global illumination: light bounces are pre-calculated and stored in
|
||||
lightmaps, giving you realistic lighting at zero runtime cost.
|
||||
|
||||
## Lighting Architecture
|
||||
|
||||
```
|
||||
DirectionalLight3D (sun) — main directional light, shadows
|
||||
└─ OmniLight3D (fill) — ambient fill lights, no shadow
|
||||
└─ LightmapGI — bakes all static lighting into textures
|
||||
└─ baked lightmaps (zero runtime cost)
|
||||
```
|
||||
|
||||
## 1. WorldEnvironment
|
||||
|
||||
Every map needs a `WorldEnvironment` node. This controls:
|
||||
|
||||
- **Sky** — Background appearance (skybox or solid colour)
|
||||
- **Ambient light** — Base light level for unlit surfaces
|
||||
- **Tonemapping** — Colour grading and exposure
|
||||
|
||||
**Setup:**
|
||||
1. The template already includes a `WorldEnvironment` node
|
||||
2. If creating from scratch, add one via the scene tree → + button → `WorldEnvironment`
|
||||
3. In the Inspector, assign an `Environment` resource (New Environment)
|
||||
4. Configure:
|
||||
- **Background:** Sky (for outdoor) or Clear Color (for underground maps)
|
||||
- **Ambient Light:** Sky (mimics sky colour) or Color (custom ambient)
|
||||
- **Tonemap:** Reinhard or Filmic (prevents blown-out highlights)
|
||||
|
||||
> The validator warns if WorldEnvironment is missing — add it early.
|
||||
|
||||
## 2. DirectionalLight3D (Sun)
|
||||
|
||||
The main sun light. One per map.
|
||||
|
||||
**Recommended settings:**
|
||||
| Property | Value | Reason |
|
||||
|----------|-------|--------|
|
||||
| `light_bake_mode` | Static (2) | Bakes into lightmap |
|
||||
| `shadow_enabled` | true | Required for competitive visibility |
|
||||
| `shadow_bias` | 0.1 | Prevents shadow acne on CSG |
|
||||
| `angular_distance` | 0.01 | Sharp sunlight (competitive visual clarity) |
|
||||
| `range` | 50–100 | Large enough to cover the map |
|
||||
|
||||
**Positioning:**
|
||||
- Rotate the sun to create interesting shadows through windows/doorways
|
||||
- Avoid straight-down lighting (makes everything look flat)
|
||||
- A 45° angle works well for most maps
|
||||
|
||||
## 3. Fill Lights (OmniLight3D)
|
||||
|
||||
Fill lights illuminate areas the sun doesn't reach (interiors, tunnels).
|
||||
|
||||
**Rules:**
|
||||
- **Maximum 4 dynamic (non-baked) lights** — enforced by the validator
|
||||
- Fill lights should use `light_bake_mode = Static (2)` so they bake into
|
||||
the lightmap and don't count toward the dynamic budget
|
||||
- Use low intensity (0.3–0.5) for ambient fill
|
||||
|
||||
**Performance tip:** Every omni/spot light with `shadow_enabled = true` is
|
||||
expensive. The validator warns on dynamic lights with shadows. Baked lights
|
||||
with shadows disabled are free — use them.
|
||||
|
||||
## 4. LightmapGI — Baked Global Illumination
|
||||
|
||||
LightmapGI is the backbone of Tactical Shooter's lighting. It pre-computes
|
||||
light bouncing and stores the result as textures (lightmaps).
|
||||
|
||||
### Configuration
|
||||
|
||||
| Property | Recommended | Notes |
|
||||
|----------|-------------|-------|
|
||||
| `quality` | 2 (High) | Higher = better GI, longer bake |
|
||||
| `bounces` | 3 | Minimum 2 for realistic indirect lighting |
|
||||
| `texel_scale` | 1.0–2.0 | Lower = higher res, longer bake |
|
||||
| `max_texture_size` | 1024 | Controls lightmap atlas size |
|
||||
| `use_denoiser` | true | Removes noise from baked result |
|
||||
| `environment_mode` | 0 (Baked Environment) | Uses WorldEnvironment for sky lighting |
|
||||
|
||||
### Baking
|
||||
|
||||
To bake: select the `LightmapGI` node → click **Bake Lightmap** in the top
|
||||
toolbar (or `Ctrl+Shift+C`).
|
||||
|
||||
**Baking checklist:**
|
||||
- [ ] All geometry is finalised (moving geometry after baking invalidates it)
|
||||
- [ ] All lights have correct bake mode (Static for baked, Dynamic for real-time)
|
||||
- [ ] CSG has `use_collision = true` (non-collidable CSG may not bake properly)
|
||||
- [ ] WorldEnvironment is configured with a sky or ambient colour
|
||||
- [ ] Save the scene before baking
|
||||
|
||||
**Baking time:** Varies by map complexity. A small map (10×14 units) bakes in
|
||||
30–60 seconds. A full competitive map (20×20) may take 5–10 minutes.
|
||||
|
||||
### After Baking
|
||||
|
||||
- The LightmapGI node's `light_data` property will show a `LightmapGIData` resource
|
||||
- The validator checks for `light_data != null` to confirm the map is baked
|
||||
- Baked lightmaps are stored in the `.tscn` file as embedded resources
|
||||
- You can rebake at any time — it overwrites the previous lightmaps
|
||||
|
||||
**Warning:** Opening an unbaked map on a server will default to unlit rendering.
|
||||
Always bake before packaging.
|
||||
|
||||
## 5. ReflectionProbe
|
||||
|
||||
ReflectionProbes add specular reflections to shiny surfaces (metal, glass, water).
|
||||
|
||||
- Place one `ReflectionProbe` near the `cubemap_origin` marker
|
||||
- Set `max_distance` to cover the visible area (10–30 units)
|
||||
- Enable `box_projection` for interior spaces (corrects reflection distortion)
|
||||
- Set `intensity` to 0.5–0.8 (realistic reflection strength)
|
||||
|
||||
**Tip:** You need reflection probes even if your map has no mirrors — they make
|
||||
metal and plastic materials look correct.
|
||||
|
||||
## 6. Lighting Performance Budget
|
||||
|
||||
| Metric | Limit | Enforced |
|
||||
|--------|-------|----------|
|
||||
| Dynamic (non-baked) lights | ≤ 4 | Validator (error) |
|
||||
| Total lights (baked + dynamic) | Unlimited | — |
|
||||
| LightmapGI bounces | ≥ 2 | Validator (warning) |
|
||||
| ReflectionProbes | ≥ 1 | Validator (warning) |
|
||||
| Lightmap max texture | ≤ 2048 | Project settings |
|
||||
|
||||
If you exceed the dynamic light budget, set more lights to
|
||||
`light_bake_mode = Static (2)` — baked lights cost nothing at runtime.
|
||||
|
||||
## 7. Quick Start Checklist
|
||||
|
||||
- [ ] WorldEnvironment with Environment resource assigned
|
||||
- [ ] DirectionalLight3D (sun) with shadows
|
||||
- [ ] 1–3 fill OmniLight3D with Static bake mode
|
||||
- [ ] LightmapGI with quality=2, bounces=3, texel_scale=1.0
|
||||
- [ ] ReflectionProbe at cubemap origin
|
||||
- [ ] All geometry CSG has `use_collision = true`
|
||||
- [ ] Save scene → Bake LightmapGI (Ctrl+Shift+C)
|
||||
- [ ] Verify `light_data` is populated (not null)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Likely Cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| Light leaks at corners | CSG gap < 0.01 units | Overlap CSG edges by 0.1 |
|
||||
| Baked lighting too dark | bounces too low | Set bounces ≥ 2 |
|
||||
| Lightmap seams visible | texel_scale too high | Lower texel_scale to 1.0 |
|
||||
| Shadow acne (striped shadows) | shadow_bias too low | Increase shadow_bias to 0.1 |
|
||||
| ReflectionProbe visible as sphere | probe_mode wrong | Set to Box or disable visibility |
|
||||
| Bake fails with error | Scene not saved | Save `.tscn` before baking |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Map Validation](04-validation.md) — run all checks on your scene
|
||||
- [Packaging & Shipping](05-packaging-and-shipping.md) — publish your map
|
||||
@@ -1,235 +0,0 @@
|
||||
# Map Validation
|
||||
|
||||
The validator catches structural problems, performance budget violations, and
|
||||
lighting issues before your map ships. Two validation modes are available:
|
||||
|
||||
1. **Editor validation** — runs automatically when you open a map scene
|
||||
2. **Headless CLI validation** — runs from the terminal, suitable for CI/CD
|
||||
|
||||
## Editor Validation (template_map.gd)
|
||||
|
||||
Every map created from the template includes `template_map.gd`, a `@tool`
|
||||
script that runs automatically when the scene opens in the Godot editor.
|
||||
|
||||
**How to use it:**
|
||||
1. Open your map's `.tscn` file in the Godot editor
|
||||
2. Look at the **Output** panel (bottom dock)
|
||||
3. The script prints a validation report like:
|
||||
```
|
||||
=== Map Template: Validate Scene ===
|
||||
+ CT Spawn points (5 found)
|
||||
+ T Spawn points (5 found)
|
||||
+ Buy zones (2 found)
|
||||
+ Bomb sites (2 found)
|
||||
+ Cubemap capture origins (1 found)
|
||||
+ Map boundary walls (8 found)
|
||||
+ LightmapGI configured ✓
|
||||
+ LightmapGI: NOT YET BAKED
|
||||
+ DirectionalLight3D (sun) present
|
||||
```
|
||||
|
||||
**Checks performed:**
|
||||
- Required gameplay groups exist (`ct_spawn`, `t_spawn`, `buy_zone`, etc.)
|
||||
- CSG floor and wall geometry present
|
||||
- LightmapGI configured and baked
|
||||
- ReflectionProbe and WorldEnvironment present
|
||||
- DirectionalLight3D (sun) present
|
||||
- Playable area extents estimated
|
||||
|
||||
## Headless CLI Validator (validate_map.gd)
|
||||
|
||||
The headless validator is a 4-module CLI tool at `client/tools/validate_map.gd`.
|
||||
Use it for automated checking before packaging or in CI/CD pipelines.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# From the tactical-shooter repository root
|
||||
godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
|
||||
```
|
||||
|
||||
> The `--` separates Godot engine arguments from user arguments.
|
||||
> The scene path must be a `res://`-prefixed Godot resource path.
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| Code | Meaning | Description |
|
||||
|------|---------|-------------|
|
||||
| 0 | **PASS** | All checks passed, no warnings |
|
||||
| 1 | **WARNINGS** | Passed with non-blocking suggestions |
|
||||
| 2 | **ERRORS** | Failed — must-fix items present |
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Gate map merges on validation
|
||||
|
||||
godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
|
||||
exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "❌ Map validation FAILED — fix errors before submitting"
|
||||
exit 1
|
||||
elif [ $exit_code -eq 1 ]; then
|
||||
echo "⚠️ Map passed with warnings — review suggestions"
|
||||
fi
|
||||
|
||||
echo "✅ Map validated successfully"
|
||||
exit 0
|
||||
```
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════
|
||||
MAP VALIDATOR
|
||||
Scene: res://maps/de_dust2.tscn
|
||||
═══════════════════════════════════════════
|
||||
|
||||
── [validate_scene] ────────────────────────────
|
||||
✓ Root node: World (Node3D)
|
||||
✓ WorldEnvironment (WorldEnvironment)
|
||||
✓ LightmapGI (LightmapGI)
|
||||
✓ Group "ct_spawn" → CT_SpawnPos (5 node(s))
|
||||
✓ Group "t_spawn" → T_SpawnPos (5 node(s))
|
||||
✓ Group "buy_zone" → BuyZone_A (2 node(s))
|
||||
✓ Group "bomb_site" → BombsiteA (2 node(s))
|
||||
✓ Group "cubemap_origin" → CubemapOrigin (1 node(s))
|
||||
✓ pass
|
||||
|
||||
── [validate_polycount] ────────────────────────
|
||||
Meshes scanned: 24
|
||||
Total triangles: 12,842
|
||||
Budget: 50,000
|
||||
✓ Within budget (25.7% of 50000)
|
||||
Top meshes by triangle count:
|
||||
4200 Building_01 (MeshInstance3D)
|
||||
2100 Wall_Detail (MeshInstance3D)
|
||||
1042 Floor_Tiles (MeshInstance3D)
|
||||
✓ pass
|
||||
|
||||
── [validate_textures] ─────────────────────────
|
||||
Textures inspected: 18
|
||||
✓ All textures within 1024×1024 limit
|
||||
✓ pass
|
||||
|
||||
── [validate_lights] ───────────────────────────
|
||||
Light breakdown:
|
||||
DirectionalLight3D: 1
|
||||
OmniLight3D: 2
|
||||
SpotLight3D: 0
|
||||
Dynamic (non-baked): 1
|
||||
Baked: 2
|
||||
✓ Dynamic light count: 1 (limit: 4)
|
||||
✓ LightmapGI baked (light_data present)
|
||||
Quality: 2 (0=Low, 1=Med, 2=High, 3=Ultra)
|
||||
Bounces: 3
|
||||
Texel Scale: 1.0
|
||||
Max Texture Size: 1024
|
||||
✓ WorldEnvironment configured
|
||||
ReflectionProbes: 1
|
||||
✓ pass
|
||||
|
||||
═══════════════════════════════════════════
|
||||
SUMMARY
|
||||
═══════════════════════════════════════════
|
||||
[PASS] validate_scene
|
||||
[PASS] validate_polycount
|
||||
[PASS] validate_textures
|
||||
[PASS] validate_lights
|
||||
|
||||
Errors: 0
|
||||
Warnings: 0
|
||||
Result: PASS
|
||||
```
|
||||
|
||||
## Validator Modules
|
||||
|
||||
### 1. Scene Structure (`validate_scene.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- Scene root is a `Node3D` type
|
||||
- `WorldEnvironment` node present and named correctly
|
||||
- `LightmapGI` node present and named correctly
|
||||
- All 5 required groups assigned (`ct_spawn`, `t_spawn`, `buy_zone`, `bomb_site`, `cubemap_origin`)
|
||||
- Node names follow PascalCase convention (no spaces)
|
||||
- No duplicate required groups on different nodes
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Missing required group: "ct_spawn"
|
||||
✖ Missing WorldEnvironment — map needs a WorldEnvironment node
|
||||
✖ Node name contains spaces: "My Room Section"
|
||||
```
|
||||
|
||||
### 2. Polygon Count (`validate_polycount.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- Total triangle count across all `MeshInstance3D` nodes ≤ **50,000**
|
||||
- Per-mesh triangle breakdown printed (helps identify heavy assets)
|
||||
- Warns on individual meshes > **5,000** triangles
|
||||
- Warns if no `MeshInstance3D` nodes found at all
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Map exceeds 50,000 triangle budget: 68,420 total (36.8% over budget)
|
||||
```
|
||||
|
||||
### 3. Texture Sizes (`validate_textures.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- All material textures ≤ **1024×1024**
|
||||
- Checks `StandardMaterial3D`, `ORMMaterial3D`, and `ShaderMaterial` texture params
|
||||
- Warns on textures > **512×512** (suggested for secondary surfaces)
|
||||
- LightmapGI textures checked separately (baked textures can be larger)
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Texture exceeds 1024×1024: floor_tiles.png (albedo) — 2048×
|
||||
```
|
||||
|
||||
### 4. Lights & Lightmap (`validate_lights.gd`)
|
||||
|
||||
**What it checks:**
|
||||
- Dynamic (non-baked) OmniLight3D + SpotLight3D ≤ **4**
|
||||
- DirectionalLight3D counts toward dynamic budget if not baked
|
||||
- LightmapGI present and baked (`light_data != null`)
|
||||
- Lightmap quality, bounces, texel scale, max texture size
|
||||
- WorldEnvironment assigned with Environment resource
|
||||
- ReflectionProbe presence and `max_distance` coverage
|
||||
- Warns on dynamic lights with shadow enabled (performance cost)
|
||||
|
||||
**Typical errors:**
|
||||
```
|
||||
✖ Too many dynamic lights: 6 (max 4)
|
||||
✖ No LightmapGI node found
|
||||
✖ No WorldEnvironment node found
|
||||
```
|
||||
|
||||
## Passing the Validator
|
||||
|
||||
The goal is exit code 0: **PASS**, no errors, no warnings.
|
||||
|
||||
| Code | Action |
|
||||
|------|--------|
|
||||
| 0 (PASS) | Your map is ready to package |
|
||||
| 1 (WARNINGS) | Fix suggestions before shipping (non-blocking) |
|
||||
| 2 (ERRORS) | Fix all errors — map must pass before packaging |
|
||||
|
||||
> **CI gate rule:** Treat exit code 2 as a hard block. Exit code 1 is advisory
|
||||
> but should be reviewed. Only exit code 0 means "ship it."
|
||||
|
||||
## Adding a Custom Validator Module
|
||||
|
||||
Advanced users can extend the validator:
|
||||
|
||||
1. Create `client/tools/validate_map/validate_<name>.gd` extending `RefCounted`
|
||||
2. Implement `func validate(scene_root: Node, scene_path: String) -> Dictionary`
|
||||
3. Return `{"pass": bool, "errors": [String], "warnings": [String]}`
|
||||
4. Add a `_run_module("<name>", instance, scene_path)` call in
|
||||
`validate_map.gd:_ready()`
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Packaging & Shipping](05-packaging-and-shipping.md) — ship your validated map
|
||||
@@ -1,264 +0,0 @@
|
||||
# Packaging & Shipping
|
||||
|
||||
Once your map is built, validated, and baked, it's time to publish it to the
|
||||
world. Tactical Shooter uses the **PCK** (Godot Resource Pack) format — the map
|
||||
is packaged as a `.pck` file, uploaded to a registry server, and clients
|
||||
download it automatically when they join a server running that map.
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐
|
||||
│ pack_map │ → │ .pck + .json │ → │ Map Registry │ → │ Client │
|
||||
│ .gd │ │ files │ → │ Server │ → │ Downloader │
|
||||
└──────────┘ └──────────────┘ └──────────────┘ └────────────┘
|
||||
(editor (artifacts) (HTTP serving) (autoload)
|
||||
tool)
|
||||
```
|
||||
|
||||
## Step 1: Package the Map
|
||||
|
||||
The `pack_map.gd` script (at `scripts/map_packaging/pack_map.gd`) is a Godot
|
||||
editor tool that exports a `.tscn` scene as a standalone `.pck` resource pack.
|
||||
|
||||
### Usage
|
||||
|
||||
From the **map template project** (not the main game project):
|
||||
|
||||
```bash
|
||||
cd maps/my_first_map/
|
||||
|
||||
godot --headless --script /path/to/scripts/map_packaging/pack_map.gd \
|
||||
--map=res://de_my_map.tscn
|
||||
```
|
||||
|
||||
> Replace `/path/to/` with the actual path to the tactical-shooter repository
|
||||
> root, or copy `pack_map.gd` into your map project's `scripts/` directory.
|
||||
|
||||
### What It Does
|
||||
|
||||
1. Loads the specified `.tscn` scene
|
||||
2. Recursively collects all dependencies (materials, textures, meshes, etc.)
|
||||
3. Exports them to `user://packed_maps/de_my_map.pck`
|
||||
4. Writes a metadata file `user://packed_maps/de_my_map.json`
|
||||
|
||||
### Output Files
|
||||
|
||||
```
|
||||
user://packed_maps/
|
||||
├── de_my_map.pck # Binary resource pack (~1–10 MB for a typical map)
|
||||
└── de_my_map.json # JSON metadata (machine-readable)
|
||||
```
|
||||
|
||||
**Metadata example** (`de_my_map.json`):
|
||||
```json
|
||||
{
|
||||
"map_name": "de_my_map",
|
||||
"source_scene": "res://de_my_map.tscn",
|
||||
"godot_version": {
|
||||
"major": 4,
|
||||
"minor": 2,
|
||||
"string": "4.2"
|
||||
},
|
||||
"packed_at": "2026-06-25T14:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
- The map scene must use **local resources** only (relative `res://` paths)
|
||||
- External dependencies (game shared assets like weapon models, player
|
||||
models) should stay in the base game — the `.pck` is **additive** content
|
||||
- The `.pck` contains only the map's unique assets, not the entire game
|
||||
|
||||
### From the Editor
|
||||
|
||||
You can also run `pack_map.gd` from the Godot editor:
|
||||
|
||||
1. Open your map project in Godot
|
||||
2. Open your map scene (`de_my_map.tscn`)
|
||||
3. **Project → Tools → Pack Current Map** (if configured)
|
||||
4. The script detects the active scene and exports it
|
||||
|
||||
## Step 2: Set Up the Map Registry Server
|
||||
|
||||
The registry server is a lightweight Python HTTP server
|
||||
(`scripts/map_packaging/map_registry_server.py`) that:
|
||||
- Serves `.pck` files for download
|
||||
- Provides a JSON `/maps` endpoint listing available maps
|
||||
- Auto-scans the maps directory every 5 seconds
|
||||
- Computes and serves SHA-256 checksums for integrity verification
|
||||
|
||||
### Starting the Server
|
||||
|
||||
```bash
|
||||
cd /path/to/tactical-shooter
|
||||
|
||||
# Default: port 8090, maps from ./packed_maps/
|
||||
python3 scripts/map_packaging/map_registry_server.py
|
||||
|
||||
# Custom configuration
|
||||
python3 scripts/map_packaging/map_registry_server.py \
|
||||
--port 8080 \
|
||||
--maps-dir /data/tactical-shooter-maps \
|
||||
--verbose
|
||||
```
|
||||
|
||||
**Environment variables:**
|
||||
| Variable | Overrides | Default |
|
||||
|----------|-----------|---------|
|
||||
| `MAP_REGISTRY_PORT` | `--port` / `-p` | 8090 |
|
||||
| `MAP_REGISTRY_MAPS` | `--maps-dir` / `-d` | `./packed_maps/` |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `GET /` | — | Server info and endpoint documentation |
|
||||
| `GET /maps` | — | JSON list of all available maps with metadata |
|
||||
| `GET /maps/<name>.pck` | — | Download map `.pck` binary |
|
||||
| `GET /maps/<name>.json` | — | Download per-map metadata |
|
||||
| `OPTIONS /maps` | — | CORS preflight |
|
||||
|
||||
**Example responses:**
|
||||
|
||||
```
|
||||
GET /maps
|
||||
```
|
||||
```json
|
||||
{
|
||||
"maps": [
|
||||
{
|
||||
"name": "de_dust2",
|
||||
"size": 5432100,
|
||||
"version": 1,
|
||||
"description": "Classic competitive map",
|
||||
"checksum_sha256": "a1b2c3d4...",
|
||||
"packed_at": "2026-06-25T14:30:00"
|
||||
}
|
||||
],
|
||||
"server_name": "Tactical Shooter Map Registry",
|
||||
"server_version": "1.0.0",
|
||||
"map_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
GET /maps/de_dust2.pck
|
||||
```
|
||||
→ Returns the `.pck` binary file as an octet-stream download.
|
||||
|
||||
### Deployment Options
|
||||
|
||||
- **Local dev:** Run on the same machine as the game (default `127.0.0.1:8090`)
|
||||
- **Server host:** Run on a public server alongside the master server
|
||||
- **Docker:** Wrap in a Docker container for easy deployment
|
||||
- **Systemd service:** Create a service file for production hosting
|
||||
|
||||
**Systemd service example:**
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Tactical Shooter Map Registry
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=tactical
|
||||
WorkingDirectory=/opt/tactical-shooter
|
||||
ExecStart=/usr/bin/python3 scripts/map_packaging/map_registry_server.py \
|
||||
--port 8090 --maps-dir /data/tactical-maps
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Step 3: Upload Your Map
|
||||
|
||||
1. Copy the `.pck` and `.json` files to the registry server's maps directory:
|
||||
```bash
|
||||
cp user://packed_maps/de_my_map.pck /data/tactical-maps/
|
||||
cp user://packed_maps/de_my_map.json /data/tactical-maps/
|
||||
```
|
||||
2. The server auto-detects new files within 5 seconds
|
||||
3. Verify via `curl http://your-server:8090/maps`
|
||||
|
||||
## Step 4: Client Downloads
|
||||
|
||||
The client uses `MapDownloader` (autoload at `client/scripts/map_downloader.gd`)
|
||||
to fetch and load maps at runtime.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Player joins a server that specifies a map name
|
||||
2. The client checks its local cache (`user://maps/manifest.json`)
|
||||
3. If the map is not cached, `MapDownloader.fetch_map_list()` polls
|
||||
the registry server to verify the map exists
|
||||
4. `MapDownloader.download_map(name)` downloads the `.pck` to `user://maps/`
|
||||
5. `ProjectSettings.load_resource_pack()` loads it into the game
|
||||
6. The server changes to the new level
|
||||
|
||||
### Map Caching
|
||||
|
||||
```
|
||||
user://maps/
|
||||
├── de_dust2.pck # Cached map binary
|
||||
├── de_inferno.pck # Another cached map
|
||||
└── manifest.json # Cache index
|
||||
```
|
||||
|
||||
**Cache behaviour:**
|
||||
- Maps are cached to disk after download (survives restarts)
|
||||
- The manifest tracks version numbers for staleness checks
|
||||
- `MapDownloader.remove_map(name)` clears a specific map
|
||||
- `MapDownloader.clear_cache()` removes all cached maps
|
||||
|
||||
### Configuration
|
||||
|
||||
The `MapDownloader` autoload can be configured in-game:
|
||||
|
||||
```gdscript
|
||||
# Set the registry server URL (override default http://127.0.0.1:8090)
|
||||
MapDownloader.registry_url = "http://maps.myserver.com:8090"
|
||||
```
|
||||
|
||||
The URL can also be set via the `MAP_REGISTRY_URL` environment variable,
|
||||
which is read in `_ready()`.
|
||||
|
||||
### Signals
|
||||
|
||||
```gdscript
|
||||
# Register for map lifecycle events
|
||||
MapDownloader.map_list_loaded.connect(_on_map_list)
|
||||
MapDownloader.map_download_progress.connect(_on_download_progress)
|
||||
MapDownloader.map_download_complete.connect(_on_download_done)
|
||||
MapDownloader.map_loaded.connect(_on_map_loaded)
|
||||
```
|
||||
|
||||
## Full Pipeline Example
|
||||
|
||||
```bash
|
||||
# 1. Package from the template project
|
||||
cd maps/my_first_map
|
||||
godot --headless --script ../../scripts/map_packaging/pack_map.gd \
|
||||
--map=res://de_my_map.tscn
|
||||
|
||||
# 2. Start registry server
|
||||
cd ../..
|
||||
python3 scripts/map_packaging/map_registry_server.py --port 8090 &
|
||||
|
||||
# 3. Deploy map
|
||||
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.pck \
|
||||
scripts/map_packaging/packed_maps/
|
||||
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.json \
|
||||
scripts/map_packaging/packed_maps/
|
||||
|
||||
# 4. Verify
|
||||
curl -s http://localhost:8090/maps | python3 -m json.tool
|
||||
|
||||
# 5. In-game: connect to server and play the map
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [FAQ & Troubleshooting](06-faq-and-troubleshooting.md) — common issues
|
||||
@@ -1,238 +0,0 @@
|
||||
# FAQ & Troubleshooting
|
||||
|
||||
Common questions, issues, and solutions encountered while building maps.
|
||||
|
||||
## Template Project
|
||||
|
||||
### Q: Can I use the template project without the main game repo?
|
||||
|
||||
Yes. The template project at `client/map_template/` is a **standalone Godot
|
||||
project**. Copy it anywhere, open it in Godot, and start building. You only
|
||||
need the main repo for packaging (`pack_map.gd`) and the registry server
|
||||
(`map_registry_server.py`).
|
||||
|
||||
### Q: Why does Godot say "Import" instead of "Open"?
|
||||
|
||||
The template has its own `project.godot`, making it a separate project. Use
|
||||
**Import** to add it to your Godot project list. You're not opening a scene
|
||||
within an existing project.
|
||||
|
||||
### Q: How do I add the main game's assets to my map?
|
||||
|
||||
The template uses placeholder materials. For final maps:
|
||||
|
||||
1. Copy your map's `.tscn` into the main project under `client/maps/<name>/`
|
||||
2. Open it from the main project's scenes tab
|
||||
3. The main game's modular kit and PBR materials will be available
|
||||
4. Assign materials to your CSG/MeshInstance3D nodes from the main project's
|
||||
asset library
|
||||
|
||||
### Q: Can I use external models from Blender/Maya?
|
||||
|
||||
Yes. Export as `.glb` or `.obj` and import into Godot. Keep triangle counts
|
||||
within budget (≤ 50K total, ≤ 5K per mesh recommended). Place imported meshes
|
||||
as children of the map scene root.
|
||||
|
||||
## Geometry & CSG
|
||||
|
||||
### Q: My map has light leaks at the corners
|
||||
|
||||
Thin gaps between CSG blocks let light bleed through. Fix by overlapping CSG
|
||||
edges by **0.1 units** instead of butting them flush.
|
||||
|
||||
```
|
||||
Bad: Wall ends at x=10.0, floor ends at x=10.0 → gap
|
||||
Good: Wall extends to x=10.1, floor ends at x=10.0 → overlap
|
||||
```
|
||||
|
||||
### Q: Players can walk through walls
|
||||
|
||||
Make sure `use_collision = true` is set on all CSG geometry nodes. CSG nodes
|
||||
don't have collision by default — you must enable it.
|
||||
|
||||
### Q: My map renders as a grey box / no materials
|
||||
|
||||
Placeholder materials are normal during construction. For the final look, you
|
||||
need to:
|
||||
1. Copy the map to the main project (see above)
|
||||
2. Assign materials from the game's asset library
|
||||
3. Re-bake LightmapGI after materials are assigned
|
||||
|
||||
### Q: CSG performance is bad in the editor
|
||||
|
||||
This is normal. CSG is re-calculated every time you move a node. For testing,
|
||||
convert stable geometry to MeshInstance3D:
|
||||
- Right-click CSG parent → **Convert CSG to Mesh**
|
||||
- Keep a backup copy of the CSG hierarchy in another scene
|
||||
|
||||
### Q: How do I make a skybox?
|
||||
|
||||
Add a `WorldEnvironment` node and assign an Environment resource:
|
||||
1. Set **Background** mode to **Sky**
|
||||
2. Create a new **Sky** resource
|
||||
3. Assign a **PanoramaSky** or **ProceduralSky** material
|
||||
4. The sky appears behind your geometry
|
||||
|
||||
### Q: My map has no collision after CSG → Mesh conversion
|
||||
|
||||
When you convert CSG to MeshInstance3D, collision is not preserved. Add a
|
||||
`CollisionShape3D` as a child of each MeshInstance3D, or use Godot's
|
||||
**Mesh → Create Trimesh Collision** (right-click the MeshInstance3D).
|
||||
|
||||
## Lighting
|
||||
|
||||
### Q: My lightmap bake takes forever
|
||||
|
||||
Baking time depends on:
|
||||
- **texel_scale** — lower = higher resolution = longer bake (try 2.0 for testing)
|
||||
- **bounces** — fewer bounces = faster bake (use 1 for quick tests, 3 for final)
|
||||
- **Map size** — larger maps bake longer
|
||||
- **Light count** — more lights = more calculations
|
||||
|
||||
For quick iteration, use Low quality (0) during development and High quality (2)
|
||||
for the final bake.
|
||||
|
||||
### Q: Baked lighting is too dark
|
||||
|
||||
- Increase `bounces` to 3 (more light bounce = brighter interiors)
|
||||
- Add a low-intensity ambient fill (OmniLight3D with Static bake mode)
|
||||
- Lower the `texel_scale` for more lightmap resolution
|
||||
- Check that the DirectionalLight3D intensity is sufficient
|
||||
|
||||
### Q: Lightmap has visible seams
|
||||
|
||||
Seams appear where lightmap texels don't align. Fixes:
|
||||
- Lower `texel_scale` to 1.0 (higher resolution lightmaps)
|
||||
- Ensure CSG edges overlap (no gaps)
|
||||
- Convert CSG to MeshInstance3D before final bake
|
||||
|
||||
### Q: Shadows look blocky or jagged
|
||||
|
||||
This is Godot's shadow mapping at default resolution. For competitive clarity:
|
||||
- Keep `shadow_bias` at 0.1 (higher = softer shadows, fewer artefacts)
|
||||
- Directional shadow `max_distance` at 20–30 (shadows beyond this use lower
|
||||
resolution)
|
||||
|
||||
### Q: I see a sphere artifact on my ReflectionProbe
|
||||
|
||||
The ReflectionProbe has a `probe_mode` property — if set to **Reflection Probe**
|
||||
(sphere mode), it renders as a visible sphere in reflections. Change to **Box
|
||||
Probe** for interior spaces, or disable reflection visibility via the `visible`
|
||||
property on the probe's MeshInstance3D child (Godot 4.x).
|
||||
|
||||
### Q: Can I have a day/night cycle?
|
||||
|
||||
No. LightmapGI bakes lighting statically. Dynamic lighting is limited to
|
||||
≤ 4 real-time lights. Day/night cycles are outside the current scope.
|
||||
|
||||
## Validation
|
||||
|
||||
### Q: The validator says "No MeshInstance3D nodes found"
|
||||
|
||||
Your map is using CSG nodes that haven't been converted to meshes. The
|
||||
validator counts mesh triangles — CSG nodes are scanned as MeshInstance3D
|
||||
after conversion. Either:
|
||||
1. Convert CSG → Mesh for the area you want validated (right-click → Convert CSG to Mesh)
|
||||
2. This warning is expected during early construction stages
|
||||
|
||||
### Q: The validator fails with "Scene cannot be instantiated"
|
||||
|
||||
The scene depends on resources that don't exist in the template project. This
|
||||
usually happens when the map references game assets from the main project.
|
||||
Either:
|
||||
1. Run the validator from the **main game project** (`--path client` with the
|
||||
main `client/project.godot`), or
|
||||
2. Strip external dependencies — the map should be self-contained
|
||||
|
||||
### Q: The validator reports texture errors but my textures are 1024×1024
|
||||
|
||||
Check for textures with different aspect ratios. A 2048×512 texture fails
|
||||
because the larger dimension (2048) exceeds 1024. Resize or tile.
|
||||
|
||||
### Q: Can I skip certain validator checks?
|
||||
|
||||
Not directly. Run the validator, fix errors, then fix warnings. If a check
|
||||
produces a false positive, the `@tool` scripts are in
|
||||
`client/tools/validate_map/` — you can edit the constants (e.g.
|
||||
`MAX_TOTAL_TRIANGLES`) for custom pipelines.
|
||||
|
||||
## Packaging
|
||||
|
||||
### Q: The .pck file is very large
|
||||
|
||||
The `.pck` includes all dependencies of the scene. Large files mean:
|
||||
- High-resolution textures (use ≤ 1024×1024 per the budget)
|
||||
- High-poly meshes (stay under 50K triangles)
|
||||
- Embedded audio or other large assets
|
||||
|
||||
Run the validator first to check budgets.
|
||||
|
||||
### Q: I get "Failed to load pack" in-game
|
||||
|
||||
Possible causes:
|
||||
- `.pck` file is corrupted (check SHA-256 checksum)
|
||||
- The map scene or a dependency path changed between packaging and loading
|
||||
- The `.pck` was built with a different Godot version than the game
|
||||
- The resource path in the `.pck` conflicts with a path in the base game
|
||||
|
||||
**Solution:** Re-package the map from the same Godot version the game uses.
|
||||
Verify with `curl` that the download matches the server's checksum.
|
||||
|
||||
### Q: Can I update a map without changing the client version?
|
||||
|
||||
Yes. The `.pck` is additive content. Update the `.pck` on the registry server
|
||||
and increment the `version` field in the `.json` metadata. The client will
|
||||
re-download when it notices the version mismatch.
|
||||
|
||||
### Q: Can I host the registry server on a different port?
|
||||
|
||||
Yes. Use `--port <port>` or the `MAP_REGISTRY_PORT` environment variable.
|
||||
The client's `MapDownloader` has a `registry_url` property you can set.
|
||||
|
||||
### Q: Can I load more than one .pck at a time?
|
||||
|
||||
Yes. `ProjectSettings.load_resource_pack()` is additive. Multiple `.pck` files
|
||||
can be loaded simultaneously — each adds its resources to the global namespace.
|
||||
|
||||
### Q: The registry server returns 404 for my map
|
||||
|
||||
- Check that the `.pck` file is in the maps directory (default `./packed_maps/`)
|
||||
- Check the file extension is `.pck` (lowercase)
|
||||
- The server auto-scans every 5 seconds — wait or send SIGHUP to force a scan
|
||||
- Verify by listing: `curl http://localhost:8090/maps`
|
||||
|
||||
## General
|
||||
|
||||
### Q: Can I make a hostage rescue map?
|
||||
|
||||
The SDK is gameplay-agnostic. The groups `ct_spawn`, `t_spawn`, `bomb_site`,
|
||||
and `buy_zone` are the default set. You can add your own groups (e.g.,
|
||||
`hostage`, `hostage_zone`) — the game logic discovers them at runtime.
|
||||
Check the game's plugin API documentation for custom group support.
|
||||
|
||||
### Q: Can I use this SDK for a different game?
|
||||
|
||||
The SDK is designed for Tactical Shooter's Godot project structure. The
|
||||
packaging and validation scripts are reusable for any Godot map-authoring
|
||||
workflow, but the group conventions and gameplay nodes are game-specific.
|
||||
|
||||
### Q: The editor validator doesn't run
|
||||
|
||||
The `template_map.gd` script uses `@tool` and checks `Engine.is_editor_hint()`.
|
||||
Make sure:
|
||||
1. The script is attached to the root node of the scene
|
||||
2. You're editing the scene in the Godot editor (not running the game)
|
||||
3. The Output panel is visible (Window → Output)
|
||||
|
||||
### Q: How do I report a bug in the SDK?
|
||||
|
||||
Open an issue on the project's Gitea repository. Include:
|
||||
- SDK version (commit hash)
|
||||
- Godot version
|
||||
- Steps to reproduce
|
||||
- Log output (editor or CLI validator)
|
||||
|
||||
### Q: Can I contribute validator modules?
|
||||
|
||||
Yes. See [Validation — Adding a Custom Validator Module](04-validation.md#adding-a-custom-validator-module).
|
||||
Submit a pull request with your module and updated documentation.
|
||||
@@ -1,203 +0,0 @@
|
||||
# Phase 7: Stack Upgrade — netfox + godot-jolt Migration Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Migrate from raw **ENetMultiplayerPeer** to **netfox** (foxssake/netfox, 1020⭐ MIT)
|
||||
for rollback netcode, client prediction, and lag compensation. Add **godot-jolt**
|
||||
(godot-jolt/godot-jolt, 2535⭐ MIT) for deterministic 128Hz physics.
|
||||
|
||||
**Reference architecture:** [naxIO/netfox-cs-sample](https://github.com/naxIO/netfox-cs-sample)
|
||||
— CS 1.6 clone using netfox rollback patterns (teams, economy, bomb, 6 weapons).
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture (Pre-Migration)
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────┐
|
||||
│ project.godot │
|
||||
│ Autoloads: NetworkManager, ServerConfig, │
|
||||
│ RoundManager, PluginManager │
|
||||
│ Physics: 128Hz (Godot built-in) │
|
||||
│ Rendering: Vulkan Forward+ │
|
||||
│ Main Scene: server_main.tscn │
|
||||
└─────────────────────┬─────────────────────────────┘
|
||||
│
|
||||
┌─────────────┴──────────────┐
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌──────────────────────────┐
|
||||
│ network_manager │ │ SimulationServer │
|
||||
│ .gd (ENet) │ │ (GDExtension C++) │
|
||||
│ │ │ │
|
||||
│ - ENetMultiplayer│ │ - entity spawning │
|
||||
│ - 3 channels │ │ - apply_input() │
|
||||
│ - RPC broadcasts │ │ - fire_weapon() │
|
||||
│ - signals: │ │ - get_entity() │
|
||||
│ player_* │ └──────────┬────────────────┘
|
||||
│ round_* │ │
|
||||
│ score_* │ ┌──────────▼────────────────┐
|
||||
└─────────────────┘ │ fps_character_controller │
|
||||
│ .gd (CharacterBody3D) │
|
||||
┌─────────────────┐ │ │
|
||||
│ round_manager │ │ - Input → SimulationServer│
|
||||
│ .gd (state │ │ - move_local() fallback │
|
||||
│ machine) │ │ - crouch, sprint, jump │
|
||||
│ │ └────────────────────────────┘
|
||||
│ - signals: │
|
||||
│ round_*,score │ ┌──────────────────────────┐
|
||||
│ - _process tick │ │ Player (Node3D) │
|
||||
└─────────────────┘ │ - basic BoxMesh │
|
||||
│ - movement_speed param │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
**Key issue:** ENet has no rollback, no client prediction, no lag compensation.
|
||||
Players on slower connections will feel inconsistent hit registration.
|
||||
|
||||
---
|
||||
|
||||
## Target Architecture (Post-Migration)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ project.godot │
|
||||
│ Autoloads: (same, NetworkManager rewired) │
|
||||
│ Physics: Jolt @ 128Hz (godot-jolt GDExtension) │
|
||||
│ Rendering: Vulkan Forward+ │
|
||||
│ Addons: netfox, netfox.noray, netfox.extras │
|
||||
└─────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌─────────────┴──────────────┐
|
||||
▼ ▼
|
||||
┌──────────────────────┐ ┌──────────────────────────┐
|
||||
│ netfox core │ │ netfox.noray │
|
||||
│ (NetworkEvents, │ │ (NAT punchthrough) │
|
||||
│ RollbackSynch., │ └──────────────────────────┘
|
||||
│ TickInterpolator) │
|
||||
└──────────┬───────────┘ ┌──────────────────────────┐
|
||||
│ │ netfox.extras │
|
||||
▼ │ (BaseNetInput, │
|
||||
┌──────────────────────┐ │ RewindableStateMachine)│
|
||||
│ NetworkManager.gd │ └──────────────────────────┘
|
||||
│ (wrapper over netfox │
|
||||
│ NetworkEvents + │ ┌──────────────────────────┐
|
||||
│ MultiplayerSynch.) │ │ godot-jolt │
|
||||
└──────────┬───────────┘ │ (deterministic physics) │
|
||||
│ └──────────────────────────┘
|
||||
▼
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ RollbackSynchronizer (player state) │
|
||||
│ - _rollback_tick() → deterministic state sync │
|
||||
│ - TickInterpolator → smooth visuals │
|
||||
│ - Client prediction: apply input locally, │
|
||||
│ reconcile from server snapshot │
|
||||
└────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ RewindableAction (hitscan weapons) │
|
||||
│ - Server-authoritative hit detection │
|
||||
│ - Server rewinds physics to match client timing │
|
||||
│ - Per-limb damage: head/chest/waist/legs │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Phases (Task Breakdown)
|
||||
|
||||
### P7.1 Research — netfox migration plan ✅ [THIS DOCUMENT]
|
||||
|
||||
### P7.2 Install — netfox addon
|
||||
1. Download latest release from GitHub
|
||||
2. Extract addons/netfox/, addons/netfox.noray/, addons/netfox.extras/, addons/netfox.internals/
|
||||
3. Enable in Project Settings → Plugins
|
||||
4. Verify `godot --headless --check-only` compiles
|
||||
|
||||
### P7.3 Install — godot-jolt physics
|
||||
1. Download from releases or Asset Library
|
||||
2. Copy addons/godot-jolt/ into project
|
||||
3. Enable plugin, configure physics → 128Hz
|
||||
4. Verify headless compilation
|
||||
|
||||
### P7.4 Port — NetworkManager.gd
|
||||
**Source:** `scripts/network/network_manager.gd` (214 lines, ENet wrapper)
|
||||
**Changes:**
|
||||
- Replace `ENetMultiplayerPeer` → netfox `NetworkEvents`
|
||||
- `start_server()` → use netfox's NetworkEvents server flow
|
||||
- `join_server()` → use netfox's NetworkEvents client flow
|
||||
- Player replication signals → netfox `MultiplayerSynchronizer` or `RollbackSynchronizer`
|
||||
- Round state RPCs (`broadcast_round_*`) → netfox events
|
||||
- Remove channel system (netfox handles prioritisation internally)
|
||||
|
||||
**Equivalent vs New:**
|
||||
|
||||
| Current ENet | netfox Equivalent |
|
||||
|---|---|
|
||||
| ENetMultiplayerPeer | NetworkEvents (connect/disconnect) |
|
||||
| @rpc("authority", "call_local", "reliable") | MultiplayerSynchronizer / RollbackSynchronizer |
|
||||
| broadcast_spawn_player RPC | NetworkEvents + spawn hook |
|
||||
| broadcast_round_start RPC | NetworkEvents event |
|
||||
| 3 channels (input/events/telemetry) | Automatic (netfox manages ordering) |
|
||||
| _process polling | TickInterpolator / _rollback_tick() |
|
||||
|
||||
### P7.5 Build — Client Prediction + Rollback
|
||||
**Key netfox patterns:**
|
||||
- `RollbackSynchronizer` with `_rollback_tick()` for deterministic state sync
|
||||
- `BaseNetInput` with `_gather()` for input collection
|
||||
- `TickInterpolator` for smooth visual interpolation between ticks
|
||||
- Client-side prediction: apply input immediately, reconcile when server snapshot arrives
|
||||
|
||||
### P7.6 Build — Rollback Hitscan + Lag Compensation
|
||||
**Key netfox patterns:**
|
||||
- `RewindableAction` for rollback-safe weapon firing
|
||||
- Server rewinds entity positions to match client's input timestamp
|
||||
- Hit detection inside rollback loop
|
||||
- Per-limb damage (head 2x, chest 1.0x, waist 0.75x, legs 0.5x)
|
||||
|
||||
### P7.7 Port — RoundManager to netfox
|
||||
**Source:** `server/scripts/round_manager.gd` (647 lines)
|
||||
**Changes:**
|
||||
- Round state signals (`round_started`, `round_ended`, etc.) → netfox events
|
||||
- `player_died` etc. → netfox replicated state or RPCs
|
||||
- `get_round_state_snapshot()` → netfox synchronized property
|
||||
|
||||
### P7.8 Port — FPS Controller to BaseNetInput
|
||||
**Source:** `client/characters/character/fps_character_controller.gd` (401 lines)
|
||||
**Changes:**
|
||||
- Replace manual `_input_dict` → `BaseNetInput` subclass
|
||||
- Use `_gather()` to collect input per tick
|
||||
- Movement logic becomes server-authoritative (no more `move_local` fallback except singleplayer)
|
||||
- Keep mouse look, crouch transition, sprint toggle — adapt for netfox tick
|
||||
|
||||
### P7.9 Test — Headless Export + Deployment
|
||||
- `godot --headless --export-debug "Linux/X11" build/server/tactical-shooter.x86_64`
|
||||
- Server starts clean, clients connect, rollback works across LAN
|
||||
- Push to Gitea, update systemd service, verify on server
|
||||
|
||||
---
|
||||
|
||||
## Reference Files
|
||||
|
||||
| File | Lines | Key Role |
|
||||
|---|---|---|
|
||||
| `scripts/network/network_manager.gd` | 214 | ENet transport wrapper, RPC broadcasts |
|
||||
| `scripts/network/player.gd` | ~60 | Basic player node |
|
||||
| `scripts/network/server_main.gd` | ~150 | Server entry point |
|
||||
| `scripts/network/client_main.gd` | ~120 | Client entry point |
|
||||
| `client/characters/character/fps_character_controller.gd` | 401 | FPS movement + input to SimulationServer |
|
||||
| `client/characters/character/fps_camera.gd` | ~80 | Camera look |
|
||||
| `server/scripts/round_manager.gd` | 647 | Round state machine |
|
||||
| `server/scripts/plugin_api/plugin_manager.gd` | 573 | Plugin system |
|
||||
| `scenes/server/server_main.tscn` | 14 | Server scene |
|
||||
| `scenes/client/client_main.tscn` | 14 | Client scene |
|
||||
| `scenes/player.tscn` | 15 | Player prefab |
|
||||
| `project.godot` | 33 | Autoloads, physics settings |
|
||||
|
||||
## Resources
|
||||
|
||||
- [netfox docs](https://foxssake.github.io/netfox/)
|
||||
- [netfox GitHub](https://github.com/foxssake/netfox)
|
||||
- [naxIO/netfox-cs-sample — CS 1.6 reference](https://github.com/naxIO/netfox-cs-sample)
|
||||
- [godot-jolt GitHub](https://github.com/godot-jolt/godot-jolt)
|
||||
- [VANTIX tactical shooter reference (C#, architecture only)](https://github.com/justin-bobr/vantix)
|
||||
@@ -1,248 +0,0 @@
|
||||
# Netfox Migration Checklist — File-by-File Audit
|
||||
|
||||
Generated: 2026-07-02
|
||||
Task: P7.1 (research — netfox migration plan)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ network_manager.gd │
|
||||
│ (ENetMultiplayerPeer transport) │
|
||||
│ + optional NetworkEvents overlay│
|
||||
└──────┬───────────────┬───────────┘
|
||||
│ │
|
||||
┌────────────┘ └────────────┐
|
||||
v v
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ server_main.gd │ │ client_main.gd │
|
||||
│ (dedicated server)│ │ (client entry) │
|
||||
└─────────┬───────────┘ └──────────┬──────────┘
|
||||
│ │
|
||||
┌─────────┴───────────┐ ┌──────────┴──────────┐
|
||||
│ round_manager.gd │ │ round_replicator.gd │
|
||||
│ (state machine) │ │ (client mirror) │
|
||||
└─────────────────────┘ └─────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ player.gd │
|
||||
│ RollbackSynchronizer (dynamic child) │
|
||||
│ ├── state_properties: pos, is_alive, team_id, spec_tgt │
|
||||
│ └── input_properties: → PlayerNetInput/* │
|
||||
│ │
|
||||
│ PlayerNetInput (child Node) │
|
||||
│ ├── move_direction, look_yaw, look_pitch │
|
||||
│ ├── jump, sprint, crouch, shoot, aim │
|
||||
│ └── connects to NetworkTime.before_tick_loop │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Headless Compilation
|
||||
✅ **Passes** — `godot --headless --check-only` reports 0 parser errors.
|
||||
- Runtime warnings (ENet host, netfox unavailable, empty map rotation) are all expected in CI environments.
|
||||
- All netfox-dependent code paths guarded by `Engine.has_singleton()` + duck-typing.
|
||||
|
||||
---
|
||||
|
||||
## File-by-File Audit
|
||||
|
||||
### 1. network_manager.gd (245 lines) — TRANSPORT LAYER
|
||||
|
||||
**Current role:** ENetMultiplayerPeer management + signal relay hub.
|
||||
|
||||
**DONE:**
|
||||
- ✅ Optional NetworkEvents overlay for lifecycle signals (`_try_connect_netfox()`)
|
||||
- ✅ All netfox refs use `Engine.has_singleton("NetworkEvents")` — no `class_name` refs
|
||||
|
||||
**REMAINING (6 legacy RPC broadcasting functions):**
|
||||
|
||||
| Line | RPC | netfox Replacement | Status |
|
||||
|------|-----|--------------------|--------|
|
||||
| 193 | `broadcast_spawn_player` | Replaced by RollbackSynchronizer state sync. Keep until client discoverse players from state, not RPC. | 🔶 keep temporarily |
|
||||
| 199 | `broadcast_despawn_player` | Same as above | 🔶 keep temporarily |
|
||||
| 209 | `broadcast_round_start` | Move to StateSynchronizer on a round-state autoload (P7.7) | ❌ |
|
||||
| 214 | `broadcast_round_end` | Same as above | ❌ |
|
||||
| 219 | `broadcast_match_end` | Same as above | ❌ |
|
||||
| 224 | `broadcast_score_change` | Same as above | ❌ |
|
||||
|
||||
**Networking signals (9 total)** — all stay as-is. They are event hooks for consumers (client_main, round_replicator), not data sync.
|
||||
|
||||
---
|
||||
|
||||
### 2. player.gd (251 lines) — PLAYER ENTITY
|
||||
|
||||
**Current role:** Player character with Team/Round integration. Uses RollbackSynchronizer.
|
||||
|
||||
**DONE:**
|
||||
- ✅ RollbackSynchronizer dynamically created: `state_properties=[position, is_alive, team_id, spectate_target_id]`
|
||||
- ✅ `input_properties=[PlayerNetInput/move_direction, look_yaw, look_pitch, jump, sprint, crouch, shoot, aim]`
|
||||
- ✅ `_rollback_tick(delta, tick, is_input)` with deterministic movement
|
||||
- ✅ `_create_rollback_sync_node()` uses `load()` + `new()` from path — no class_name refs
|
||||
- ✅ Duck-typed netfox checks via `has_method()`, `Engine.has_singleton()`
|
||||
|
||||
**REMAINING:**
|
||||
- ✅ `TickInterpolator` child for smooth interpolation (dynamically created in `_setup_tick_interpolator()`)
|
||||
- ✅ `TickInterpolator` on remote player instances for visual rollback
|
||||
|
||||
---
|
||||
|
||||
### 3. player_net_input.gd (91 lines) — INPUT GATHERING
|
||||
|
||||
**Current role:** Tick-synchronized input collector, child of Player.
|
||||
|
||||
**DONE:**
|
||||
- ✅ 8 exported input properties matching RollbackSynchronizer's `input_properties`
|
||||
- ✅ Connects to `NetworkTime.before_tick_loop`
|
||||
- ✅ Reads yaw/pitch from parent FPSCharacterController via duck-typing
|
||||
- ✅ Extends Node directly (avoids `BaseNetInput` headless class_name issues)
|
||||
|
||||
**REMAINING:**
|
||||
- FPSCharacterController duplicates input gathering in `_physics_process` — should read from PlayerNetInput instead → P7.8
|
||||
|
||||
---
|
||||
|
||||
### 4. server_main.gd (342 lines) — SERVER ENTRY POINT
|
||||
|
||||
**DONE:**
|
||||
- ✅ Uses `NetworkManager.start_server()` for ENet bootstrap
|
||||
- ✅ Connects to `NetworkManager.player_connected/disconnected`
|
||||
|
||||
**REMAINING — 7 RPC call sites:**
|
||||
|
||||
| Line | Call | netfox Plan |
|
||||
|------|------|-------------|
|
||||
| 120 | `broadcast_round_start.rpc(...)` | StateSynchronizer in P7.7 |
|
||||
| 142 | `broadcast_round_end.rpc(...)` | StateSynchronizer in P7.7 |
|
||||
| 178 | `broadcast_match_end.rpc(...)` | StateSynchronizer in P7.7 |
|
||||
| 184 | `broadcast_score_change.rpc(...)` | StateSynchronizer in P7.7 |
|
||||
| 288 | `broadcast_spawn_player.rpc(...)` | RollbackSynchronizer state (keep for now) |
|
||||
| 327 | `broadcast_despawn_player.rpc(...)` | RollbackSynchronizer state (keep for now) |
|
||||
| 332 | `broadcast_despawn_player.rpc(...)` | RollbackSynchronizer state (keep for now) |
|
||||
|
||||
---
|
||||
|
||||
### 5. client_main.gd (124 lines) — CLIENT ENTRY POINT
|
||||
|
||||
**DONE:**
|
||||
- ✅ Uses `NetworkManager.join_server()`
|
||||
- ✅ Connects to `remote_player_spawned/despawned` for visualization
|
||||
|
||||
**REMAINING:**
|
||||
- Creates full Player nodes with RollbackSynchronizer for each remote player (✅ correct pattern)
|
||||
- ✅ `TickInterpolator` on remote player instances (dynamically created in `_setup_tick_interpolator()`)
|
||||
|
||||
---
|
||||
|
||||
### 6. round_replicator.gd (157 lines) — CLIENT ROUND STATE
|
||||
|
||||
**DONE:**
|
||||
- ✅ Connects to NetworkManager's broadcast signals
|
||||
- ✅ Maintains round state (round_number, scores, time, freeze)
|
||||
- ✅ Uses netfox `NetworkTime` singleton for freeze tick tracking (line 84, 123)
|
||||
|
||||
**REMAINING:**
|
||||
- Could be replaced by a `RoundState` autoload with `StateSynchronizer` for rollback-consistent round state (P7.7)
|
||||
|
||||
---
|
||||
|
||||
### 7. round_manager.gd (647 lines) — SERVER ROUND STATE MACHINE
|
||||
|
||||
**DONE:** ✅ Zero networking code. Pure state machine emitting signals. No changes needed.
|
||||
|
||||
---
|
||||
|
||||
### 8. fps_character_controller.gd (411 lines) — FPS CONTROLS
|
||||
|
||||
**DONE:**
|
||||
- ✅ `get_current_yaw()` / `get_current_pitch()` methods for PlayerNetInput
|
||||
- ✅ Standalone `_move_local()` fallback
|
||||
- ✅ SimulationServer refs use `Engine.has_singleton()`
|
||||
|
||||
**REMAINING (P7.8):**
|
||||
- `_physics_process` gathers input AND sends to SimulationServer directly — duplicates PlayerNetInput
|
||||
- Need to read from PlayerNetInput child instead of `_get_move_direction()`
|
||||
- Clarify: PlayerNetInput → RollbackSynchronizer → server physics → reconcile → FPSCharacterController applies
|
||||
|
||||
---
|
||||
|
||||
## netfox Pattern Mapping
|
||||
|
||||
| Pattern | netfox API | Status |
|
||||
|---------|-----------|--------|
|
||||
| Player state sync | RollbackSynchronizer.state_properties | ✅ |
|
||||
| Input client→server | RollbackSynchronizer.input_properties | ✅ |
|
||||
| Tick-driven movement | `_rollback_tick(delta, tick, is_input)` | ✅ |
|
||||
| Client prediction | `enable_prediction = true` | ✅ |
|
||||
| Smooth interpolation | TickInterpolator | ✅ |
|
||||
| Round state broadcast | StateSynchronizer | ❌ P7.7 |
|
||||
| Rollback-safe weapon fire | RewindableAction | ❌ P7.6 |
|
||||
| Lag comp / hitscan | RewindableAction + mutation | ❌ P7.6 |
|
||||
| Visual rollback | TickInterpolator + _rollback_tick | ✅ |
|
||||
| Lifecycle signals | NetworkEvents | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Netfox Singleton Availability (Headless)
|
||||
|
||||
| Singleton | Editor | Headless | Access Pattern |
|
||||
|-----------|--------|----------|----------------|
|
||||
| NetworkEvents | ✅ | ❌ | `Engine.has_singleton("NetworkEvents")` |
|
||||
| NetworkTime | ✅ | ❌ | `Engine.has_singleton("NetworkTime")` |
|
||||
| NetworkRollback | ✅ | ❌ | `Engine.has_singleton("NetworkRollback")` |
|
||||
| NetworkPerformance | ✅ | ❌ | `Engine.has_singleton("NetworkPerformance")` |
|
||||
|
||||
All access guarded — no class_name references in our code.
|
||||
|
||||
---
|
||||
|
||||
## Remaining Work Order (Dependency Graph)
|
||||
|
||||
```
|
||||
P7.6 (RewindableAction) ── needs ── RollbackSynchronizer working (✅ DONE)
|
||||
P7.5 (TickInterpolator) ── needs ── RollbackSynchronizer working (✅ DONE)── [[DONE]] ✅
|
||||
P7.7 (Round state netfox) ── needs ── NetworkEvents working (✅ DONE)
|
||||
P7.8 (FPSController refactor) ── needs ── P7.6 input flow clarified
|
||||
P7.9 (LAN test + deploy) ── needs ── all of the above
|
||||
```
|
||||
|
||||
**Recommended order:** P7.6 → P7.5 → P7.7 → P7.8 → P7.9
|
||||
|
||||
---
|
||||
|
||||
## Files NOT Needing Changes
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `server/scripts/round_manager.gd` | Pure state machine, no networking |
|
||||
| `scripts/config/server_config.gd` | Config loader, no networking |
|
||||
| `scripts/network/netfox_bootstrap.gd` | No-op placeholder |
|
||||
| `server/scripts/plugin_api/plugin_manager.gd` | Plugin mgmt, no networking |
|
||||
| `client/weapons/data/weapon_registry.gd` | Data-only |
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|------------|------------|
|
||||
| Headless compilation fails | High | Low | All netfox refs use has_singleton/has_method. Current check passes. |
|
||||
| RollbackSynchronizer vs PlayerNetInput property mismatch | Medium | Low | `input_properties` paths verified against @export var names — matched. |
|
||||
| Dual input path (SimulationServer + RollbackSynchronizer) | High | Medium | FPSCharacterController sends to both — needs architectural cleanup (P7.8) |
|
||||
| netfox API incompatibility with Godot 4.7 | Medium | Low | netfox 1.35.3 targets 4.3+; manual verification done — compilation passes |
|
||||
|
||||
---
|
||||
|
||||
## Key Files Summary
|
||||
|
||||
| File | Lines | Netfox Status | RPCs | Signals | Changes Needed |
|
||||
|------|-------|---------------|------|---------|----------------|
|
||||
| network_manager.gd | 245 | ✅ overlay wired | 6 ❌ | 9 ✅ | Remove 6 RPCs post-P7.7 |
|
||||
| player.gd | 251 | ✅ rollback + tick + interpolator | 0 | 0 | ✅ |
|
||||
| player_net_input.gd | 91 | ✅ tick gather | 0 | 0 | None atm |
|
||||
| server_main.gd | 342 | ✅ thin layer | 7 call sites ❌ | 2 | Migrate to StateSynchronizer P7.7 |
|
||||
| client_main.gd | 124 | ✅ thin layer | 0 | 2 signal connects | None atm |
|
||||
| round_replicator.gd | 157 | 🔶 signal-driven | 0 | 5 connects + 4 emits | Migrate to StateSynchronizer P7.7 |
|
||||
| round_manager.gd | 647 | ✅ no changes | 0 | 0 | None |
|
||||
| fps_character_controller.gd | 411 | 🔶 has getters | 0 | 0 | Input flow refactor P7.8 |
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 162 KiB |
@@ -0,0 +1,30 @@
|
||||
# BaseNetInput
|
||||
|
||||
Base class for Input nodes used with rollback.
|
||||
|
||||
During rollback, multiple logical ticks are simulated in the span of a single
|
||||
network tick. Since these are just logical ticks, no actual input arrives during
|
||||
them from the input devices.
|
||||
|
||||
The solution is to gather input before the tick loop, and use that input for
|
||||
any new ticks simulated during the rollback.
|
||||
|
||||
## Gathering input
|
||||
|
||||
This class provides a virtual `_gather` method that you can override. Set the
|
||||
variables configured in [RollbackSynchronizer] in your own implementation:
|
||||
|
||||
```gdscript
|
||||
extends BaseNetInput
|
||||
|
||||
var movement: Vector3 = Vector3.ZERO
|
||||
|
||||
func _gather():
|
||||
movement = Vector3(
|
||||
Input.get_axis("move_west", "move_east"),
|
||||
0,
|
||||
Input.get_axis("move_north", "move_south")
|
||||
)
|
||||
```
|
||||
|
||||
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
|
||||
@@ -0,0 +1,80 @@
|
||||
# NetworkSimulator
|
||||
|
||||
During testing, it is crucial to test your game under realistic network
|
||||
conditions, including latency and potentially packet loss.
|
||||
|
||||
Network conditions can be simulated using [clumsy], [netem], or with *netfox*'s
|
||||
*NetworkSimulator*. It auto-connects instances when launched from the editor,
|
||||
and simulates various network configurations.
|
||||
|
||||
## How to Use
|
||||
|
||||
Enable and configure *Autoconnect* in the project settings.
|
||||
|
||||

|
||||
|
||||
When your game launches one instance will start an [ENetMultiplayerPeer] server
|
||||
and the rest will connect to it.
|
||||
|
||||
Either a `NetworkSimulator.server_created` or
|
||||
`NetworkSimulator.client_connected` signal will be fired which you can use to
|
||||
bootstrap your game code to.
|
||||
|
||||
!!!note
|
||||
*NetworkSimulator* will only work when the game is ran from the editor.
|
||||
Otherwise it will disable itself, regardless of project settings. This is a
|
||||
failsafe to avoid affecting production builds of your games.
|
||||
|
||||
## Configuration
|
||||
|
||||
Hostname
|
||||
: The hosting address. Usually `127.0.0.1` but can be changed to `*`, if you
|
||||
want other machines to be able to join.
|
||||
|
||||
Server Port
|
||||
: Which port to listen on. A second server port with latency / loss will open
|
||||
one number higher if they are set to more than zero.
|
||||
|
||||
Use Compression
|
||||
: Will make use of ENET's range encoder to keep packet sizes down.
|
||||
|
||||
Simulated Latency ( ms )
|
||||
: Traffic delay, in milliseconds.
|
||||
|
||||
Simlated Packet Loss Chance
|
||||
: What percentage of packets will to drop, simulating bad network conditions.
|
||||
|
||||
## Running in CI and other environments
|
||||
|
||||
In certain cases, you might not need the autoconnect feature to run, even if
|
||||
it's enabled in the project settings.
|
||||
|
||||
One example could be running checks on your project in CI, or wanting to run a
|
||||
single script.
|
||||
|
||||
In these cases, Godot will still identify itself as running in editor, so the
|
||||
autoconnect feature will start hosting, keeping the process alive. This leads
|
||||
to timeout errors, or the process running indefinitely.
|
||||
|
||||
To avoid such cases, set any of the following environment variables to any
|
||||
non-empty string:
|
||||
|
||||
- `CI`
|
||||
- `NETFOX_CI`
|
||||
- `NETFOX_NO_AUTOCONNECT`
|
||||
|
||||
If *NetworkSimulator* detects any of the above environment variables, it will
|
||||
deactivate, regardless of project settings.
|
||||
|
||||
!!!tip
|
||||
Github Actions automatically sets the `CI` environment variable.
|
||||
*NetworkSimulator* will automatically disable itself when running in a
|
||||
Github Actions workflow.
|
||||
|
||||
See the [Github blog entry] for more details.
|
||||
|
||||
|
||||
[clumsy]: https://jagt.github.io/clumsy/
|
||||
[netem]: https://man7.org/linux/man-pages/man8/tc-netem.8.html
|
||||
[ENetMultiplayerPeer]: https://docs.godotengine.org/en/4.1/classes/class_enetmultiplayerpeer.html
|
||||
[Github blog entry]: https://github.blog/changelog/2020-04-15-github-actions-sets-the-ci-environment-variable-to-true/
|
||||
@@ -0,0 +1,137 @@
|
||||
# NetworkWeapon
|
||||
|
||||
Class to simplify writing networked weapons.
|
||||
|
||||
A weapon, in this context, is anything that can be fired and spawn objects (
|
||||
projectiles ) upon being fired.
|
||||
|
||||
## Responsive projectiles
|
||||
|
||||
Upon firing, sending a request to the server and waiting for the response with
|
||||
the projectile would introduce a delay. Doing a full-on state synchronization
|
||||
with [MultiplayerSynchronizer] or [RollbackSynchronizer] can be unfeasible with
|
||||
too many projectiles, and unnecessary, since most of the time, projectiles act
|
||||
and move the same way regardless of their surroundings.
|
||||
|
||||
Instead, upon firing, a projectile is spawned instantly. At the same time, a
|
||||
request is sent to the server. If the server accepts the projectile, it will
|
||||
spawn it and broadcasts its starting state. Since the server's state is the
|
||||
source of truth, the projectile's local state will be updated with the
|
||||
difference. This is called *reconciliation*.
|
||||
|
||||
If the client requests a projectile with an unlikely state, it will be
|
||||
rejected. This is to avoid players cheating, for example by requesting
|
||||
projectiles at a more advantageous position than they're at.
|
||||
|
||||
If the server is too strict with what difference is considered acceptable and
|
||||
what not, legitimate players may get cases where they fire a projectile which
|
||||
disappears after a short time period.
|
||||
|
||||
## Implementing a weapon
|
||||
|
||||
*NetworkWeapon* provides multiple functions to override. Make sure that all
|
||||
these methods work the same way on every player's game, otherwise players will
|
||||
experience glitches.
|
||||
|
||||
*_can_fire* returns a bool, indicating whether the weapon can be fired. For
|
||||
example, this method can return false if the weapon was fired recently and is
|
||||
still on cooldown. **Do not** update state here. Use *_after_fire* instead.
|
||||
|
||||
*_can_peer_use* indicates whether a given peer can fire the weapon. Due to the
|
||||
way RPCs are set up under the hood, any of the players can try to fire a
|
||||
weapon. Use this method to check if the player trying to fire has permission,
|
||||
e.g. a player is not trying to use someone else's weapon.
|
||||
|
||||
*_after_fire* is called after the weapon is successfully fired. Can be used to
|
||||
update state ( e.g. last time the weapon was fired ) and play sound effects.
|
||||
|
||||
*_spawn* creates the projectile. Make sure to return the created node.
|
||||
|
||||
*_get_data* must return the projectile's starting state in a dictionary. This
|
||||
can contain any property that is relevant to the projectile and must be
|
||||
synchronized. For example, *global_transform* is important to ensure that the
|
||||
projectile starts from the right position. On the other hand, projectile speed
|
||||
does not need to be captured if it's the same for every projectile.
|
||||
|
||||
*_apply_data* must apply the captured properties to a projectile.
|
||||
|
||||
*_is_reconcilable* checks if the difference between two projectile states ( as
|
||||
captured by *_get_data* ) is close enough to be allowed. Can be used to reject
|
||||
cheating.
|
||||
|
||||
*_reconcile* adjusts the projectile based on the difference between the local
|
||||
and server state.
|
||||
|
||||
## Specializations
|
||||
|
||||
*NetworkWeapon* extends [Node]. This also means that anything extending
|
||||
*NetworkWeapon* is also a node, and thus can't have a position for example.
|
||||
|
||||
Two specialized classes are provided - *NetworkWeapon3D*, and *NetworkWeapon2D*
|
||||
- extending Node3D and Node2D respectively.
|
||||
|
||||
This way, weapons can have transforms and have a presence in the game world.
|
||||
They also take care of reconciliation, implementing *_get_data*, *_apply_data*,
|
||||
*_is_reconcilable*, and *_reconcile*. These can be overridden, but make sure to
|
||||
to call the base class with *super(...)*.
|
||||
|
||||
Reconciliation is based on distance, and can be configured with the
|
||||
*distance_threshold* property.
|
||||
|
||||
Under the hood, these specializations create a special *NetworkWeapon* node,
|
||||
that proxies all the method calls back to the specialization. This is a
|
||||
workaround to build multiple inheritance in a single inheritance language.
|
||||
|
||||
## Compensating latency
|
||||
|
||||
Whenever the weapon is fired, it takes time for that event to arrive at the
|
||||
host. To combat this, along with the weapon being fired, the firing's tick is
|
||||
also sent. This way, the host doesn't only know that the weapon was fired, but
|
||||
it also knows *when*.
|
||||
|
||||
To retrieve the exact tick, call *get_fired_tick()*.
|
||||
|
||||
This can be used to adjust the created projectile's simulation, e.g. by
|
||||
simulating it from its spawn to the current tick in `_after_fire()`:
|
||||
|
||||
```gdscript
|
||||
func _after_fire(projectile: Node3D):
|
||||
last_fire = get_fired_tick()
|
||||
|
||||
for t in range(get_fired_tick(), NetworkTime.tick):
|
||||
if projectile.is_queued_for_deletion():
|
||||
break
|
||||
projectile._tick(NetworkTime.ticktime, t)
|
||||
```
|
||||
|
||||
!!!note
|
||||
To track the tick the weapon was last fired ( e.g. for cooldowns ), make sure
|
||||
to use `get_fired_tick()`, instead of `NetworkTime.tick`.
|
||||
|
||||
## Hitscan weapons
|
||||
|
||||
Use *NetworkWeaponHitscan3D* to build networked hitscan weapons. It builds upon
|
||||
the same principle as *NetworkWeapon*, but modified for hitscan.
|
||||
|
||||
Most of the callbacks are the same, with the following differences:
|
||||
|
||||
*_spawn* is not used, as there's no projectiles involved.
|
||||
|
||||
*_on_fire* is called whenever the weapon is successfully fired. Can be used to
|
||||
implement effects on firing, such as sound or visual effects.
|
||||
|
||||
*_on_hit* is called whenever the weapon hits a target. Depending on
|
||||
configuration, this may be another player, a different character, or just level
|
||||
geometry. The raycast result is passed as a parameter to distinguish between
|
||||
hits.
|
||||
|
||||
Reconciliation is handled under the hood - *_get_data*, *_apply_data*,
|
||||
*_is_reconcilable*, and *_reconcile* do not need to be implemented.
|
||||
|
||||
Hitscan weapons don't implement *get_fired_tick()*, as there's no projectile to
|
||||
simulate.
|
||||
|
||||
|
||||
[MultiplayerSynchronizer]: https://docs.godotengine.org/en/stable/classes/class_multiplayersynchronizer.html
|
||||
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
|
||||
[Node]: https://docs.godotengine.org/en/stable/classes/class_node.html
|
||||
@@ -0,0 +1,86 @@
|
||||
# Physics
|
||||
|
||||
At the time of writing official Godot releases have no support for manually
|
||||
stepping physics simulations. As such if you want to use physics nodes with
|
||||
rollback such as RigidBody you will need to run either a fork that supports
|
||||
stepping or use an alternate physics addon that exposes stepping.
|
||||
|
||||
!!!tip
|
||||
An example game demonstrating physics and rollback is available: [Godot
|
||||
Rocket League].
|
||||
|
||||
## Known Options
|
||||
|
||||
- **Godot with Stepping PR**: Build the Godot editor and relevant export
|
||||
templates manually with the [physics stepping PR] applied. See the [Godot
|
||||
documentation] for compilation instructions.
|
||||
- **Rapier Physics Addon**: A third-party 2D/3D physics engine for Godot with
|
||||
stepping support. Visit [godot.rapier.rs] for details.
|
||||
- **Blazium Fork**: A Godot fork with enhanced physics features, including
|
||||
stepping. Learn more at [blazium.app].
|
||||
|
||||
!!!tip
|
||||
For using Godot with the Stepping Physics PR applied, a [community run
|
||||
repository] is available with a custom build. Note that this repository and
|
||||
its builds are provided as-is, and are not associated with Godot nor the PR's
|
||||
author.
|
||||
|
||||
!!!note
|
||||
The current version of the Rapier drivers were tested against Godot Rapier
|
||||
version **0.8.26**.
|
||||
|
||||
## Enabling Physics Engine Rollback
|
||||
|
||||
To enable physics rollback, add the appropriate physics driver node to the root
|
||||
of your scene tree based on your physics engine. Because stepping methods are
|
||||
unavailable in standard Godot the classes are hidden to avoid compile errors.
|
||||
You will need to enable them in the editor by going to *Project -> Tools ->
|
||||
Enable physics driver*
|
||||
|
||||
- PhysicsDriver2D or PhysicsDriver3D for Godot’s default physics or Blazium.
|
||||
- RapierPhysicsDriver2D or RapierPhysicsDriver3D for the Rapier Physics Addon.
|
||||
|
||||
These nodes disable Godot’s default physics processing and step the physics
|
||||
simulation at **netfox**’s network tick rate.
|
||||
|
||||

|
||||
|
||||
## Physics Driver Configuration
|
||||
|
||||
**Physics Factor** - Controls the number of physics steps per network tick.
|
||||
For example, if your network tick rate is 30 Hz (one tick every ~33ms) but you
|
||||
need a 60 Hz physics simulation for smoother collisions, set this to 2 to run
|
||||
two physics steps per tick.
|
||||
|
||||
**Rollback Physics Space** - When enabled, rolls back all physics objects in
|
||||
the scene tree. Depending on how complex your scene tree is you may wish to
|
||||
only rollback specific nodes for performance rather than the entire simulation
|
||||
space.
|
||||
|
||||
## NetworkRigidBody
|
||||
|
||||
NetworkRigidBody2D and NetworkRigidBody3D nodes enable RigidBody
|
||||
synchronization with RollbackSynchronizer, keeping clients in sync with the
|
||||
server’s physics simulation. These nodes can replace standard RigidBody nodes
|
||||
with some minor setup.
|
||||
|
||||
!!!note
|
||||
Avoid using StateSyncronizer with NetworkRigidBody, as it doesn't participate
|
||||
in rollback it will end up stepping faster than other nodes.
|
||||
|
||||
To make use of NetworkRigidBody you need to:
|
||||
|
||||
1. Configure your RollbackSynchronizer to include the NetworkRigidBody's
|
||||
`physics_state` as a state property.
|
||||
2. Move physics-related logic from `_physics_process` to
|
||||
`_physics_rollback_tick`.
|
||||
|
||||

|
||||
|
||||
|
||||
[Godot Rocket League]: https://github.com/albertok/godot-rocket-league
|
||||
[community run repository]: https://github.com/albertok/godot/releases
|
||||
[physics stepping PR]: https://github.com/godotengine/godot/pull/76462
|
||||
[Godot documentation]: https://docs.godotengine.org/en/stable/contributing/development/compiling/index.html
|
||||
[godot.rapier.rs]: https://godot.rapier.rs/
|
||||
[blazium.app]: https://blazium.app/
|
||||
@@ -0,0 +1,66 @@
|
||||
# RewindableRandomNumberGenerator
|
||||
|
||||
A random number generator that can be used inside the [rollback tick loop].
|
||||
|
||||
An important point for writing code that works well in rollback is that it
|
||||
behaves the same given the same circumstances, no matter how many times it's
|
||||
run. It also must behave the same no matter which peer is running it.
|
||||
|
||||
Godot's built-in [RandomNumberGenerator] is not aware of rollback, so it will
|
||||
consistently return different numbers for each tick, and potentially on each
|
||||
peer.
|
||||
|
||||
To fix that, the *RewindableRandomNumberGenerator* generates the same random
|
||||
sequences on each peer, and for each tick.
|
||||
|
||||
It implements most of the same methods, so it's close to a drop-in replacement.
|
||||
|
||||
## Creating the generator
|
||||
|
||||
The *RewindableRandomNumberGenerator* requires a seed upon initialization. This
|
||||
seed must be the same on all peers.
|
||||
|
||||
!!!warning
|
||||
If two *different* RNGs use the *same* seed, they will also generate the
|
||||
same random numbers. Make sure that you use different seeds for different
|
||||
objects.
|
||||
|
||||
For RNG's used by singletons, hard-coding different seed values is a very
|
||||
simple approach:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(15)
|
||||
```
|
||||
|
||||
If these objects also have names or similar, consistent identifiers, a simple
|
||||
hash works well too:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(hash("Exit beacon"))
|
||||
```
|
||||
|
||||
For dynamically spawned objects, or just to avoid the possibility of a human
|
||||
error, the node path can be hashed:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(hash(get_path()))
|
||||
```
|
||||
|
||||
This assumes that the same node will be spawned under the same path on all
|
||||
peers, which is also a requirement for RPCs to work.
|
||||
|
||||
## Generating random numbers
|
||||
|
||||
The *RewindableRandomNumberGenerator* can be used in the same way as Godot's
|
||||
built-in [RandomNumberGenerator]. All the per-peer and per-tick consistency is
|
||||
ensured under the hood:
|
||||
|
||||
```gd
|
||||
var rng := RewindableRandomNumberGenerator.new(0)
|
||||
|
||||
var dice_roll := rng.randi_range(1, 6)
|
||||
```
|
||||
|
||||
|
||||
[rollback tick loop]: ../../netfox/guides/network-rollback.md
|
||||
[RandomNumberGenerator]: https://docs.godotengine.org/en/stable/classes/class_randomnumbergenerator.html
|
||||
@@ -0,0 +1,214 @@
|
||||
# RewindableStateMachine
|
||||
|
||||
Rollback-aware state machine implementation.
|
||||
|
||||
State machines are often used in games to implement different behaviors.
|
||||
However, most implementations are not prepared for rollbacks. This class
|
||||
provides an extensible implementation that can be used alongside a
|
||||
[RollbackSynchronizer].
|
||||
|
||||
For a full example, see [multiplayer-state-machine].
|
||||
|
||||
## Creating a state machine
|
||||
|
||||
The first step is to add the RewindableStateMachine to your scene. It also
|
||||
requires a RollbackSynchronizer that manages its `state` property. Unless these
|
||||
conditions are satisfied, an editor warning will be displayed.
|
||||
|
||||
!!!note
|
||||
Editor warnings are only updated when the node tree changes. Configuration
|
||||
changes don't trigger an update. You may need to reload the scene after
|
||||
fixing a warning, or make a tree change, like deleting and re-adding a node
|
||||
by cutting and pasting.
|
||||
|
||||

|
||||
|
||||
Notice the RollbackSynchronizer added as a sibling to the
|
||||
RewindableStateMachine, and having its `state` property configured.
|
||||
|
||||
## Implementing states
|
||||
|
||||
States are where the custom gameplay logic can be implemented. Each state must
|
||||
be an extension of the RewindableState class, and added as a child to the
|
||||
RewindableStateMachine.
|
||||
|
||||
States react to the game world using the following callbacks:
|
||||
|
||||
`tick(delta, tick, is_fresh)`
|
||||
: Called for every rollback tick the state is active.
|
||||
|
||||
`enter(previous_state, tick)`
|
||||
: Called when entering the state.
|
||||
|
||||
`exit(next_state, tick)`
|
||||
: Called when exiting the state.
|
||||
|
||||
`can_enter(previous_state)`
|
||||
: Called before entering the state. The state is only entered if this method
|
||||
returns true.
|
||||
|
||||
`display_enter(previous_state, tick)`
|
||||
: Called before displaying the state.
|
||||
|
||||
`display_exit(next_state, tick)`
|
||||
: Called before displaying a different state.
|
||||
|
||||
You can override any of these callbacks to implement your custom behaviors.
|
||||
|
||||
For example, the snippet below implements an idle state, that transitions to
|
||||
other states based on movement inputs:
|
||||
|
||||
```gdscript
|
||||
extends RewindableState
|
||||
|
||||
@export var input: PlayerInputStateMachine
|
||||
|
||||
func tick(delta, tick, is_fresh):
|
||||
if input.movement != Vector3.ZERO:
|
||||
state_machine.transition(&"Move")
|
||||
elif input.jump:
|
||||
state_machine.transition(&"Jump")
|
||||
```
|
||||
|
||||
Transitions are based on *node names*, i.e. calling `transition(&"Move")` will
|
||||
transition to a state node called *Move*.
|
||||
|
||||

|
||||
|
||||
States must be added as children under a RewindableStateMachine to work.
|
||||
|
||||
## Using signals instead of classes
|
||||
|
||||
*RewindableState* nodes also emit signals during their lifetime. This enables
|
||||
an alternate style of implementing states, by connecting handlers to different
|
||||
signals. This can be useful if you want to keep all your logic in a single
|
||||
script, among others.
|
||||
|
||||
Each of these signals correspond to a callback explained above:
|
||||
|
||||
* `on_enter()` → `enter()`
|
||||
* `on_tick()` → `tick()`
|
||||
* `on_exit()` → `exit()`
|
||||
* `on_display_enter()` → `display_enter()`
|
||||
* `on_display_exit()` → `display_exit()`
|
||||
|
||||
## Adding states
|
||||
|
||||
Once implemented, add the state nodes as children of the
|
||||
*RewindableStateMachine* in the Scene Tree. When doing this programmatically,
|
||||
make sure to set the state's `owner` to the target *RewindableStateMachine*.
|
||||
Without the owner set, the *RewindableStateMachine* won't recognize the state.
|
||||
|
||||
## Display State vs State
|
||||
|
||||
There's two sets of callbacks for state transition - `enter()`/`exit()` and
|
||||
`display_enter()`/`display_exit()`.
|
||||
|
||||
The `enter()`/`exit()` callbacks are intended for implementing game logic. The
|
||||
`display_enter()`/`display_exit()` are intended for implementing presentation
|
||||
logic - visuals, animations, sound effects, etc.
|
||||
|
||||
The same applies to `on_state_changed` vs. `on_display_state_changed`.
|
||||
|
||||
Let's take an example. The game is currently on tick @8. It needs to re-run
|
||||
ticks @0 to @8 during rollback. In these ticks, the player moves a bit,
|
||||
performs a jump, and then stops after moving a bit more:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
concise "Player" as P
|
||||
|
||||
@0
|
||||
P is Idle
|
||||
|
||||
@1
|
||||
P is Moving
|
||||
|
||||
@3
|
||||
P is Jumping
|
||||
|
||||
@5
|
||||
P is Moving
|
||||
|
||||
@8
|
||||
P is Idle
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
This will trigger the following state changes:
|
||||
|
||||
* Tick@1: Idle → Moving
|
||||
* Tick@3: Moving → Jumping
|
||||
* Tick@5: Jumping → Moving
|
||||
* Tick@8: Moving → Idle
|
||||
|
||||
For each of the above, the `on_state_changed` signal will be emitted, and the
|
||||
`enter()`/`exit()` callbacks will be triggered.
|
||||
|
||||
This makes the above callbacks ideal for game logic, e.g. adding an upward
|
||||
velocity to the player when they enter the `Jumping` state.
|
||||
|
||||
Note that the *displayed* state does not change. Before the rollback loop, the
|
||||
player's state was `Idle`. After the rollback loop, the player's state is also
|
||||
`Idle`. Even though the player has ran and performed a jump, it wouldn't make
|
||||
sense to change their animation or play any sound effect.
|
||||
|
||||
Let's take a different rollback example:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
concise "Player" as P
|
||||
|
||||
@5
|
||||
P is Moving
|
||||
|
||||
@8
|
||||
P is Idle
|
||||
|
||||
@9
|
||||
P is Jumping
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
In this case, the display state *did* change. Before the rollback loop, the
|
||||
player's state was `Moving`. After the rollback loop, the player's state is
|
||||
`Jumping`. It would make sense to change the player's animation and play a
|
||||
jumping sound effect.
|
||||
|
||||
This can be done by using the display state callbacks - the
|
||||
`on_display_state_changed` signal, and the `display_enter()`/`display_exit()`
|
||||
methods.
|
||||
|
||||
## Caveats
|
||||
|
||||
RewindableStateMachine runs in the [rollback tick loop], which means that all
|
||||
the [Rollback Caveats] apply.
|
||||
|
||||
In addition, rollback ticks are only ran for nodes that have known inputs for
|
||||
the given tick, and *need* to be simulated - either on the server to determine
|
||||
the new state, or on the client to predict. In practice, ticks are usually only
|
||||
ran on the host owning state and the client owning inputs. The rest of the
|
||||
peers use the state broadcast by the host.
|
||||
|
||||
**This means that transition callbacks are not always ran.** This is by design
|
||||
and expected ( see [#327] ).
|
||||
|
||||
As a best practice, in the `enter()`, `exit()` callbacks and the
|
||||
`on_state_changed` signal, only change game state - i.e. properties that are
|
||||
configured as state in [RollbackSynchronizer].
|
||||
|
||||
To update visuals - e.g. change animation, spawn effects, etc. -, use either
|
||||
the `on_display_state_changed` signal, or the `display_enter()` and
|
||||
`display_exit()` callbacks to react to state transitions.
|
||||
|
||||
[multiplayer-state-machine]: https://github.com/foxssake/netfox/tree/main/examples/multiplayer-state-machine
|
||||
[RollbackSynchronizer]: ../../netfox/nodes/rollback-synchronizer.md
|
||||
[rollback tick loop]: ../../netfox/guides/network-rollback.md#network-rollback-loop
|
||||
[Rollback Caveats]: ../../netfox/tutorials/rollback-caveats.md
|
||||
[#327]: https://github.com/foxssake/netfox/issues/327#issuecomment-2491251374
|
||||
@@ -0,0 +1,33 @@
|
||||
# WindowTiler
|
||||
|
||||
A developer convenience feature that automatically tiles the launched windows
|
||||
when working from the editor.
|
||||
|
||||

|
||||
|
||||
## Limitations
|
||||
|
||||
### Borderless mode on Linux
|
||||
|
||||
Setting window position and size works inconsistently under Linux at the time
|
||||
of writing. Your mileage may vary based on your desktop environment and
|
||||
distribution.
|
||||
|
||||
In case the windows don't tile properly with *Borderless* enabled, disabling it
|
||||
is a fallback.
|
||||
|
||||
### Window decorations
|
||||
|
||||
At the time of writing, there is no known and consistent way to compensate for
|
||||
window decoration size and offset. In practice, this means that windows may
|
||||
slightly overlap.
|
||||
|
||||
## Configuration
|
||||
|
||||
*Auto Tile Windows* Enables auto tiling from editor launches.
|
||||
|
||||
*Screen* Which screen number to move and tile the windows to.
|
||||
|
||||
*Borderless* Enable borderless mode to make the most out of the screen real
|
||||
estate.
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# Noray
|
||||
|
||||
Singleton providing [noray] integration.
|
||||
|
||||
*noray* is a backend application that orchestrates connection between players.
|
||||
To do this, players send a connection request to *noray*, and in turn *noray*
|
||||
sends the players' external addresses to eachother. It is then up to the
|
||||
players to conduct a handshake process.
|
||||
|
||||
If the handshake fails, players can request a *relay* from *noray*. In these
|
||||
cases, *noray* will receive data from one player and forward it to the other,
|
||||
acting as a middle man.
|
||||
|
||||
## Identifiers
|
||||
|
||||
*noray* identifies players with two different IDs: OpenID and PrivateID.
|
||||
|
||||
*OpenID* is public, and can be shared with other players. This ID is used to
|
||||
identify hosts when connecting to games.
|
||||
|
||||
*PrivateID* is only sent to the player it identifies and should **never** be
|
||||
shared. Acts similar to a password, and is used to authorize commands.
|
||||
|
||||
## Relays and NAT Punchthrough
|
||||
|
||||
*noray* provides two methods of connecting players.
|
||||
|
||||
*NAT Punchthrough* relies on the NAT table. Players must continuously send data
|
||||
to eachother until either two-way communication is established, or a timeout is
|
||||
reached. For certain router setups, NAT punchthrough does not work.
|
||||
|
||||
See: [NAT Punch-through for Multiplayer Games]
|
||||
|
||||
For *relays*, *noray* allocates a specific port to a given player. When *noray*
|
||||
receives data on this port, it will forward it as-is to the player. As long as
|
||||
*noray* is accessible over the internet, relays should work reliably no matter
|
||||
the router setup.
|
||||
|
||||
## Registering with noray
|
||||
|
||||
To start using *noray*, connect to a *noray* server, request IDs by
|
||||
registering, and then register the remote address:
|
||||
|
||||
```gdscript
|
||||
var host = "some.noray.host"
|
||||
var port = 8890
|
||||
var err = OK
|
||||
|
||||
# Connect to noray
|
||||
err = await Noray.connect_to_host(host, port)
|
||||
if err != OK:
|
||||
return err # Failed to connect
|
||||
|
||||
# Register host
|
||||
Noray.register_host()
|
||||
await Noray.on_pid
|
||||
|
||||
# Register remote address
|
||||
# This is where noray will direct traffic
|
||||
err = await Noray.register_remote()
|
||||
if err != OK:
|
||||
return err # Failed to register
|
||||
```
|
||||
|
||||
By calling `Noray.register_host()`, a request is sent to *noray*. Once a
|
||||
response is received, both the `on_pid` and `on_oid` signals are fired, for
|
||||
receiving the PrivateID and OpenID respectively.
|
||||
|
||||
The remote address must be registered so that *noray* knows where to direct
|
||||
other players wanting to connect. This process also sets `Noray.local_port`,
|
||||
which is where traffic can be received through *noray*.
|
||||
|
||||
## Starting a host
|
||||
|
||||
To host a game, start listening on *noray*'s local port:
|
||||
|
||||
```gdscript
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
var err = peer.create_server(Noray.local_port)
|
||||
|
||||
if err != OK:
|
||||
return false # Failed to listen on port
|
||||
```
|
||||
|
||||
The rest is handled by *noray*.
|
||||
|
||||
## Starting a client
|
||||
|
||||
To connect to a game, send a request to *noray* with the host's OpenID.
|
||||
|
||||
```gdscript
|
||||
var oid = "abcd1234"
|
||||
|
||||
# Connect using NAT punchthrough
|
||||
Noray.connect_nat(oid)
|
||||
|
||||
# Or connect using relay
|
||||
Noray.connect_relay(oid)
|
||||
```
|
||||
|
||||
Once the request is sent, *noray* will send a message to both the client and
|
||||
the host players to connect to each other. The actual connection is done by
|
||||
handling signals.
|
||||
|
||||
!!!note
|
||||
*noray* provides no functionality to share OpenIDs. For development, you
|
||||
can display the OpenID in a textbox, letting players copy it and share over
|
||||
their preferred messaging app.
|
||||
|
||||
## Handling signals
|
||||
|
||||
When a connect message is received, the appropriate signal is fired.
|
||||
|
||||
*on_connect_nat* is fired to connect with NAT punchthrough.
|
||||
|
||||
*on_connect_relay* is fired to connect to a relay.
|
||||
|
||||
In both cases, a public address is passed to the signal handler, in the form of
|
||||
an address string and a port. Handlers must conduct a handshake ( e.g. with
|
||||
[PacketHandshake] ) and connect if successful.
|
||||
|
||||
Client example:
|
||||
|
||||
```gdscript
|
||||
func _ready():
|
||||
Noray.on_connect_nat.connect(_handle_connect)
|
||||
Noray.on_connect_relay.connect(_handle_connect)
|
||||
|
||||
func _handle_connect(address: String, port: int) -> Error:
|
||||
# Do a handshake
|
||||
var udp = PacketPeerUDP.new()
|
||||
udp.bind(Noray.local_port)
|
||||
udp.set_dest_address(address, port)
|
||||
|
||||
var err = await PacketHandshake.over_packet_peer(udp)
|
||||
udp.close()
|
||||
|
||||
if err != OK:
|
||||
return err
|
||||
|
||||
# Connect to host
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
err = peer.create_client(address, port, 0, 0, 0, Noray.local_port)
|
||||
|
||||
if err != OK:
|
||||
return err
|
||||
|
||||
return OK
|
||||
```
|
||||
|
||||
!!!note
|
||||
Make sure to **always** specifiy the local port for the client - this is
|
||||
the only port noray recognizes, and failing to specify it will result in
|
||||
broken connectivity.
|
||||
|
||||
Host example:
|
||||
|
||||
```gdscript
|
||||
func _ready():
|
||||
Noray.on_connect_nat.connect(_handle_connect)
|
||||
Noray.on_connect_relay.connect(_handle_connect)
|
||||
|
||||
func _handle_connect(address: String, port: int) -> Error:
|
||||
var peer = get_tree().get_multiplayer().multiplayer_peer as ENetMultiplayerPeer
|
||||
var err = await PacketHandshake.over_enet(peer.host, address, port)
|
||||
|
||||
if err != OK:
|
||||
return err
|
||||
|
||||
return OK
|
||||
```
|
||||
|
||||
!!!note
|
||||
The host handshake is a bit different, as it can't receive manual packets,
|
||||
only send them. So it assumes that the target is always responsive, and
|
||||
just blasts them with a bunch of packets. If the target is indeed
|
||||
responsive, it can connect. If not, nothing happens, as expected.
|
||||
|
||||
[noray]: https://github.com/foxssake/noray
|
||||
[NAT Punch-through for Multiplayer Games]: https://keithjohnston.wordpress.com/2014/02/17/nat-punch-through-for-multiplayer-games/
|
||||
[PacketHandshake]: ./packet-handshake.md
|
||||
@@ -0,0 +1,116 @@
|
||||
# PacketHandshake
|
||||
|
||||
Singleton implementing handshake over UDP.
|
||||
|
||||
The point of the handshake itself is to confirm two-way connection between
|
||||
two parties - i.e. both parties can receive message from the other and
|
||||
receive acknowledgement from the other that their messages have arrived.
|
||||
|
||||
This is an important step before establishing connection for actual game
|
||||
play, as this lets both the client's and server's routers ( if any ) know
|
||||
that traffic is expected and should be let through.
|
||||
|
||||
## NAT punchthrough
|
||||
|
||||
Most players are behind a router. Routers are directly connected to the
|
||||
internet, and protect machines behind them from unwanted traffic.
|
||||
|
||||
When routers receive packets from an unknown source, those packets are rejected
|
||||
and don't reach the player's device. When data was sent to that address first,
|
||||
routers see traffic as a reply and allow incoming data.
|
||||
|
||||
To take an example, if a random PC starts sending traffic your way, the router
|
||||
will reject it. If you send data to the host behind godotengine.org, your
|
||||
router will allow incoming traffic from it. Otherwise, you wouldn't be able to
|
||||
open the website in your browser, as the incoming HTTP response would be
|
||||
rejected.
|
||||
|
||||
This can be used to our advantage. If both players start sending traffic
|
||||
towards eachother, eventually the routers will assume it's a response to some
|
||||
request and allow the traffic.
|
||||
|
||||
> This is a very simplified description of how routers work. NAT punchthrough
|
||||
> does not always work. For further reading, see [Network address translation].
|
||||
|
||||
## Handshake process
|
||||
|
||||
To confirm two-way connectivity, a string is sent back and forth, encoding the
|
||||
player's knowledge about the connection:
|
||||
|
||||
The *Read flag* is set once we have received data from the other player.
|
||||
|
||||
The *Write flag* is set once we send data to the other player. Since data is
|
||||
always sent, this flag is always set.
|
||||
|
||||
The *Duplex flag* is set when we have received data from the other player
|
||||
knowing that they have also received data from us. This means that data flows
|
||||
both ways.
|
||||
|
||||
The handshake process is successful when both players have the *Duplex flag*
|
||||
set *and* both players know that the other player has the *Duplex flag* set.
|
||||
|
||||
Each flag is encoded as its specific character or a hyphen. The encoded string
|
||||
is prepended with a dollar sign. For example:
|
||||
|
||||
* *$rw-* means that we have sent and received data from the other player
|
||||
* *$rwx* means that the *read*, *write*, and *duplex* flags are all set
|
||||
|
||||
Here's the handshake process illustrated:
|
||||
|
||||
```puml
|
||||
@startuml
|
||||
|
||||
actor "Player A" as A
|
||||
entity "Router A" as RA
|
||||
boundary Internet as Net
|
||||
entity "Router B" as RB
|
||||
actor "Player B" as B
|
||||
|
||||
A ->x RB : $-w-
|
||||
note over RB: Packet denied
|
||||
|
||||
B ->x RA : $-w-
|
||||
note over RA: Packet denied
|
||||
|
||||
note over RA, RB: NAT table updated on both routers
|
||||
|
||||
A -> B: $-w-
|
||||
note over RB: Packed allowed
|
||||
|
||||
B -> A: $-w-
|
||||
note over RB: Packed allowed
|
||||
|
||||
A -> B: $rwx
|
||||
B -> A: $rwx
|
||||
|
||||
note over Net #lightgreen: Connection established
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## Handshake over PacketPeer
|
||||
|
||||
To run the handshake over raw UDP, call `PacketHandshake.over_packet_peer()`. The
|
||||
specified PacketPeer will be used to send data until two-way connectivity is
|
||||
confirmed or the timeout is reached. Between every packet sent, it takes a
|
||||
short pause.
|
||||
|
||||
!!!note
|
||||
The PacketPeer must already be configured with a target address.
|
||||
|
||||
## Handshake over ENet
|
||||
|
||||
If the game is already running, the handshake must be done over the already
|
||||
active connection. For this case, use `PacketHandshake.over_enet_peer()`. If
|
||||
the [ENetMultiplayerPeer] is not accessible from where you want to do the
|
||||
handshake, use `PacketHandshake.over_enet()`.
|
||||
|
||||
This connection can't be used to receive custom packets, only to send them. So
|
||||
the target address will be spammed with traffic confirming two-way connectivity
|
||||
until timeout. Handshake will always be considered successful.
|
||||
|
||||
If the connectivity exists, players will simply connect. Otherwise,
|
||||
connectivity will fail as expected, regardless of the handshake results.
|
||||
|
||||
[Network address translation]: https://en.wikipedia.org/wiki/Network_address_translation
|
||||
[ENetMultiplayerPeer]: https://docs.godotengine.org/en/4.1/classes/class_enetmultiplayerpeer.html
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,64 @@
|
||||
# Authoritative servers
|
||||
|
||||
The idea behind multiplayer servers is replicating state. As long each player
|
||||
sees approximately the same things happening on their screen, the illusion of a
|
||||
shared world works.
|
||||
|
||||
## Naive replication
|
||||
|
||||
To implement state replication, we could say that each player is responsible
|
||||
for their own state. Players see the effects of their input instantly, as they
|
||||
own their state and thus their avatar.
|
||||
|
||||
The issue is that clients can't be trusted. Your game client is distributed to
|
||||
players, who run it in various environments. These environments are out of the
|
||||
developer's control, and provide an attack surface for cheats.
|
||||
|
||||
For example, a modified game client might always report full HP no matter how
|
||||
many hits the player takes. If each player is responsible for their own state,
|
||||
the cheating player's full-HP state will be replicated to everyone else.
|
||||
|
||||
## Server as the source of truth
|
||||
|
||||
What can be controlled is the server, with dedicated hosting. Thus, the server
|
||||
can be the single source of truth - or in other words, authoritative. Clients
|
||||
send their inputs to the server, and the server responds with the updated game
|
||||
state.
|
||||
|
||||
This makes cheating difficult, as players have limited influence over the game
|
||||
world.
|
||||
|
||||
Game code can also be simplified - everything that affects the gameplay is run
|
||||
on the server, while other things such as visual effects are run on the
|
||||
clients.
|
||||
|
||||
The tradeoff is that it takes time for the updated game state to arrive from
|
||||
the server. This necessitates techniques that mask this delay, such as
|
||||
[Client-side prediction and Server reconciliation].
|
||||
|
||||
## Other approaches
|
||||
|
||||
Server-authoritative gameplay with CSP is not a silver bullet unfortunately,
|
||||
and different games may require different approaches to network state
|
||||
replication.
|
||||
|
||||
One good example is RTS games. These games can have 50+ or even hundreds of
|
||||
units navigating the map and interacting. Broadcasting all of their state to
|
||||
all of the players from the server may not always be feasible.
|
||||
|
||||
Instead, players broadcast their actions ( inputs ) to each other and update
|
||||
their game state in lockstep. While this approach can scale up to hundreds of
|
||||
units, it has other drawbacks. One of these is developing the game in such a
|
||||
way that the simulation is exactly the same across multiple CPU architectures
|
||||
down to each bit.
|
||||
|
||||
For more on this approach, see: [1500 Archers on a 28.8: Network Programming in
|
||||
Age of Empires and Beyond]
|
||||
|
||||
For more approaches, see: [Networking for Physics Programmers]
|
||||
|
||||
[1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond]: https://www.gamedeveloper.com/programming/1500-archers-on-a-28-8-network-programming-in-age-of-empires-and-beyond
|
||||
|
||||
[Networking for Physics Programmers]: https://www.gdcvault.com/play/1022195/Physics-for-Game-Programmers-Networking
|
||||
|
||||
[Client-side prediction and Server reconciliation]: https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html
|
||||
@@ -0,0 +1,35 @@
|
||||
# Servers, clients, and ownership
|
||||
|
||||
Much of this documentation discusses things in context of servers and clients.
|
||||
This page is intended to clear up how this translates to Godot's concept of
|
||||
multiplayer ownership.
|
||||
|
||||
## Ownership in Godot
|
||||
|
||||
In Godot's multiplayer system, each node belongs to a multiplayer peer, i.e. a
|
||||
player. This can be set from scripts, and is not replicated. This means that
|
||||
the logic assigning ownership to nodes must produce the same result on every
|
||||
machine for things to work consistently.
|
||||
|
||||
## Ownership in netfox
|
||||
|
||||
To mesh better with Godot's existing conventions, *netfox* doesn't work in
|
||||
terms of server and client, but uses ownership instead.
|
||||
|
||||
Whenever *the server* is mentioned, it refers to a given node's owner.
|
||||
|
||||
In practice, this means that nodes representing game state are and should be
|
||||
owned by the server.
|
||||
|
||||
## Limitations
|
||||
|
||||
At the time of writing, ownership is hard-coded in some cases. One such case is
|
||||
*NetworkTime*, which is always owned by the host peer and always takes the host
|
||||
peer's time as reference.
|
||||
|
||||
This means that peer-to-peer games are not officially supported by *netfox*,
|
||||
but might be able to work with some workarounds. If feasible, you can build
|
||||
self-hosted games by including *netfox.noray*.
|
||||
|
||||
In theory, multiple players can own different parts of the game state, but
|
||||
*netfox* is not tested for such use cases.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Interpolators
|
||||
|
||||
Tracks interpolators for various data types. Provided as a static class.
|
||||
|
||||
To smooth out motion between network ticks, [TickInterpolator] interpolates
|
||||
nodes' state properties between the current and the previous tick. The type of
|
||||
data to be interpolated is not known in advance, and can be any built-in or
|
||||
even custom type configured by the developer.
|
||||
|
||||
*Interpolators* provides methods to register interpolators for any data type,
|
||||
and even provides some for built-in data types.
|
||||
|
||||
## Interpolating values
|
||||
|
||||
*Interpolators* can be used to interpolate between any two values, as long as
|
||||
they hold the same data type:
|
||||
|
||||
```gdscript
|
||||
extends Node3D
|
||||
|
||||
@export var target_node
|
||||
@export var approach_time = 0.5
|
||||
|
||||
func _process(delta):
|
||||
# Approach target node, if it exists
|
||||
if target_node:
|
||||
var from_xform = global_transform
|
||||
var to_xform = target_node.global_transform
|
||||
var factor = delta / approach_time
|
||||
|
||||
global_transform = Interpolators.interpolate(from_xform, to_xform, factor)
|
||||
```
|
||||
|
||||
Note that in this case, *Interpolators* will try to look up the appropriate
|
||||
interpolator based on the provided values. If no interpolator is found, a
|
||||
fallback is used, that simply returns the value closer to *factor* - i.e. the
|
||||
starting value if *factor* is less than 0.5 and the target value otherwise.
|
||||
|
||||
## Caching interpolators
|
||||
|
||||
To avoid having to look up the right interpolator every frame, you can cache
|
||||
it:
|
||||
|
||||
```gdscript
|
||||
extends Node3D
|
||||
|
||||
@export target_node
|
||||
@export approach_time = 0.5
|
||||
|
||||
var interpolator
|
||||
|
||||
func _ready():
|
||||
interpolator = Interpolators.find_for(global_transform)
|
||||
|
||||
func _process(delta):
|
||||
# Approach target node, if it exists
|
||||
if target_node:
|
||||
var from_xform = global_transform
|
||||
var to_xform = target_node.global_transform
|
||||
var factor = delta / approach_time
|
||||
|
||||
global_transform = interpolator.call(from_xform, to_xform, factor)
|
||||
```
|
||||
|
||||
## Custom interpolators
|
||||
|
||||
*Interpolators* supports interpolators for custom data types, and even
|
||||
overriding built-in interpolators. Both can be done by registering an
|
||||
interpolator:
|
||||
|
||||
```gdscript
|
||||
Interpolators.register(
|
||||
func(a): return a is float, # Condition
|
||||
func(a, b, f): return lerpf(a, b, f * f) # Interpolation
|
||||
)
|
||||
```
|
||||
|
||||
The above registers a custom interpolator by specifying a condition function
|
||||
and an interpolation function. Since it applies to an already supported type,
|
||||
it overrides the built-in interpolator.
|
||||
|
||||
During lookup, *Interpolators* calls the *condition* function of each
|
||||
interpolator and returns the one whose *condition* function returns true. If
|
||||
multiple interpolators are applicable, *Interpolators* returns the last
|
||||
registered one.
|
||||
|
||||
The *interpolation* function receives the starting value *a*, the target value
|
||||
*b* and the interpolation factor *f*.
|
||||
|
||||
## Built-in interpolators
|
||||
|
||||
The following types are supported by default:
|
||||
|
||||
* float
|
||||
* Vector2
|
||||
* Vector3
|
||||
* Transform2D
|
||||
* Transform3D
|
||||
|
||||
[TickInterpolator]: ../nodes/tick-interpolator.md
|
||||
@@ -0,0 +1,148 @@
|
||||
# Logging
|
||||
|
||||
During runtime, it can be useful to print some diagnostic info to the console -
|
||||
this is called logging. The netfox addons include a logging system to help with
|
||||
debugging. This is useful when running the game locally, but also helps if
|
||||
there's log files players can attach with their bug reports.
|
||||
|
||||
The system produces logs like this:
|
||||
|
||||
```
|
||||
[DBG][@0][#1][_][netfox::NetworkPerformance] Network performance enabled, registering performance monitors
|
||||
[DBG][@0][#1][_][netfox.extras::WindowTiler] Tiling with sid: f2682d1, uid: 17627261006193110
|
||||
[DBG][@0][#1][_][netfox.extras::NetworkSimulator] Feature disabled
|
||||
[DBG][@0][#1][_][netfox.extras::WindowTiler] Tiling as idx 0 / 1 - 17627261006193110 in ["17627261006193110"]
|
||||
[DBG][@22][#1][_][netfox.extras::NetworkWeapon] Calling after fire hook for Bomb Projectile 5sswh7lcsgbq
|
||||
[DBG][@27][#1][_][fb::Displacer] Created explosion at (2.027323, 1.500942, -14.99592)@26
|
||||
[DBG][@34][#1][_][netfox.extras::NetworkWeapon] Calling after fire hook for Bomb Projectile u4h8opz52lin
|
||||
[DBG][@46][#1][_][fb::Displacer] Created explosion at (4.892477, 1.500942, -14.83388)@45
|
||||
[DBG][@46][#1][_][netfox.extras::NetworkWeapon] Calling after fire hook for Bomb Projectile 2u1d9n456yl1
|
||||
[DBG][@57][#1][_][fb::Displacer] Created explosion at (4.814114, 1.500942, -14.57117)@56
|
||||
```
|
||||
|
||||
This page will elaborate on how to produce your own logs, and what each part
|
||||
means.
|
||||
|
||||
## Using the logger
|
||||
|
||||
The logging system can be accessed by creating an instance of `NetfoxLogger`.
|
||||
Every logger has a name, and belongs to a module. Both of these can be
|
||||
arbitrary strings, and are included in the logged messages.
|
||||
|
||||
Messages can be logged as different *logging levels*:
|
||||
|
||||
```gd
|
||||
var logger := NetfoxLogger.new("my-game", "Player")
|
||||
logger.trace("Detailed message")
|
||||
logger.debug("Something happened")
|
||||
logger.info("Hi!")
|
||||
logger.warning("Couldn't connect")
|
||||
logger.error("Game missing?")
|
||||
```
|
||||
|
||||
To use string interpolation, you can also pass the template string and values
|
||||
separately. This can be useful to avoid substituting the values in case the
|
||||
message never gets printed because of filtering:
|
||||
|
||||
```gd
|
||||
logger.trace("Adjusted clock by %.2fms, offset: %.2fms, new time: %.4fss", [nudge * 1000., offset * 1000., _clock.get_time()])
|
||||
```
|
||||
|
||||
In the above example, there's a lot of data to be included in the message.
|
||||
However, if trace logs are disabled, that data will never be substituted,
|
||||
saving some processing time.
|
||||
|
||||
!!!tip
|
||||
This same logging system is used by netfox itself.
|
||||
|
||||
## Log levels
|
||||
|
||||
Each log message can belong to one of the following categories:
|
||||
|
||||
Error
|
||||
: Something goes irrecoverably wrong, or something that should never happen
|
||||
just happened
|
||||
|
||||
Warning
|
||||
: Something goes wrong, but can be handled
|
||||
|
||||
Info
|
||||
: Useful information on expected behaviour
|
||||
|
||||
Debug
|
||||
: Verbose messages, to help debug general code flow
|
||||
|
||||
Trace
|
||||
: Extremely verbose messages, to help follow the code flow to the smallest
|
||||
detail
|
||||
|
||||
Depending on your game, different logs may be needed. To accommodate this,
|
||||
*netfox* can be configured in the [Project Settings](#settings) to omit certain
|
||||
log messages.
|
||||
|
||||
Filtering based on log levels can also be configured from code. To set the
|
||||
global log level, set `NetfoxLogger.log_level`. To configure the log level per
|
||||
module, use the `NetfoxLogger.module_log_level` dictionary.
|
||||
|
||||
## Tags
|
||||
|
||||
Tags can be attached to the logging system. They provide pieces of information
|
||||
that appear in each log message, for every logger.
|
||||
|
||||
By default, netfox provides a few tags, to help with debugging. These are, in
|
||||
order:
|
||||
|
||||
Current tick
|
||||
: The current tick, as per `NetworkTime`
|
||||
|
||||
Peer ID
|
||||
: The currently active multiplayer peer's ID
|
||||
|
||||
Rollback status
|
||||
: Contains the current rollback stage, simulated tick, and resimulated tick
|
||||
interval.
|
||||
|
||||
The stage can be `B` for before loop, `P` for prepare tick, `S` for
|
||||
simulate tick, `R` for record tick, and `A` for after loop.
|
||||
|
||||
The current tick is in the form of `X|A>B`, meaning we're currently
|
||||
simulating tick X, in a loop going from tick A to tick B.
|
||||
|
||||
Defaults to `_` if currently not in rollback.
|
||||
|
||||
!!!note
|
||||
These default tags are subject to change between releases.
|
||||
|
||||
Custom tags can be attached by calling `NetfoxLogger.register_tag()`. In this
|
||||
sense, tags are callbacks that must return a single string, containing the tag
|
||||
data to be logged.
|
||||
|
||||
This method takes a second, `priority` parameter. This priority is used to sort
|
||||
them for logging - tags are printed from lowest priority to highest.
|
||||
|
||||
!!!warning
|
||||
Make sure to free your custom tags using `NetfoxLogger.free_tag()`. Not
|
||||
doing so might cause crashes. See [#433] for details.
|
||||
|
||||
## Settings
|
||||
|
||||

|
||||
|
||||
These settings control the *minimum* log level - e.g. if the log level is set
|
||||
to *info*, only messages at or above the info level will be logged, namely
|
||||
info, warning and error. If the setting is set to *all*, all messages are
|
||||
logged.
|
||||
|
||||
Log levels can be controlled globally and per addon. A message will be logged
|
||||
if it passes *both* logging level checks.
|
||||
|
||||
For example, if the *Log Level* setting is at *Warning* and the *Netfox Log
|
||||
Level* is at *Info*, only warning and error messages are logged for netfox.
|
||||
This happens because the *Log Level* is more restrictive than the *Netfox Log
|
||||
Level* setting.
|
||||
|
||||
Note that you don't need to install all netfox addons for the logging settings
|
||||
to work. If an addon is not installed, its log level setting is simply ignored.
|
||||
|
||||
|
||||
[#433]: https://github.com/foxssake/netfox/issues/433
|
||||
@@ -0,0 +1,98 @@
|
||||
# Netfox Sharp
|
||||
|
||||
!!!warning
|
||||
**[Netfox Sharp] is currently an experimental build and not ready for
|
||||
production. During this time, breaking changes may be introduced at any
|
||||
time.**
|
||||
|
||||
The [Netfox Sharp] addon is designed to bridge the gap between GDScript and C#
|
||||
by allowing core netfox features to be accessed in C# without having to worry
|
||||
about [Cross-Language Scripting] with GDScript directly.
|
||||
|
||||
### What Netfox Sharp Is
|
||||
|
||||
- A wrapper for netfox that uses the existing netfox codebase for its logic.
|
||||
- A way to more conveniently call netfox logic in a C# environment.
|
||||
- Partially compatible with existing codebases that use GDScript.
|
||||
|
||||
### What Netfox Sharp Isn't
|
||||
|
||||
- A standalone addon written entirely in C#.
|
||||
- A perfect 1:1 translation. Due to quirks of netfox, some code will differ,
|
||||
detailed below.
|
||||
- A wrapper for netfox.noray or netfox.extras. Support for either of those
|
||||
currently isn't planned, but may be considered based on interest.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Download the [Netfox Sharp] repo, and move the `netfox_sharp` and
|
||||
`netfox_sharp_internals` folders into the addons of a C#-enabled Godot
|
||||
project using the .NET version of Godot 4.x.
|
||||
- Install the netfox addon. See the Netfox Sharp repo for details on which
|
||||
version of netfox you need.
|
||||
- Build your project, then enable netfox and Netfox Sharp in your project
|
||||
settings.
|
||||
- Restart Godot, and you've successfully set up Netfox Sharp!
|
||||
|
||||
## Differences Between Netfox And Netfox Sharp
|
||||
|
||||
Existing documentation for netfox should be easily translatable to Netfox Sharp
|
||||
by following the below differences.
|
||||
|
||||
- Most changes follow Godot's rules for [Cross-Language Scripting], taking
|
||||
netfox as the base. In netfox, consider the following:
|
||||
|
||||
```gdscript
|
||||
# The following example is a snippet of netfox code
|
||||
func _ready():
|
||||
NetworkTime.before_tick_loop.connect(_gather)
|
||||
|
||||
func _gather():
|
||||
# Input gathering here
|
||||
pass
|
||||
|
||||
func _rollback_tick(delta, tick, is_fresh):
|
||||
# Rollback logic here
|
||||
pass
|
||||
```
|
||||
|
||||
Whereas in Netfox Sharp:
|
||||
|
||||
```cs
|
||||
// This is functionally identical Netfox Sharp code
|
||||
public override void _Ready()
|
||||
{
|
||||
// All netfox autoloads like NetworkTime are accessed through static members
|
||||
// in NetfoxSharp, to save on GetNode() calls and reduce clutter in the
|
||||
// project settings.
|
||||
|
||||
// All members like BeforeTickLoop are in PascalCase, similar to Godot's C#
|
||||
NetfoxSharp.NetworkTime.BeforeTickLoop += Gather;
|
||||
}
|
||||
|
||||
// As Gather is linked to a signal, it can be any naming convention.
|
||||
private void Gather()
|
||||
{
|
||||
// Input gathering here
|
||||
}
|
||||
|
||||
// Since _rollback_tick isn't connected to a signal and is instead handled by
|
||||
// netfox internally, netfox's naming convention must be followed.
|
||||
public void _rollback_tick(double delta, long tick, bool isFresh)
|
||||
{
|
||||
// Rollback logic here
|
||||
}
|
||||
```
|
||||
|
||||
- Nodes in the add mode menu have similar names to the GDScript version, but
|
||||
with 'Sharp' affixed, IE `RollbackSynchronizerSharp`. The GDScript versions
|
||||
of the nodes are also present in the add node menu. This is a limitation of
|
||||
how netfox interacts with Godot and cannot be removed.
|
||||
|
||||
# Other Notes
|
||||
- `RollbackSynchronizerSharp`, `StateSynchronizerSharp`, and
|
||||
`TickInterpolatorSharp` create their own respective GDScript nodes, which are
|
||||
instanced as internal children nodes and should not be accessed.
|
||||
|
||||
[Cross-Language Scripting]: https://docs.godotengine.org/en/stable/tutorials/scripting/cross_language_scripting.html
|
||||
[Netfox Sharp]: https://github.com/CyFurStudios/NetfoxSharp/
|
||||
@@ -0,0 +1,94 @@
|
||||
# NetworkCommandServer
|
||||
|
||||
Implements a simpler, lightweight alternative to RPCs. Provided as an autoload.
|
||||
|
||||
Commands consist of a single byte for ID, and the raw binary data. The ID lets
|
||||
the receiving peer decide what to execute, with the binary data serving as the
|
||||
input.
|
||||
|
||||
Being a simpler construct makes commands a good fit for regular, fundamental
|
||||
operations. For example, commands internally are used for time synchronization,
|
||||
or synchronizing state and input between peers.
|
||||
|
||||
Commands are, by default, transmitted over regular RPCs. To use less data,
|
||||
commands can also be transmitted as raw packets, using
|
||||
[SceneMultiplayer.send_bytes()]. This is an opt-in feature - if the game is
|
||||
already using [SceneMultiplayer.send_bytes()], it needs to be aware of
|
||||
commands, and must check each packet whether it's a command or one of its own
|
||||
packets. To check if a packet is a command, use `is_command_packet()`.
|
||||
|
||||
## Implementing custom commands
|
||||
|
||||
Custom commands can be registered with the *NetworkCommandServer*, using
|
||||
`register_command()`. This returns a *Command* object that provides a
|
||||
convenient interface.
|
||||
|
||||
During registration, a callback must be provided, that will be ran when the
|
||||
command is received.
|
||||
|
||||
Commands can be sent using its `send()` method.
|
||||
|
||||
```gdscript
|
||||
@onready var cmd_message := NetworkCommandServer.register_command(handle_message, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
||||
|
||||
func handle_message(sender: int, data: PackedByteArray) -> void:
|
||||
var message := data.get_string_from_utf8()
|
||||
print("#%d: %s" % [sender, message])
|
||||
|
||||
func _ready() -> void:
|
||||
cmd_message.send("Hello, world!".to_utf8_buffer())
|
||||
```
|
||||
|
||||
!!!tip
|
||||
It is recommended to setup commands once, at game start. When registering
|
||||
commands from autoloads, make sure they run *after* netfox's autoloads.
|
||||
|
||||
## Differences compared to RPCs
|
||||
|
||||
Commands are a fundamentally simpler constructs compared to RPCs.
|
||||
|
||||
### Maximum 256 commands
|
||||
|
||||
Commands are limited to 256 indices - make sure to not register more than that.
|
||||
Some commands are registered by netfox on startup as well.
|
||||
|
||||
This limitation also makes commands a poor fit for registering dynamically.
|
||||
Dynamic registrations often mean registering commands as certain nodes or
|
||||
objects are created. This, in turn, makes it difficult to place an upper bound
|
||||
on the number of commands needed, which can conflict with this limitation.
|
||||
|
||||
### Commands are not tied to any node
|
||||
|
||||
Commands do not refer to any specific node or object in their content. They
|
||||
only contain a command index. Even though the API encapsulates this into
|
||||
*Command* objects, it is completely feasible to have different nodes handle the
|
||||
same command on different peers ( if the game is built as different Godot
|
||||
projects ).
|
||||
|
||||
### Commands do not track authority
|
||||
|
||||
Any peer can send any command to any other peer. It is the receiving peer's
|
||||
responsibility to check whether the sender is allowed to send such a command or
|
||||
not.
|
||||
|
||||
### Commands do not have arguments
|
||||
|
||||
To stay lightweight and to give maximum control, commands contain raw bytes
|
||||
only, no arguments.
|
||||
|
||||
In general, this can be worked around by wrapping the arguments in an array and
|
||||
converting it using [var_to_bytes()] and [bytes_to_var()].
|
||||
|
||||
However, for cases where bandwidth matters, this allows users to encode data in
|
||||
a way that fits best.
|
||||
|
||||
## Settings
|
||||
|
||||
netfox ▸ General ▸ Use Raw Commands
|
||||
|
||||
: When enabled, netfox will transmit commands as raw packets, instead of RPCs.
|
||||
|
||||
|
||||
[SceneMultiplayer.send_bytes()]: https://docs.godotengine.org/en/stable/classes/class_scenemultiplayer.html#class-scenemultiplayer-method-send-bytes
|
||||
[var_to_bytes()]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-method-var-to-bytes
|
||||
[bytes_to_var()]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-method-bytes-to-var
|
||||