init
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
./scavgame.x86_64 &
|
||||
GODOT_PID=$!
|
||||
|
||||
wmctrl -r "scavgame" -b add,hidden
|
||||
|
||||
wait "$GODOT_PID"
|
||||
-1243
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bj1b5wm8lhd0v"
|
||||
uid="uid://b35ewh20gulwl"
|
||||
path="res://.godot/imported/scavgame.144x144.png-b48426c1c0d44c524725caf94371d8cf.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c3ffnf40pfswc"
|
||||
uid="uid://dxxnbj0y02str"
|
||||
path="res://.godot/imported/scavgame.180x180.png-98da9fc07cc8cd6f3596b8214cb44abf.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://4ele4xirpeuo"
|
||||
uid="uid://cwtujh1x71cxy"
|
||||
path="res://.godot/imported/scavgame.512x512.png-fc91a6f75062f7a09d36054b561598b5.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c08c15fn25u5a"
|
||||
path="res://.godot/imported/scavgame.apple-touch-icon.png-fae91e97aebbd193ef8488b9ce4380d1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Build/scavgame.apple-touch-icon.png"
|
||||
dest_files=["res://.godot/imported/scavgame.apple-touch-icon.png-fae91e97aebbd193ef8488b9ce4380d1.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
+1
-1
@@ -110,7 +110,7 @@ body {
|
||||
|
||||
<script src="scavgame.js"></script>
|
||||
<script>
|
||||
const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"emscriptenPoolSize":8,"ensureCrossOriginIsolationHeaders":true,"executable":"scavgame","experimentalVK":false,"fileSizes":{"scavgame.pck":200656,"scavgame.wasm":1601069},"focusCanvas":true,"gdextensionLibs":[],"godotPoolSize":4,"serviceWorker":"scavgame.service.worker.js"};
|
||||
const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"emscriptenPoolSize":8,"ensureCrossOriginIsolationHeaders":true,"executable":"scavgame","experimentalVK":false,"fileSizes":{"scavgame.pck":232728,"scavgame.wasm":1601069},"focusCanvas":true,"gdextensionLibs":[],"godotPoolSize":4,"serviceWorker":"scavgame.service.worker.js"};
|
||||
const GODOT_THREADS_ENABLED = true;
|
||||
const engine = new Engine(GODOT_CONFIG);
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dd0gt71ad26cw"
|
||||
path="res://.godot/imported/scavgame.icon.png-73f49704b23c124153b229d5b0cda394.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Build/scavgame.icon.png"
|
||||
dest_files=["res://.godot/imported/scavgame.icon.png-73f49704b23c124153b229d5b0cda394.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d2j704bew40yv"
|
||||
uid="uid://cgr0gin458n38"
|
||||
path="res://.godot/imported/scavgame.png-7a036069753eca4aeee3fbdd0ed99f6b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Incrementing CACHE_VERSION will kick off the install event and force
|
||||
// previously cached resources to be updated from the network.
|
||||
/** @type {string} */
|
||||
const CACHE_VERSION = '1788537868|6220518696';
|
||||
const CACHE_VERSION = '1788972329|18914709609';
|
||||
/** @type {string} */
|
||||
const CACHE_PREFIX = 'scavgame-sw-cache-';
|
||||
const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
|
||||
|
||||
-906
@@ -1,906 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from websockets.asyncio.server import serve
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Configuration
|
||||
# ============================================================
|
||||
|
||||
HTTP_HOST = "0.0.0.0"
|
||||
HTTP_PORT = 8080
|
||||
|
||||
WS_HOST = "0.0.0.0"
|
||||
WS_PORT = 8081
|
||||
|
||||
GAME_EXECUTABLE = Path(
|
||||
"/data/1/Projects/Scavenger-2027/Build/scavgame.x86_64"
|
||||
)
|
||||
|
||||
GAME_DIRECTORY = GAME_EXECUTABLE.parent
|
||||
WEB_DIRECTORY = GAME_DIRECTORY
|
||||
|
||||
MAX_SESSIONS = 4
|
||||
|
||||
DEBUG_LOGGING = True
|
||||
|
||||
sessions: dict[str, "GameSession"] = {}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Logging
|
||||
# ============================================================
|
||||
|
||||
def log(*args):
|
||||
print("[Server]", *args, flush=True)
|
||||
|
||||
|
||||
def debug(*args):
|
||||
if DEBUG_LOGGING:
|
||||
print("[Server][DEBUG]", *args, flush=True)
|
||||
|
||||
|
||||
def warning(*args):
|
||||
print("[Server][WARNING]", *args, flush=True)
|
||||
|
||||
|
||||
def error(*args):
|
||||
print("[Server][ERROR]", *args, flush=True)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Message helpers
|
||||
# ============================================================
|
||||
|
||||
def normalize_sdp_message(message: dict) -> dict | None:
|
||||
"""
|
||||
Normalizes these formats:
|
||||
|
||||
Browser:
|
||||
{
|
||||
"type": "offer",
|
||||
"offer": {
|
||||
"type": "offer",
|
||||
"sdp": "..."
|
||||
}
|
||||
}
|
||||
|
||||
Godot:
|
||||
{
|
||||
"type": "offer",
|
||||
"sdp": "..."
|
||||
}
|
||||
|
||||
The same applies to answers.
|
||||
"""
|
||||
|
||||
message_type = message.get("type")
|
||||
|
||||
if message_type not in {"offer", "answer"}:
|
||||
return None
|
||||
|
||||
nested_key = message_type
|
||||
|
||||
nested = message.get(nested_key)
|
||||
|
||||
if isinstance(nested, dict):
|
||||
sdp = nested.get("sdp", "")
|
||||
|
||||
if not sdp:
|
||||
return None
|
||||
|
||||
return {
|
||||
"type": message_type,
|
||||
"sdp": sdp
|
||||
}
|
||||
|
||||
sdp = message.get("sdp", "")
|
||||
|
||||
if not sdp:
|
||||
return None
|
||||
|
||||
return {
|
||||
"type": message_type,
|
||||
"sdp": sdp
|
||||
}
|
||||
|
||||
|
||||
def normalize_ice_message(message: dict) -> dict | None:
|
||||
"""
|
||||
Normalizes both:
|
||||
|
||||
{ "type": "ice", "candidate": {...} }
|
||||
|
||||
and:
|
||||
|
||||
{ "type": "candidate", "candidate": {...} }
|
||||
"""
|
||||
|
||||
candidate = message.get("candidate")
|
||||
|
||||
if not isinstance(candidate, dict):
|
||||
return None
|
||||
|
||||
if not candidate.get("candidate"):
|
||||
return None
|
||||
|
||||
return {
|
||||
"type": "ice",
|
||||
"candidate": candidate
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Game session
|
||||
# ============================================================
|
||||
|
||||
class GameSession:
|
||||
def __init__(
|
||||
self,
|
||||
browser,
|
||||
requested_session_id: str | None = None
|
||||
):
|
||||
self.id = (
|
||||
requested_session_id
|
||||
if requested_session_id
|
||||
else uuid.uuid4().hex
|
||||
)
|
||||
|
||||
self.browser = browser
|
||||
self.godot = None
|
||||
self.process = None
|
||||
|
||||
self.browser_connected = True
|
||||
self.godot_connected = False
|
||||
self.stopping = False
|
||||
|
||||
self.send_lock = asyncio.Lock()
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Game process
|
||||
# --------------------------------------------------------
|
||||
|
||||
async def start(self):
|
||||
if not GAME_EXECUTABLE.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Game executable not found: {GAME_EXECUTABLE}"
|
||||
)
|
||||
|
||||
if not os.access(GAME_EXECUTABLE, os.X_OK):
|
||||
raise PermissionError(
|
||||
f"Game is not executable: {GAME_EXECUTABLE}"
|
||||
)
|
||||
|
||||
env = os.environ.copy()
|
||||
|
||||
env["SCAV_SESSION_ID"] = self.id
|
||||
|
||||
# Keep Forward+ rendering.
|
||||
env["DISPLAY"] = ":1"
|
||||
|
||||
# Start minimized and avoid taking focus.
|
||||
env["SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS"] = "1"
|
||||
|
||||
command = [
|
||||
str(GAME_EXECUTABLE),
|
||||
|
||||
"--session-id",
|
||||
self.id,
|
||||
|
||||
"--debug-logging",
|
||||
|
||||
"--resolution",
|
||||
"1x1",
|
||||
]
|
||||
|
||||
debug(
|
||||
f"[{self.id}] Starting game command:",
|
||||
command
|
||||
)
|
||||
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
*command,
|
||||
|
||||
cwd=str(GAME_DIRECTORY),
|
||||
env=env,
|
||||
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
)
|
||||
|
||||
log(
|
||||
f"[{self.id}] Started game "
|
||||
f"PID={self.process.pid}"
|
||||
)
|
||||
|
||||
asyncio.create_task(
|
||||
self.read_output()
|
||||
)
|
||||
|
||||
async def read_output(self):
|
||||
process = self.process
|
||||
|
||||
if process is None:
|
||||
return
|
||||
|
||||
if process.stdout is None:
|
||||
return
|
||||
|
||||
try:
|
||||
async for line in process.stdout:
|
||||
output = line.decode(
|
||||
errors="replace"
|
||||
).rstrip()
|
||||
|
||||
print(
|
||||
f"[{self.id}] {output}",
|
||||
flush=True
|
||||
)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
except Exception as exc:
|
||||
error(
|
||||
f"[{self.id}] Output reader error:",
|
||||
exc
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
if self.stopping:
|
||||
return
|
||||
|
||||
self.stopping = True
|
||||
|
||||
process = self.process
|
||||
self.process = None
|
||||
|
||||
if process is None:
|
||||
return
|
||||
|
||||
if process.returncode is not None:
|
||||
return
|
||||
|
||||
log(
|
||||
f"[{self.id}] Stopping game "
|
||||
f"PID={process.pid}"
|
||||
)
|
||||
|
||||
try:
|
||||
process.terminate()
|
||||
|
||||
await asyncio.wait_for(
|
||||
process.wait(),
|
||||
timeout=3
|
||||
)
|
||||
|
||||
log(
|
||||
f"[{self.id}] Game exited "
|
||||
f"code={process.returncode}"
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
warning(
|
||||
f"[{self.id}] Game did not exit; "
|
||||
f"killing PID={process.pid}"
|
||||
)
|
||||
|
||||
try:
|
||||
process.kill()
|
||||
await process.wait()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
# --------------------------------------------------------
|
||||
# WebSocket sending
|
||||
# --------------------------------------------------------
|
||||
|
||||
async def send_json(
|
||||
self,
|
||||
websocket,
|
||||
message: dict
|
||||
) -> bool:
|
||||
if websocket is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
payload = json.dumps(message)
|
||||
|
||||
async with self.send_lock:
|
||||
await websocket.send(payload)
|
||||
|
||||
debug(
|
||||
f"[{self.id}] Sent:",
|
||||
message
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except ConnectionClosed:
|
||||
return False
|
||||
|
||||
except Exception as exc:
|
||||
error(
|
||||
f"[{self.id}] Send error:",
|
||||
exc
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
async def send_to_godot(
|
||||
self,
|
||||
message: dict
|
||||
) -> bool:
|
||||
if self.godot is None:
|
||||
warning(
|
||||
f"[{self.id}] Cannot send to Godot; "
|
||||
f"Godot is not connected"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
success = await self.send_json(
|
||||
self.godot,
|
||||
message
|
||||
)
|
||||
|
||||
if not success:
|
||||
self.godot = None
|
||||
self.godot_connected = False
|
||||
|
||||
return success
|
||||
|
||||
async def send_to_browser(
|
||||
self,
|
||||
message: dict
|
||||
) -> bool:
|
||||
if self.browser is None:
|
||||
warning(
|
||||
f"[{self.id}] Cannot send to browser; "
|
||||
f"browser is not connected"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
success = await self.send_json(
|
||||
self.browser,
|
||||
message
|
||||
)
|
||||
|
||||
if not success:
|
||||
self.browser = None
|
||||
self.browser_connected = False
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# ============================================================
|
||||
# HTTP server
|
||||
# ============================================================
|
||||
|
||||
class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(
|
||||
*args,
|
||||
directory=str(WEB_DIRECTORY),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def log_message(self, format_string, *args):
|
||||
print(
|
||||
"[HTTP] " + format_string % args,
|
||||
flush=True
|
||||
)
|
||||
|
||||
|
||||
def start_http_server():
|
||||
server = ThreadingHTTPServer(
|
||||
(HTTP_HOST, HTTP_PORT),
|
||||
HTTPHandler
|
||||
)
|
||||
|
||||
log(
|
||||
f"HTTP server listening on "
|
||||
f"http://{HTTP_HOST}:{HTTP_PORT}"
|
||||
)
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
server.server_close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Browser connection
|
||||
# ============================================================
|
||||
|
||||
async def handle_browser(
|
||||
websocket,
|
||||
requested_session_id: str | None = None
|
||||
):
|
||||
if len(sessions) >= MAX_SESSIONS:
|
||||
await websocket.send(json.dumps({
|
||||
"type": "error",
|
||||
"message": "Server is full"
|
||||
}))
|
||||
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
session = GameSession(
|
||||
websocket,
|
||||
requested_session_id
|
||||
)
|
||||
|
||||
if session.id in sessions:
|
||||
await websocket.send(json.dumps({
|
||||
"type": "error",
|
||||
"message": "Session ID is already in use"
|
||||
}))
|
||||
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
sessions[session.id] = session
|
||||
|
||||
log(
|
||||
f"[{session.id}] Browser connected"
|
||||
)
|
||||
|
||||
try:
|
||||
await session.send_to_browser({
|
||||
"type": "session_created",
|
||||
"session_id": session.id
|
||||
})
|
||||
|
||||
await session.start()
|
||||
|
||||
# Wait for Godot to connect.
|
||||
for _ in range(100):
|
||||
if session.godot is not None:
|
||||
break
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
if session.godot is None:
|
||||
warning(
|
||||
f"[{session.id}] Godot did not connect"
|
||||
)
|
||||
|
||||
await session.send_to_browser({
|
||||
"type": "error",
|
||||
"message": "Godot did not connect"
|
||||
})
|
||||
|
||||
return
|
||||
|
||||
await session.send_to_godot({
|
||||
"type": "browser_connected"
|
||||
})
|
||||
|
||||
await session.send_to_browser({
|
||||
"type": "godot_connected"
|
||||
})
|
||||
|
||||
log(
|
||||
f"[{session.id}] Browser and Godot paired"
|
||||
)
|
||||
|
||||
async for raw in websocket:
|
||||
try:
|
||||
message = json.loads(raw)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
warning(
|
||||
f"[{session.id}] Invalid browser JSON:",
|
||||
raw
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
if not isinstance(message, dict):
|
||||
warning(
|
||||
f"[{session.id}] Browser message "
|
||||
f"is not an object"
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
debug(
|
||||
f"[{session.id}] Browser message:",
|
||||
message
|
||||
)
|
||||
|
||||
message_type = message.get("type")
|
||||
|
||||
# ------------------------------------------------
|
||||
# WebRTC offer
|
||||
# ------------------------------------------------
|
||||
|
||||
if message_type == "offer":
|
||||
normalized = normalize_sdp_message(
|
||||
message
|
||||
)
|
||||
|
||||
if normalized is None:
|
||||
warning(
|
||||
f"[{session.id}] Invalid browser offer"
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
await session.send_to_godot(
|
||||
normalized
|
||||
)
|
||||
|
||||
# ------------------------------------------------
|
||||
# WebRTC ICE
|
||||
# ------------------------------------------------
|
||||
|
||||
elif message_type in {
|
||||
"ice",
|
||||
"candidate"
|
||||
}:
|
||||
normalized = normalize_ice_message(
|
||||
message
|
||||
)
|
||||
|
||||
if normalized is None:
|
||||
warning(
|
||||
f"[{session.id}] Invalid browser ICE"
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
await session.send_to_godot(
|
||||
normalized
|
||||
)
|
||||
|
||||
else:
|
||||
debug(
|
||||
f"[{session.id}] Ignoring browser message:",
|
||||
message_type
|
||||
)
|
||||
|
||||
except ConnectionClosed:
|
||||
log(
|
||||
f"[{session.id}] Browser WebSocket closed"
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
error(
|
||||
f"[{session.id}] Browser error:",
|
||||
exc
|
||||
)
|
||||
|
||||
finally:
|
||||
log(
|
||||
f"[{session.id}] Browser disconnected"
|
||||
)
|
||||
|
||||
session.browser_connected = False
|
||||
|
||||
if session.godot is not None:
|
||||
await session.send_to_godot({
|
||||
"type": "browser_disconnected"
|
||||
})
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
try:
|
||||
await session.godot.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
session.godot = None
|
||||
session.godot_connected = False
|
||||
|
||||
await session.stop()
|
||||
|
||||
sessions.pop(
|
||||
session.id,
|
||||
None
|
||||
)
|
||||
|
||||
log(
|
||||
f"[{session.id}] Session removed"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Godot connection
|
||||
# ============================================================
|
||||
|
||||
async def handle_godot(
|
||||
websocket,
|
||||
session_id: str
|
||||
):
|
||||
session = sessions.get(session_id)
|
||||
|
||||
if session is None:
|
||||
warning(
|
||||
f"[{session_id}] Godot attempted to connect "
|
||||
f"to unknown session"
|
||||
)
|
||||
|
||||
await websocket.send(json.dumps({
|
||||
"type": "error",
|
||||
"message": "Unknown session"
|
||||
}))
|
||||
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
if session.godot is not None:
|
||||
warning(
|
||||
f"[{session.id}] Replacing existing Godot connection"
|
||||
)
|
||||
|
||||
try:
|
||||
await session.godot.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
session.godot = websocket
|
||||
session.godot_connected = True
|
||||
|
||||
log(
|
||||
f"[{session.id}] Godot connected"
|
||||
)
|
||||
|
||||
try:
|
||||
await session.send_to_godot({
|
||||
"type": "registered",
|
||||
"session_id": session.id
|
||||
})
|
||||
|
||||
if session.browser_connected:
|
||||
await session.send_to_godot({
|
||||
"type": "browser_connected"
|
||||
})
|
||||
|
||||
await session.send_to_browser({
|
||||
"type": "godot_connected"
|
||||
})
|
||||
|
||||
async for raw in websocket:
|
||||
try:
|
||||
message = json.loads(raw)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
warning(
|
||||
f"[{session.id}] Invalid Godot JSON:",
|
||||
raw
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
if not isinstance(message, dict):
|
||||
warning(
|
||||
f"[{session.id}] Godot message "
|
||||
f"is not an object"
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
debug(
|
||||
f"[{session.id}] Godot message:",
|
||||
message
|
||||
)
|
||||
|
||||
message_type = message.get("type")
|
||||
|
||||
# ------------------------------------------------
|
||||
# WebRTC answer
|
||||
# ------------------------------------------------
|
||||
|
||||
if message_type == "answer":
|
||||
normalized = normalize_sdp_message(
|
||||
message
|
||||
)
|
||||
|
||||
if normalized is None:
|
||||
warning(
|
||||
f"[{session.id}] Invalid Godot answer"
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
await session.send_to_browser({
|
||||
"type": "answer",
|
||||
"answer": {
|
||||
"type": "answer",
|
||||
"sdp": normalized["sdp"]
|
||||
}
|
||||
})
|
||||
|
||||
# ------------------------------------------------
|
||||
# WebRTC ICE
|
||||
# ------------------------------------------------
|
||||
|
||||
elif message_type in {
|
||||
"ice",
|
||||
"candidate"
|
||||
}:
|
||||
normalized = normalize_ice_message(
|
||||
message
|
||||
)
|
||||
|
||||
if normalized is None:
|
||||
warning(
|
||||
f"[{session.id}] Invalid Godot ICE"
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
await session.send_to_browser(
|
||||
normalized
|
||||
)
|
||||
|
||||
else:
|
||||
debug(
|
||||
f"[{session.id}] Ignoring Godot message:",
|
||||
message_type
|
||||
)
|
||||
|
||||
except ConnectionClosed:
|
||||
log(
|
||||
f"[{session.id}] Godot WebSocket closed"
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
error(
|
||||
f"[{session.id}] Godot error:",
|
||||
exc
|
||||
)
|
||||
|
||||
finally:
|
||||
log(
|
||||
f"[{session.id}] Godot disconnected"
|
||||
)
|
||||
|
||||
if session.godot is websocket:
|
||||
session.godot = None
|
||||
session.godot_connected = False
|
||||
|
||||
if session.browser is not None:
|
||||
await session.send_to_browser({
|
||||
"type": "godot_disconnected"
|
||||
})
|
||||
|
||||
|
||||
# ============================================================
|
||||
# WebSocket routing
|
||||
# ============================================================
|
||||
|
||||
async def websocket_handler(websocket):
|
||||
try:
|
||||
raw = await websocket.recv()
|
||||
|
||||
message = json.loads(raw)
|
||||
|
||||
if not isinstance(message, dict):
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
role = message.get("role")
|
||||
|
||||
debug(
|
||||
"Initial WebSocket registration:",
|
||||
message
|
||||
)
|
||||
|
||||
if role == "browser":
|
||||
requested_session_id = message.get(
|
||||
"session_id"
|
||||
)
|
||||
|
||||
await handle_browser(
|
||||
websocket,
|
||||
requested_session_id
|
||||
)
|
||||
|
||||
elif role == "godot":
|
||||
session_id = message.get(
|
||||
"session_id"
|
||||
)
|
||||
|
||||
if not session_id:
|
||||
await websocket.send(json.dumps({
|
||||
"type": "error",
|
||||
"message": "Missing session_id"
|
||||
}))
|
||||
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
await handle_godot(
|
||||
websocket,
|
||||
session_id
|
||||
)
|
||||
|
||||
else:
|
||||
await websocket.send(json.dumps({
|
||||
"type": "error",
|
||||
"message": "Invalid role"
|
||||
}))
|
||||
|
||||
await websocket.close()
|
||||
|
||||
except ConnectionClosed:
|
||||
pass
|
||||
|
||||
except json.JSONDecodeError:
|
||||
warning(
|
||||
"Received invalid initial WebSocket JSON"
|
||||
)
|
||||
|
||||
try:
|
||||
await websocket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as exc:
|
||||
error(
|
||||
"WebSocket handler error:",
|
||||
exc
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# WebSocket server
|
||||
# ============================================================
|
||||
|
||||
async def websocket_server():
|
||||
log(
|
||||
f"WebSocket server listening on "
|
||||
f"ws://{WS_HOST}:{WS_PORT}"
|
||||
)
|
||||
|
||||
async with serve(
|
||||
websocket_handler,
|
||||
WS_HOST,
|
||||
WS_PORT
|
||||
):
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
|
||||
async def main():
|
||||
http_task = asyncio.to_thread(
|
||||
start_http_server
|
||||
)
|
||||
|
||||
websocket_task = asyncio.create_task(
|
||||
websocket_server()
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
http_task,
|
||||
websocket_task
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(
|
||||
"\nServer stopped",
|
||||
flush=True
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
4.6.2.stable
|
||||
+128
-64
@@ -1,97 +1,161 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
const SPEED := 5.0
|
||||
const JUMP_VELOCITY := 4.5
|
||||
const MOUSE_SENSITIVITY := 0.003
|
||||
# Acceleration and deceleration are m/s².
|
||||
# max_speed is m/s.
|
||||
@export var accel := 6.7
|
||||
@export var deccel := 8.5
|
||||
@export var max_speed := 6.2
|
||||
|
||||
const ACCEL := 20.0
|
||||
const DECEL := 15.0
|
||||
@export var has_suit := true
|
||||
@export var has_hvms2 := true
|
||||
|
||||
@export var gravity := 9.8
|
||||
@export var jump_velocity := 4.5
|
||||
|
||||
# Mouse sensitivity
|
||||
@export var mouse_sensitivity := 0.003
|
||||
|
||||
# Maximum camera pitch in degrees
|
||||
@export var max_pitch := 89.0
|
||||
|
||||
var movement_vec := Vector3.ZERO
|
||||
var input_vec := Vector4.ZERO
|
||||
var jump := false
|
||||
var air_jumps := 0
|
||||
|
||||
func _ready() -> void:
|
||||
if not has_suit:
|
||||
$yaw/pitch/Camera/Hud.visible = false
|
||||
|
||||
if has_hvms2:
|
||||
max_speed = 30.0
|
||||
accel = 10
|
||||
deccel = 20
|
||||
jump_velocity = 10
|
||||
|
||||
# Capture the mouse when the game starts
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Mouse-look
|
||||
if event is InputEventMouseMotion:
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
# Horizontal rotation: rotate the player
|
||||
$yaw.rotate_y(-event.relative.x * mouse_sensitivity)
|
||||
|
||||
# Vertical rotation: rotate the camera
|
||||
$yaw/pitch.rotate_x(-event.relative.y * mouse_sensitivity)
|
||||
|
||||
# Clamp pitch so the camera cannot flip upside down
|
||||
$yaw/pitch.rotation.x = clamp(
|
||||
$yaw/pitch.rotation.x,
|
||||
deg_to_rad(-max_pitch),
|
||||
deg_to_rad(max_pitch)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
# Mouse click captures the mouse again
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
return
|
||||
|
||||
# Keyboard input
|
||||
if event is InputEventKey:
|
||||
if event.keycode == KEY_ESCAPE and event.pressed:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
return
|
||||
var pressed: bool = event.pressed
|
||||
|
||||
if event.keycode == KEY_SPACE and event.pressed and is_on_floor():
|
||||
velocity.y = JUMP_VELOCITY
|
||||
match event.keycode:
|
||||
KEY_W:
|
||||
input_vec.w = 1.0 if pressed else 0.0
|
||||
|
||||
if event.keycode == KEY_E and event.pressed and not event.echo:
|
||||
var node = $yaw/pitch/Facing.get_collider()
|
||||
KEY_S:
|
||||
input_vec.x = 1.0 if pressed else 0.0
|
||||
|
||||
if node and node.has_method("interact"):
|
||||
node.interact(self)
|
||||
KEY_A:
|
||||
input_vec.y = 1.0 if pressed else 0.0
|
||||
|
||||
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
$yaw.rotate_y(-event.relative.x * MOUSE_SENSITIVITY)
|
||||
KEY_D:
|
||||
input_vec.z = 1.0 if pressed else 0.0
|
||||
|
||||
$yaw/pitch.rotate_x(-event.relative.y * MOUSE_SENSITIVITY)
|
||||
KEY_SPACE:
|
||||
if pressed and is_on_floor():
|
||||
jump=true
|
||||
|
||||
$yaw/pitch.rotation.x = clamp(
|
||||
$yaw/pitch.rotation.x,
|
||||
deg_to_rad(-89.0),
|
||||
deg_to_rad(89.0)
|
||||
)
|
||||
KEY_ESCAPE:
|
||||
if pressed:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
KEY_SHIFT:
|
||||
if has_hvms2:
|
||||
velocity.y = max(velocity.y, -3) if pressed else velocity.y
|
||||
gravity = 3 if pressed else 9.8
|
||||
if is_on_floor():
|
||||
velocity=$yaw/pitch/Camera.basis*Vector3(0,0,50)
|
||||
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# Gravity
|
||||
if not is_on_floor():
|
||||
velocity += get_gravity() * delta
|
||||
# W/S/A/D input
|
||||
var input_direction := Vector2(
|
||||
input_vec.z - input_vec.y,
|
||||
input_vec.w - input_vec.x
|
||||
)
|
||||
|
||||
# Read keyboard state directly
|
||||
var input_x := 0.0
|
||||
var input_z := 0.0
|
||||
|
||||
if Input.is_key_pressed(KEY_A):
|
||||
input_x -= 1.0
|
||||
|
||||
if Input.is_key_pressed(KEY_D):
|
||||
input_x += 1.0
|
||||
|
||||
if Input.is_key_pressed(KEY_W):
|
||||
input_z -= 1.0
|
||||
|
||||
if Input.is_key_pressed(KEY_S):
|
||||
input_z += 1.0
|
||||
|
||||
# Create movement vector
|
||||
var input_direction := Vector3(input_x, 0.0, input_z)
|
||||
|
||||
# Prevent diagonal movement from being faster
|
||||
if input_direction.length_squared() > 0.0:
|
||||
if input_direction.length() > 1.0:
|
||||
input_direction = input_direction.normalized()
|
||||
|
||||
# Rotate movement relative to yaw
|
||||
var direction : Vector3 = $yaw.global_transform.basis * input_direction
|
||||
direction.y = 0.0
|
||||
# Camera yaw only
|
||||
var yaw_basis: Basis = $yaw.global_transform.basis
|
||||
|
||||
if direction.length_squared() > 0.0:
|
||||
direction = direction.normalized()
|
||||
var forward := -yaw_basis.z
|
||||
var right := yaw_basis.x
|
||||
|
||||
# Target velocity
|
||||
var target := direction * SPEED
|
||||
forward.y = 0.0
|
||||
right.y = 0.0
|
||||
|
||||
var acceleration := ACCEL
|
||||
if direction.length_squared() == 0.0:
|
||||
acceleration = DECEL
|
||||
forward = forward.normalized()
|
||||
right = right.normalized()
|
||||
|
||||
# Smooth movement toward target
|
||||
velocity.x = move_toward(
|
||||
var target_direction := (
|
||||
right * input_direction.x
|
||||
+ forward * input_direction.y
|
||||
)
|
||||
|
||||
# Target horizontal velocity in m/s
|
||||
var target_velocity := target_direction * max_speed
|
||||
|
||||
# Current horizontal velocity in m/s
|
||||
var current_velocity := Vector3(
|
||||
velocity.x,
|
||||
target.x,
|
||||
0.0,
|
||||
velocity.z
|
||||
)
|
||||
|
||||
# Acceleration/deceleration in m/s²
|
||||
var acceleration := accel if target_direction != Vector3.ZERO else deccel
|
||||
|
||||
current_velocity = current_velocity.move_toward(
|
||||
target_velocity,
|
||||
acceleration * delta
|
||||
)
|
||||
|
||||
velocity.z = move_toward(
|
||||
velocity.z,
|
||||
target.z,
|
||||
acceleration * delta
|
||||
)
|
||||
velocity.x = current_velocity.x
|
||||
velocity.z = current_velocity.z
|
||||
|
||||
# Gravity
|
||||
if not is_on_floor():
|
||||
velocity.y -= gravity * delta
|
||||
else:
|
||||
if jump:
|
||||
velocity.y=jump_velocity
|
||||
jump=false
|
||||
else:
|
||||
velocity.y=0.0
|
||||
|
||||
if global_position.y<=-1000:
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
move_and_slide()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
extends Node2D
|
||||
|
||||
func _ready() -> void:
|
||||
$Container.size=get_viewport_rect().size
|
||||
|
||||
func set_tooltip(text: String) -> void:
|
||||
$Tooltip.text=text
|
||||
$Container/Tooltip.text=text
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,72 @@
|
||||
extends MeshInstance3D
|
||||
|
||||
@export var max_lights := 256
|
||||
@export var camera: Camera3D
|
||||
|
||||
var lights: Array[Light3D] = []
|
||||
|
||||
func _ready() -> void:
|
||||
find_lights(get_tree().current_scene)
|
||||
|
||||
func find_lights(node: Node) -> void:
|
||||
if node is Light3D:
|
||||
lights.append(node)
|
||||
|
||||
for child in node.get_children():
|
||||
find_lights(child)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var mat := material_override as ShaderMaterial
|
||||
if mat == null:
|
||||
return
|
||||
|
||||
if camera == null:
|
||||
camera = get_viewport().get_camera_3d()
|
||||
|
||||
if camera == null:
|
||||
return
|
||||
|
||||
mat.set_shader_parameter(
|
||||
"inv_projection",
|
||||
camera.get_camera_projection().inverse()
|
||||
)
|
||||
|
||||
mat.set_shader_parameter(
|
||||
"inv_view",
|
||||
camera.global_transform
|
||||
)
|
||||
|
||||
if mat == null:
|
||||
return
|
||||
|
||||
var positions := PackedVector3Array()
|
||||
var colors := PackedColorArray()
|
||||
var energies := PackedFloat32Array()
|
||||
var ranges := PackedFloat32Array()
|
||||
|
||||
for light in lights:
|
||||
if not is_instance_valid(light):
|
||||
continue
|
||||
|
||||
if not light.visible:
|
||||
continue
|
||||
|
||||
positions.append(light.global_position)
|
||||
colors.append(light.light_color)
|
||||
energies.append(light.light_energy)
|
||||
|
||||
if light is OmniLight3D:
|
||||
ranges.append(light.omni_range)
|
||||
elif light is SpotLight3D:
|
||||
ranges.append(light.spot_range)
|
||||
else:
|
||||
ranges.append(100.0)
|
||||
|
||||
if positions.size() >= max_lights:
|
||||
break
|
||||
|
||||
mat.set_shader_parameter("light_positions", positions)
|
||||
mat.set_shader_parameter("light_colors", colors)
|
||||
mat.set_shader_parameter("light_energies", energies)
|
||||
mat.set_shader_parameter("light_ranges", ranges)
|
||||
mat.set_shader_parameter("light_count", positions.size())
|
||||
@@ -0,0 +1 @@
|
||||
uid://blvu4cfl61262
|
||||
@@ -0,0 +1,135 @@
|
||||
shader_type spatial;
|
||||
|
||||
render_mode unshaded, fog_disabled, depth_draw_never, cull_disabled, blend_mix;
|
||||
|
||||
const int MAX_LIGHTS = 256;
|
||||
|
||||
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_linear;
|
||||
uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;
|
||||
|
||||
// Camera matrices — set from GDScript.
|
||||
uniform mat4 inv_projection;
|
||||
uniform mat4 inv_view;
|
||||
|
||||
// Fog
|
||||
uniform vec3 fog_color : source_color = vec3(0.55, 0.65, 0.75);
|
||||
uniform float fog_density : hint_range(0.0, 10.0) = 0.5;
|
||||
uniform float fog_height_falloff : hint_range(0.0, 10.0) = 0.15;
|
||||
|
||||
// Light scattering
|
||||
uniform int light_count = 0;
|
||||
uniform vec3 light_positions[MAX_LIGHTS];
|
||||
uniform vec4 light_colors[MAX_LIGHTS];
|
||||
uniform float light_energies[MAX_LIGHTS];
|
||||
uniform float light_ranges[MAX_LIGHTS];
|
||||
|
||||
uniform float scattering : hint_range(0.0, 2.0) = 0.5;
|
||||
uniform float scattering_power : hint_range(0.1, 32.0) = 8.0;
|
||||
|
||||
uniform int steps : hint_range(8, 64) = 32;
|
||||
|
||||
vec3 reconstruct_view_position(vec2 uv, float depth) {
|
||||
vec3 ndc = vec3(uv * 2.0 - 1.0, depth);
|
||||
|
||||
vec4 view_pos = inv_projection * vec4(ndc, 1.0);
|
||||
view_pos.xyz /= view_pos.w;
|
||||
|
||||
return view_pos.xyz;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 screen_uv = SCREEN_UV;
|
||||
|
||||
vec4 scene = texture(screen_texture, screen_uv);
|
||||
float depth = texture(depth_texture, screen_uv).r;
|
||||
|
||||
vec3 ray_end_view =
|
||||
reconstruct_view_position(screen_uv, depth);
|
||||
|
||||
vec3 camera_position =
|
||||
(inv_view * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
|
||||
|
||||
vec3 ray_end =
|
||||
(inv_view * vec4(ray_end_view, 1.0)).xyz;
|
||||
|
||||
vec3 ray_vector = ray_end - camera_position;
|
||||
float ray_length = length(ray_vector);
|
||||
|
||||
vec3 ray_dir = normalize(ray_vector);
|
||||
|
||||
float step_length = ray_length / float(steps);
|
||||
|
||||
vec3 accumulated = vec3(0.0);
|
||||
float transmittance = 1.0;
|
||||
|
||||
for (int i = 0; i < steps; i++) {
|
||||
float t = (float(i) + 0.5) / float(steps);
|
||||
|
||||
vec3 world_position =
|
||||
camera_position +
|
||||
ray_dir * (ray_length * t);
|
||||
|
||||
float height_density =
|
||||
exp(-world_position.y * fog_height_falloff);
|
||||
|
||||
float density =
|
||||
fog_density * height_density;
|
||||
|
||||
vec3 light_scattering = vec3(0.0);
|
||||
|
||||
for (int l = 0; l < MAX_LIGHTS; l++) {
|
||||
if (l >= light_count)
|
||||
break;
|
||||
|
||||
vec3 to_light =
|
||||
light_positions[l] - world_position;
|
||||
|
||||
float light_distance = length(to_light);
|
||||
|
||||
if (light_distance <= 0.001)
|
||||
continue;
|
||||
|
||||
vec3 light_dir =
|
||||
to_light / light_distance;
|
||||
|
||||
float attenuation =
|
||||
1.0 - smoothstep(
|
||||
0.0,
|
||||
light_ranges[l],
|
||||
light_distance
|
||||
);
|
||||
|
||||
attenuation /=
|
||||
1.0 + light_distance * light_distance * 0.01;
|
||||
|
||||
float phase =
|
||||
pow(
|
||||
max(dot(-ray_dir, light_dir), 0.0),
|
||||
scattering_power
|
||||
);
|
||||
|
||||
light_scattering +=
|
||||
light_colors[l].rgb *
|
||||
light_energies[l] *
|
||||
attenuation *
|
||||
phase *
|
||||
scattering;
|
||||
}
|
||||
|
||||
float absorption =
|
||||
exp(-density * step_length * 10.0);
|
||||
|
||||
accumulated +=
|
||||
(fog_color + light_scattering) *
|
||||
(1.0 - absorption) *
|
||||
transmittance;
|
||||
|
||||
transmittance *= absorption;
|
||||
}
|
||||
|
||||
ALBEDO =
|
||||
scene.rgb * transmittance +
|
||||
accumulated;
|
||||
|
||||
ALPHA = 1.0;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_add, cull_disabled, unshaded;
|
||||
|
||||
uniform sampler2D dust : source_color;
|
||||
uniform float dust_speed = 0.03;
|
||||
uniform vec4 color : source_color;
|
||||
uniform float intensity = 2.75;
|
||||
uniform float softness = 3.0;
|
||||
|
||||
// Godot 4 explicit texture hints
|
||||
uniform sampler2D depth_texture : hint_depth_texture, filter_linear_mipmap;
|
||||
uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap;
|
||||
|
||||
uniform float proximity_fade_distance = 0.5;
|
||||
uniform float distance_fade_min = 1.0;
|
||||
uniform float distance_fade_max = 2.0;
|
||||
|
||||
varying vec3 obj_vertex;
|
||||
|
||||
void vertex() {
|
||||
obj_vertex = VERTEX;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
// Dust texture sampling using screen coordinates
|
||||
vec2 dust_uv = vec2(SCREEN_UV.x + dust_speed * TIME, SCREEN_UV.y);
|
||||
vec4 dust_overlay = texture(dust, dust_uv);
|
||||
|
||||
float attenuation = clamp(1.0 + obj_vertex.y / intensity, 0.0, 1.0);
|
||||
float soft_edge = clamp(pow(dot(NORMAL, normalize(VERTEX)), softness), 0.0, 1.0);
|
||||
|
||||
ALBEDO = dust_overlay.rgb * color.rgb * attenuation * soft_edge;
|
||||
|
||||
// Proximity Fade calculation in Godot 4
|
||||
float depth_raw = texture(depth_texture, SCREEN_UV).r;
|
||||
vec4 ndc = vec4(SCREEN_UV * 2.0 - 1.0, depth_raw, 1.0);
|
||||
vec4 view_pos = INV_PROJECTION_MATRIX * ndc;
|
||||
view_pos.xyz /= view_pos.w;
|
||||
|
||||
float depth_diff = -VERTEX.z - (-view_pos.z);
|
||||
float proximity_alpha = clamp(depth_diff / proximity_fade_distance, 0.0, 1.0);
|
||||
|
||||
// Distance Fade calculation
|
||||
float dist_alpha = clamp(smoothstep(distance_fade_min, distance_fade_max, -VERTEX.z), 0.0, 1.0);
|
||||
|
||||
ALPHA = proximity_alpha * dist_alpha;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,805 @@
|
||||
extends Node
|
||||
|
||||
const CONSOLE_THEME : String = &"console/theme"
|
||||
const CONSOLE_SCALE : String = &"console/scale"
|
||||
const CONSOLE_HEIGHT : String = &"console/height"
|
||||
const CONSOLE_COLOR_WARNING : String = &"console/color_warning"
|
||||
const CONSOLE_COLOR_ERROR : String = &"console/color_error"
|
||||
const CONSOLE_COLOR_INFO : String = &"console/color_info"
|
||||
const CONSOLE_COLOR_LITERAL : String = &"console/color_literal"
|
||||
const CONSOLE_TABSTOP : String = &"console/tabstop"
|
||||
|
||||
const color_dictionary : Dictionary[String, Color] = {
|
||||
CONSOLE_COLOR_ERROR: Color.LIGHT_CORAL,
|
||||
CONSOLE_COLOR_INFO: Color.LIGHT_BLUE,
|
||||
CONSOLE_COLOR_LITERAL: Color.PALE_GREEN,
|
||||
CONSOLE_COLOR_WARNING: Color.LIGHT_GOLDENROD
|
||||
}
|
||||
|
||||
var enabled : bool = true
|
||||
var enable_on_release_build : bool = false : set = set_enable_on_release_build
|
||||
var pause_enabled : bool = false
|
||||
var font_size : int : set = _set_font_size
|
||||
|
||||
## What visual scale should the console be
|
||||
var console_scale : float : set = set_console_scale
|
||||
|
||||
## Is the console in full screen or part screen mode
|
||||
var console_full_screen : bool = false
|
||||
|
||||
var _conosle_tween_time : float = 5
|
||||
|
||||
signal console_opened
|
||||
signal console_closed
|
||||
signal console_unknown_command
|
||||
signal console_cvar_changed(cvar_name : String, value : Variant)
|
||||
|
||||
|
||||
class ConsoleCommand:
|
||||
var function : Callable
|
||||
var arguments : PackedStringArray
|
||||
var required : int
|
||||
var description : String
|
||||
var hidden : bool
|
||||
func _init(in_function : Callable, in_arguments : PackedStringArray, in_required : int = 0, in_description : String = ""):
|
||||
function = in_function
|
||||
arguments = in_arguments
|
||||
required = in_required
|
||||
description = in_description
|
||||
|
||||
|
||||
class ConsoleCvar:
|
||||
var name : String
|
||||
var description : String
|
||||
var type : int
|
||||
var save : bool
|
||||
var value : Variant
|
||||
var object : Object
|
||||
var property : String
|
||||
|
||||
func _init(in_name : String, in_type : int, in_description : String = "", in_save : bool = false):
|
||||
name = in_name
|
||||
type = in_type
|
||||
description = in_description
|
||||
save = in_save
|
||||
|
||||
func is_reference() -> bool:
|
||||
return object != null
|
||||
|
||||
func is_alive() -> bool:
|
||||
return object == null or is_instance_valid(object)
|
||||
|
||||
func get_value() -> Variant:
|
||||
if object != null:
|
||||
return object.get_indexed(property)
|
||||
return value
|
||||
|
||||
func set_value(new_value : Variant) -> void:
|
||||
if object != null:
|
||||
object.set_indexed(property, new_value)
|
||||
else:
|
||||
value = new_value
|
||||
|
||||
var theme : Theme
|
||||
var canvas_layer : CanvasLayer = CanvasLayer.new()
|
||||
var v_box_container : VBoxContainer = VBoxContainer.new()
|
||||
|
||||
# If you want to customize the way the console looks, you can direcly modify
|
||||
# the properties of the rich text and line edit here:
|
||||
var rich_label : RichTextLabel = RichTextLabel.new()
|
||||
var panel : Panel = Panel.new()
|
||||
var line_edit : LineEdit = LineEdit.new()
|
||||
|
||||
var console_commands : Dictionary[String, ConsoleCommand] = {}
|
||||
var console_cvars : Dictionary[String, ConsoleCvar] = {}
|
||||
# Pending values are applied when cvars are registered (if loaded from config)
|
||||
var _pending_cvar_values : Dictionary[String, Variant] = {}
|
||||
var command_parameters : Dictionary[String, PackedStringArray] = {}
|
||||
var console_history : Array[String] = []
|
||||
var console_history_index : int = 0
|
||||
var was_paused_already : bool = false
|
||||
|
||||
var tab_string : String = " "
|
||||
var text_block_cache : Array[String]
|
||||
|
||||
## Usage: Console.add_command("command_name", <function to call>, <number of arguments or array of argument names>, <required number of arguments>, "Help description")
|
||||
func add_command(command_name : String, function : Callable, arguments = [], required: int = 0, description : String = "") -> void:
|
||||
if (arguments is int):
|
||||
# Legacy call using an argument number
|
||||
var param_array : PackedStringArray
|
||||
for i in range(arguments):
|
||||
param_array.append("arg_" + str(i + 1))
|
||||
console_commands[command_name] = ConsoleCommand.new(function, param_array, required, description)
|
||||
elif (arguments is Array):
|
||||
# New array argument system
|
||||
var str_args : PackedStringArray
|
||||
for argument in arguments:
|
||||
str_args.append(str(argument))
|
||||
console_commands[command_name] = ConsoleCommand.new(function, str_args, required, description)
|
||||
|
||||
|
||||
## Adds a secret command that will not show up in the help or auto-complete.
|
||||
func add_hidden_command(command_name : String, function : Callable, arguments = [], required : int = 0) -> void:
|
||||
add_command(command_name, function, arguments, required)
|
||||
console_commands[command_name].hidden = true
|
||||
|
||||
|
||||
## Removes a command from the console. This should be called on a script's _exit_tree()
|
||||
## if you have console commands for things that are unloaded before the project closes.
|
||||
func remove_command(command_name : String) -> void:
|
||||
console_commands.erase(command_name)
|
||||
command_parameters.erase(command_name)
|
||||
|
||||
|
||||
## Registers an auto-managed cvar that stores its value internally.
|
||||
## Type the name to print the value, or "name value" to set it. The value is coerced to the
|
||||
## type of default_value. If save is true, the value is persisted to user://console_cvars.txt.
|
||||
## Usage: Console.add_cvar("cl_fov", 90.0, "Field of view.", true)
|
||||
func add_cvar(cvar_name : String, default_value : Variant, description : String = "", save : bool = false) -> void:
|
||||
var cvar := ConsoleCvar.new(cvar_name, typeof(default_value), description, save)
|
||||
cvar.value = default_value
|
||||
console_cvars[cvar_name] = cvar
|
||||
_apply_pending_cvar_value(cvar)
|
||||
|
||||
|
||||
## Registers a cvar that reads from and writes to a property on another object.
|
||||
## The cvar type is inferred from the property's current value. property may be a nested path
|
||||
## (ex: "position:x").
|
||||
## Usage: Console.add_cvar_reference("cl_fov", camera, "fov", "Camera field of view.")
|
||||
func add_cvar_reference(cvar_name : String, object : Object, property : String, description : String = "", save : bool = false) -> void:
|
||||
if not is_instance_valid(object):
|
||||
print_error("Cannot register cvar \"%s\": invalid object." % cvar_name)
|
||||
return
|
||||
var cvar := ConsoleCvar.new(cvar_name, typeof(object.get_indexed(property)), description, save)
|
||||
cvar.object = object
|
||||
cvar.property = property
|
||||
console_cvars[cvar_name] = cvar
|
||||
_apply_pending_cvar_value(cvar)
|
||||
|
||||
|
||||
## Removes a cvar. Call this on _exit_tree() for reference cvars whose object is freed early.
|
||||
func remove_cvar(cvar_name : String) -> void:
|
||||
console_cvars.erase(cvar_name)
|
||||
|
||||
|
||||
## Returns the current value of a cvar, or null if it doesn't exist / its object was freed.
|
||||
func get_cvar(cvar_name : String) -> Variant:
|
||||
if console_cvars.has(cvar_name):
|
||||
var cvar : ConsoleCvar = console_cvars[cvar_name]
|
||||
if cvar.is_alive():
|
||||
return cvar.get_value()
|
||||
return null
|
||||
|
||||
|
||||
## Sets the value of a cvar programmatically (no string coercion) and emits console_cvar_changed.
|
||||
func set_cvar(cvar_name : String, value : Variant) -> void:
|
||||
if console_cvars.has(cvar_name):
|
||||
var cvar : ConsoleCvar = console_cvars[cvar_name]
|
||||
if cvar.is_alive():
|
||||
cvar.set_value(value)
|
||||
console_cvar_changed.emit(cvar_name, value)
|
||||
|
||||
|
||||
func _apply_pending_cvar_value(cvar : ConsoleCvar) -> void:
|
||||
if cvar.save and _pending_cvar_values.has(cvar.name):
|
||||
var saved_value : Variant = _pending_cvar_values[cvar.name]
|
||||
if typeof(saved_value) != cvar.type:
|
||||
saved_value = type_convert(saved_value, cvar.type)
|
||||
cvar.set_value(saved_value)
|
||||
_pending_cvar_values.erase(cvar.name)
|
||||
|
||||
|
||||
## Coerces a string into the given Variant.Type. Returns [success : bool, value : Variant].
|
||||
func _coerce_string_to_type(string_value : String, type : int) -> Array:
|
||||
match type:
|
||||
TYPE_BOOL:
|
||||
var lower := string_value.strip_edges().to_lower()
|
||||
if lower in ["1", "true", "yes", "on"]:
|
||||
return [true, true]
|
||||
if lower in ["0", "false", "no", "off"]:
|
||||
return [true, false]
|
||||
return [false, null]
|
||||
TYPE_INT:
|
||||
var int_string := string_value.strip_edges()
|
||||
if int_string.is_valid_int():
|
||||
return [true, int_string.to_int()]
|
||||
return [false, null]
|
||||
TYPE_FLOAT:
|
||||
var float_string := string_value.strip_edges()
|
||||
if float_string.is_valid_float():
|
||||
return [true, float_string.to_float()]
|
||||
return [false, null]
|
||||
TYPE_STRING:
|
||||
return [true, string_value]
|
||||
TYPE_STRING_NAME:
|
||||
return [true, StringName(string_value)]
|
||||
_:
|
||||
# Complex types (Vector2, etc.) require GDScript literal syntax, ex: "Vector2(1, 2)".
|
||||
var parsed : Variant = str_to_var(string_value)
|
||||
if parsed != null and typeof(parsed) == type:
|
||||
return [true, parsed]
|
||||
return [false, null]
|
||||
|
||||
|
||||
func _print_cvar_value(cvar : ConsoleCvar) -> void:
|
||||
print_line("%s = [system_color color=CONSOLE_COLOR_LITERAL]%s[/system_color]" % [cvar.name, str(cvar.get_value())])
|
||||
|
||||
|
||||
func _handle_cvar(cvar : ConsoleCvar, arguments : PackedStringArray) -> void:
|
||||
if not cvar.is_alive():
|
||||
print_error("Variable \"%s\" references a freed object." % cvar.name)
|
||||
return
|
||||
|
||||
if arguments.is_empty():
|
||||
_print_cvar_value(cvar)
|
||||
if cvar.description:
|
||||
print_line("%s%s" % [tab_string, cvar.description])
|
||||
return
|
||||
|
||||
var raw_value := " ".join(arguments)
|
||||
var result := _coerce_string_to_type(raw_value, cvar.type)
|
||||
if not result[0]:
|
||||
print_error("Invalid value \"%s\" for variable \"%s\" (expected %s)." % [raw_value, cvar.name, type_string(cvar.type)])
|
||||
return
|
||||
|
||||
cvar.set_value(result[1])
|
||||
console_cvar_changed.emit(cvar.name, result[1])
|
||||
_print_cvar_value(cvar)
|
||||
|
||||
|
||||
## Useful if you have a list of possible parameters (ex: level names).
|
||||
func add_command_autocomplete_list(command_name : String, param_list : PackedStringArray):
|
||||
command_parameters[command_name] = param_list
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
var console_history_file := FileAccess.open("user://console_history.txt", FileAccess.READ)
|
||||
if (console_history_file):
|
||||
while (!console_history_file.eof_reached()):
|
||||
var line := console_history_file.get_line()
|
||||
if (line.length()):
|
||||
add_input_history(line)
|
||||
|
||||
# Load persisted cvar values. They're stashed until the matching cvar is registered.
|
||||
var console_cvars_file := FileAccess.open("user://console_cvars.txt", FileAccess.READ)
|
||||
if (console_cvars_file):
|
||||
while (!console_cvars_file.eof_reached()):
|
||||
var line := console_cvars_file.get_line()
|
||||
if (line.length()):
|
||||
var split := line.split(" ", true, 1) # "name var_to_str(value)"
|
||||
if (split.size() == 2):
|
||||
_pending_cvar_values[split[0]] = str_to_var(split[1])
|
||||
|
||||
if ProjectSettings.has_setting(CONSOLE_THEME):
|
||||
theme = load(ProjectSettings.get_setting(CONSOLE_THEME))
|
||||
if theme:
|
||||
v_box_container.theme = theme
|
||||
|
||||
if ProjectSettings.has_setting(CONSOLE_TABSTOP):
|
||||
tab_string = ""
|
||||
for i in range(ProjectSettings.get_setting(CONSOLE_TABSTOP)):
|
||||
tab_string += " "
|
||||
|
||||
canvas_layer.layer = ProjectSettings.get_setting(&"console/canvas_layer", 3)
|
||||
add_child(canvas_layer)
|
||||
console_scale = _get_console_scale_setting()
|
||||
v_box_container.offset_bottom = 0
|
||||
v_box_container.offset_left = 0
|
||||
v_box_container.offset_right = 0
|
||||
v_box_container.offset_top = 0
|
||||
canvas_layer.add_child(v_box_container)
|
||||
panel.size_flags_vertical = Control.SIZE_FILL ^ Control.SIZE_EXPAND
|
||||
v_box_container.add_child(panel)
|
||||
rich_label.selection_enabled = true
|
||||
rich_label.context_menu_enabled = true
|
||||
rich_label.bbcode_enabled = true
|
||||
rich_label.scroll_following = true
|
||||
rich_label.anchor_right = 1.0
|
||||
rich_label.anchor_bottom = 1.0
|
||||
rich_label.install_effect(preload("res://addons/console/system_color.gd").new())
|
||||
panel.add_child(rich_label)
|
||||
rich_label.append_text("Development console.\n")
|
||||
line_edit.anchor_right = 1.0
|
||||
line_edit.placeholder_text = "Enter \"help\" for instructions"
|
||||
if font_size > 0:
|
||||
line_edit.add_theme_font_size_override("font_size", font_size)
|
||||
v_box_container.add_child(line_edit)
|
||||
line_edit.text_submitted.connect(_on_text_entered)
|
||||
line_edit.text_changed.connect(_on_line_edit_text_changed)
|
||||
v_box_container.visible = false
|
||||
process_mode = PROCESS_MODE_ALWAYS
|
||||
|
||||
|
||||
## Get the scale of the console from the settings -- if this is not in the system settings return a default value
|
||||
func _get_console_scale_setting() -> float:
|
||||
if ProjectSettings.has_setting(CONSOLE_SCALE):
|
||||
return ProjectSettings.get_setting(CONSOLE_SCALE)
|
||||
|
||||
return 1.0
|
||||
|
||||
|
||||
## Set the console scale
|
||||
func set_console_scale(scale : float):
|
||||
console_scale = scale
|
||||
v_box_container.scale = Vector2(console_scale, console_scale)
|
||||
v_box_container.anchor_right = _get_console_width()
|
||||
v_box_container.anchor_bottom = _get_console_height()
|
||||
|
||||
|
||||
## Get the height of the console - this is related to the scaling of the console
|
||||
func _get_console_height() -> float:
|
||||
if console_full_screen:
|
||||
return 1.0 / console_scale
|
||||
|
||||
if ProjectSettings.has_setting(CONSOLE_HEIGHT):
|
||||
return ProjectSettings.get_setting(CONSOLE_HEIGHT) / console_scale
|
||||
|
||||
return 0.5 / console_scale
|
||||
|
||||
func _get_console_width() -> float:
|
||||
return 1.0 / console_scale
|
||||
|
||||
|
||||
func _set_font_size(value: int) -> void:
|
||||
font_size = value
|
||||
if value > 0:
|
||||
line_edit.add_theme_font_size_override("font_size", font_size)
|
||||
rich_label.add_theme_font_size_override("normal_font_size", font_size)
|
||||
rich_label.add_theme_font_size_override("bold_font_size", font_size)
|
||||
rich_label.add_theme_font_size_override("bold_italics_font_size", font_size)
|
||||
rich_label.add_theme_font_size_override("italics_font_size", font_size)
|
||||
rich_label.add_theme_font_size_override("mono_font_size", font_size)
|
||||
else:
|
||||
line_edit.remove_theme_font_size_override("font_size")
|
||||
rich_label.remove_theme_font_size_override("normal_font_size")
|
||||
rich_label.remove_theme_font_size_override("bold_font_size")
|
||||
rich_label.remove_theme_font_size_override("bold_italics_font_size")
|
||||
rich_label.remove_theme_font_size_override("italics_font_size")
|
||||
rich_label.remove_theme_font_size_override("mono_font_size")
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var console_history_file := FileAccess.open("user://console_history.txt", FileAccess.WRITE)
|
||||
if (console_history_file):
|
||||
var write_index := 0
|
||||
var start_write_index := console_history.size() - 100 # Max lines to write
|
||||
for line in console_history:
|
||||
if (write_index >= start_write_index):
|
||||
console_history_file.store_line(line)
|
||||
write_index += 1
|
||||
|
||||
var console_cvars_file := FileAccess.open("user://console_cvars.txt", FileAccess.WRITE)
|
||||
if (console_cvars_file):
|
||||
for cvar_name in console_cvars:
|
||||
var cvar : ConsoleCvar = console_cvars[cvar_name]
|
||||
if (cvar.save and cvar.is_alive()):
|
||||
console_cvars_file.store_line("%s %s" % [cvar_name, var_to_str(cvar.get_value())])
|
||||
for pending_name in _pending_cvar_values:
|
||||
if (!console_cvars.has(pending_name)):
|
||||
console_cvars_file.store_line("%s %s" % [pending_name, var_to_str(_pending_cvar_values[pending_name])])
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
v_box_container.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # For that retro look.
|
||||
add_command("quit", quit, 0, 0, "Quits the game.")
|
||||
add_command("exit", quit, 0, 0, "Quits the game.")
|
||||
add_command("clear", clear, 0, 0, "Clears the text on the console.")
|
||||
add_command("delete_history", delete_history, 0, 0, "Deletes the history of previously entered commands.")
|
||||
add_command("help", help, 0, 0, "Displays instructions on how to use the console.")
|
||||
add_command("commands_list", commands_list, 0, 0, "Lists all commands and their descriptions.")
|
||||
add_command("commands", commands, 0, 0, "Lists commands with no descriptions.")
|
||||
add_command("cvars", cvars, 0, 0, "Lists all console variables and their values.")
|
||||
add_command("calc", calculate, ["mathematical expression to evaluate"], 0, "Evaluates the math passed in for quick arithmetic.")
|
||||
add_command("echo", print_line, ["string"], 1, "Prints given string to the console.")
|
||||
add_command("echo_warning", print_warning, ["string"], 1, "Prints given string as warning to the console.")
|
||||
add_command("echo_info", print_info, ["string"], 1, "Prints given string as info to the console.")
|
||||
add_command("echo_error", print_error, ["string"], 1, "Prints given string as an error to the console.")
|
||||
add_command("pause", pause, 0, 0, "Pauses node processing.")
|
||||
add_command("unpause", unpause, 0, 0, "Unpauses node processing.")
|
||||
add_command("exec", exec, 1, 1, "Execute a script.")
|
||||
|
||||
|
||||
func _input(event : InputEvent) -> void:
|
||||
if (event is InputEventKey):
|
||||
if (event.physical_keycode == KEY_QUOTELEFT): # ~ key.
|
||||
if (event.physical_keycode == KEY_QUOTELEFT and event.is_command_or_control_pressed()): # Toggles console size or opens big console.
|
||||
if (event.pressed):
|
||||
if (v_box_container.visible):
|
||||
toggle_size()
|
||||
else:
|
||||
toggle_console()
|
||||
toggle_size()
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
else:
|
||||
if (event.pressed):
|
||||
toggle_console()
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
elif (event.get_physical_keycode_with_modifiers() == KEY_ESCAPE && v_box_container.visible): # Disable console on ESC
|
||||
if (event.pressed):
|
||||
toggle_console()
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
if (v_box_container.visible and event.pressed):
|
||||
if (event.get_physical_keycode_with_modifiers() == KEY_UP):
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
if (console_history_index > 0):
|
||||
console_history_index -= 1
|
||||
if (console_history_index >= 0):
|
||||
line_edit.text = console_history[console_history_index]
|
||||
line_edit.caret_column = line_edit.text.length()
|
||||
reset_autocomplete()
|
||||
if (event.get_physical_keycode_with_modifiers() == KEY_DOWN):
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
if (console_history_index < console_history.size()):
|
||||
console_history_index += 1
|
||||
if (console_history_index < console_history.size()):
|
||||
line_edit.text = console_history[console_history_index]
|
||||
line_edit.caret_column = line_edit.text.length()
|
||||
reset_autocomplete()
|
||||
else:
|
||||
line_edit.text = ""
|
||||
reset_autocomplete()
|
||||
if (event.get_physical_keycode_with_modifiers() == KEY_PAGEUP):
|
||||
var scroll := rich_label.get_v_scroll_bar()
|
||||
var tween := create_tween()
|
||||
tween.tween_property(scroll, "value", scroll.value - (scroll.page - scroll.page * 0.1), 0.1)
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
if (event.get_physical_keycode_with_modifiers() == KEY_PAGEDOWN):
|
||||
var scroll := rich_label.get_v_scroll_bar()
|
||||
var tween := create_tween()
|
||||
tween.tween_property(scroll, "value", scroll.value + (scroll.page - scroll.page * 0.1), 0.1)
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
if (event.get_physical_keycode_with_modifiers() == KEY_TAB):
|
||||
autocomplete()
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
|
||||
elif event is InputEventMouseButton:
|
||||
if (v_box_container.visible):
|
||||
if (event.is_command_or_control_pressed()):
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP: # Increase font size with ctrl+mouse wheel up
|
||||
font_size = min(128, font_size + 2) # Limit to max of 128
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: # Decrease font size with ctrl+mouse wheel down
|
||||
font_size = max(8, font_size - 2) # Limit to minimum of 8
|
||||
get_tree().get_root().set_input_as_handled()
|
||||
|
||||
|
||||
var suggestions := []
|
||||
var current_suggest := 0
|
||||
var suggesting := false
|
||||
|
||||
func autocomplete() -> void:
|
||||
if (suggesting):
|
||||
for i in range(suggestions.size()):
|
||||
if (current_suggest == i):
|
||||
line_edit.text = str(suggestions[i])
|
||||
line_edit.caret_column = line_edit.text.length()
|
||||
if (current_suggest == suggestions.size() - 1):
|
||||
current_suggest = 0
|
||||
else:
|
||||
current_suggest += 1
|
||||
return
|
||||
else:
|
||||
suggesting = true
|
||||
|
||||
if (" " in line_edit.text): # We're searching for a parameter to autocomplete
|
||||
var split_text := parse_line_input(line_edit.text)
|
||||
if (split_text.size() > 1):
|
||||
var command := split_text[0]
|
||||
var param_input := split_text[1]
|
||||
if (command_parameters.has(command)):
|
||||
for param in command_parameters[command]:
|
||||
if (param_input in param) or (param_input.length() == 0):
|
||||
suggestions.append(str(command, " ", param))
|
||||
else:
|
||||
var sorted_commands := []
|
||||
for command in console_commands:
|
||||
if (!console_commands[command].hidden):
|
||||
sorted_commands.append(str(command))
|
||||
for cvar_name in console_cvars:
|
||||
sorted_commands.append(str(cvar_name))
|
||||
sorted_commands.sort()
|
||||
sorted_commands.reverse()
|
||||
|
||||
var prev_index := 0
|
||||
for command in sorted_commands:
|
||||
if (!line_edit.text || command.contains(line_edit.text)):
|
||||
var index : int = command.find(line_edit.text)
|
||||
if (index <= prev_index):
|
||||
suggestions.push_front(command)
|
||||
else:
|
||||
suggestions.push_back(command)
|
||||
prev_index = index
|
||||
autocomplete()
|
||||
|
||||
|
||||
func reset_autocomplete() -> void:
|
||||
suggestions.clear()
|
||||
current_suggest = 0
|
||||
suggesting = false
|
||||
|
||||
|
||||
func toggle_size() -> void:
|
||||
console_full_screen = !console_full_screen
|
||||
v_box_container.anchor_bottom = _get_console_height()
|
||||
|
||||
|
||||
func disable():
|
||||
enabled = false
|
||||
toggle_console() # Ensure hidden if opened
|
||||
|
||||
|
||||
func enable():
|
||||
enabled = true
|
||||
|
||||
|
||||
func toggle_console() -> void:
|
||||
var was_visible := v_box_container.visible
|
||||
if (enabled):
|
||||
v_box_container.visible = !v_box_container.visible
|
||||
else:
|
||||
v_box_container.visible = false
|
||||
|
||||
if (v_box_container.visible):
|
||||
was_paused_already = get_tree().paused
|
||||
get_tree().paused = was_paused_already || pause_enabled
|
||||
line_edit.grab_focus()
|
||||
if was_visible != v_box_container.visible:
|
||||
console_opened.emit()
|
||||
else:
|
||||
scroll_to_bottom()
|
||||
reset_autocomplete()
|
||||
if (pause_enabled && !was_paused_already):
|
||||
get_tree().paused = false
|
||||
if was_visible != v_box_container.visible:
|
||||
console_closed.emit()
|
||||
|
||||
|
||||
func is_visible():
|
||||
return v_box_container.visible
|
||||
|
||||
|
||||
func scroll_to_bottom() -> void:
|
||||
var scroll: ScrollBar = rich_label.get_v_scroll_bar()
|
||||
scroll.value = scroll.max_value - scroll.page
|
||||
|
||||
|
||||
func print_error(text : Variant, print_godot := false) -> void:
|
||||
var _color : Color = Color.LIGHT_CORAL
|
||||
if not text is String:
|
||||
text = str(text)
|
||||
|
||||
print_line("%s[system_color color=CONSOLE_COLOR_ERROR]ERROR:[/system_color] %s" % [tab_string, text], print_godot)
|
||||
|
||||
|
||||
func print_info(text : Variant, print_godot := false) -> void:
|
||||
var _color : Color = Color.LIGHT_BLUE
|
||||
if not text is String:
|
||||
text = str(text)
|
||||
|
||||
print_line("%s[system_color color=CONSOLE_COLOR_INFO]INFO:[/system_color] %s" % [tab_string, text], print_godot)
|
||||
|
||||
|
||||
func print_warning(text : Variant, print_godot := false) -> void:
|
||||
var _color : Color = Color.LIGHT_GOLDENROD
|
||||
if not text is String:
|
||||
text = str(text)
|
||||
|
||||
print_line("%s[system_color color=CONSOLE_COLOR_WARNING]WARNING:[/system_color] %s" % [tab_string, text], print_godot)
|
||||
|
||||
func print_line(text : Variant, print_godot := false) -> void:
|
||||
if not text is String:
|
||||
text = str(text)
|
||||
if (!rich_label): # Tried to print something before the console was loaded.
|
||||
call_deferred("print_line", text)
|
||||
else:
|
||||
rich_label.append_text(text)
|
||||
rich_label.append_text("\n")
|
||||
if (print_godot):
|
||||
print_rich(text.dedent())
|
||||
|
||||
|
||||
func parse_line_input(text : String) -> PackedStringArray:
|
||||
var out_array : PackedStringArray
|
||||
var first_char := true
|
||||
var in_quotes := false
|
||||
var escaped := false
|
||||
var token : String
|
||||
for c in text:
|
||||
if (c == '\\'):
|
||||
escaped = true
|
||||
continue
|
||||
elif (escaped):
|
||||
if (c == 'n'):
|
||||
c = '\n'
|
||||
elif (c == 't'):
|
||||
c = '\t'
|
||||
elif (c == 'r'):
|
||||
c = '\r'
|
||||
elif (c == 'a'):
|
||||
c = '\a'
|
||||
elif (c == 'b'):
|
||||
c = '\b'
|
||||
elif (c == 'f'):
|
||||
c = '\f'
|
||||
escaped = false
|
||||
elif (c == '\"'):
|
||||
in_quotes = !in_quotes
|
||||
continue
|
||||
elif (c == ' ' || c == '\t'):
|
||||
if (!in_quotes):
|
||||
out_array.push_back(token)
|
||||
token = ""
|
||||
continue
|
||||
token += c
|
||||
out_array.push_back(token)
|
||||
return out_array
|
||||
|
||||
|
||||
func _on_text_entered(new_text : String) -> void:
|
||||
scroll_to_bottom()
|
||||
reset_autocomplete()
|
||||
line_edit.clear()
|
||||
if (line_edit.has_method(&"edit")):
|
||||
line_edit.call_deferred(&"edit")
|
||||
|
||||
if not new_text.strip_edges().is_empty():
|
||||
add_input_history(new_text)
|
||||
print_line("[i]> " + new_text + "[/i]")
|
||||
var text_split := parse_line_input(new_text)
|
||||
var text_command := text_split[0]
|
||||
|
||||
if console_commands.has(text_command):
|
||||
var arguments := text_split.slice(1)
|
||||
var console_command : ConsoleCommand = console_commands[text_command]
|
||||
|
||||
# calc is a especial command that needs special treatment
|
||||
if (text_command.match("calc")):
|
||||
var expression := ""
|
||||
for word in arguments:
|
||||
expression += word
|
||||
console_command.function.callv([expression])
|
||||
return
|
||||
|
||||
if (arguments.size() < console_command.required):
|
||||
print_error("Too few arguments! Required < %d >" % console_command.required)
|
||||
return
|
||||
elif (arguments.size() > console_command.arguments.size()):
|
||||
arguments.resize(console_command.arguments.size())
|
||||
|
||||
# Functions fail to call if passed the incorrect number of arguments, so fill out with blank strings.
|
||||
while (arguments.size() < console_command.arguments.size()):
|
||||
arguments.append("")
|
||||
|
||||
console_command.function.callv(arguments)
|
||||
elif console_cvars.has(text_command):
|
||||
_handle_cvar(console_cvars[text_command], text_split.slice(1))
|
||||
else:
|
||||
console_unknown_command.emit(text_command)
|
||||
print_error("Unknown command or variable.")
|
||||
|
||||
|
||||
func _on_line_edit_text_changed(new_text : String) -> void:
|
||||
reset_autocomplete()
|
||||
|
||||
|
||||
func quit() -> void:
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
rich_label.clear()
|
||||
|
||||
|
||||
func delete_history() -> void:
|
||||
console_history.clear()
|
||||
console_history_index = 0
|
||||
DirAccess.remove_absolute("user://console_history.txt")
|
||||
|
||||
|
||||
func help() -> void:
|
||||
rich_label.append_text(" Built in commands:
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]calc[/system_color]: Calculates a given expresion
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]clear[/system_color]: Clears the registry view
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]commands[/system_color]: Shows a reduced list of all the currently registered commands
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]commands_list[/system_color]: Shows a detailed list of all the currently registered commands
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]cvars[/system_color]: Lists all console variables and their values
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]delete_history[/system_color]: Deletes the commands history
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]echo[/system_color]: Prints a given string to the console
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]echo_error[/system_color]: Prints a given string as an error to the console
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]echo_info[/system_color]: Prints a given string as info to the console
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]echo_warning[/system_color]: Prints a given string as warning to the console
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]pause[/system_color]: Pauses node processing
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]unpause[/system_color]: Unpauses node processing
|
||||
[system_color color=CONSOLE_COLOR_LITERAL]quit[/system_color]: Quits the game
|
||||
Controls:
|
||||
[system_color color=CONSOLE_COLOR_INFO]Up[/system_color] and [system_color color=CONSOLE_COLOR_INFO]Down[/system_color] arrow keys to navigate commands history
|
||||
[system_color color=CONSOLE_COLOR_INFO]PageUp[/system_color] and [system_color color=CONSOLE_COLOR_INFO]PageDown[/system_color] to scroll registry
|
||||
[[system_color color=CONSOLE_COLOR_INFO]Ctrl[/system_color] + [system_color color=CONSOLE_COLOR_INFO]~[/system_color]] to change console size between half screen and full screen
|
||||
[[system_color color=CONSOLE_COLOR_INFO]Ctrl[/system_color] + [system_color color=CONSOLE_COLOR_INFO]Mouse Wheel[/system_color]] up/down to change console font size
|
||||
[system_color color=CONSOLE_COLOR_INFO]~[/system_color] or [system_color color=CONSOLE_COLOR_INFO]Esc[/system_color] key to close the console
|
||||
[system_color color=CONSOLE_COLOR_INFO]Tab[/system_color] key to autocomplete, [system_color system_color=CONSOLE_COLOR_INFO]Tab[/system_color] again to cycle between matching suggestions\n\n")
|
||||
|
||||
|
||||
func calculate(command : String) -> void:
|
||||
var expression := Expression.new()
|
||||
var error = expression.parse(command)
|
||||
if error:
|
||||
print_error("%s" % expression.get_error_text())
|
||||
return
|
||||
var result = expression.execute()
|
||||
if not expression.has_execute_failed():
|
||||
print_line(str(result))
|
||||
else:
|
||||
print_error("%s" % expression.get_error_text())
|
||||
|
||||
|
||||
func commands() -> void:
|
||||
var commands := []
|
||||
for command in console_commands:
|
||||
if (!console_commands[command].hidden):
|
||||
commands.append(str(command))
|
||||
commands.sort()
|
||||
rich_label.append_text(" ")
|
||||
rich_label.append_text(str(commands) + "\n\n")
|
||||
|
||||
|
||||
func commands_list() -> void:
|
||||
var commands := []
|
||||
for command in console_commands:
|
||||
if (!console_commands[command].hidden):
|
||||
commands.append(str(command))
|
||||
commands.sort()
|
||||
|
||||
for command in commands:
|
||||
var arguments_string := ""
|
||||
var description : String = console_commands[command].description
|
||||
for i in range(console_commands[command].arguments.size()):
|
||||
if i < console_commands[command].required:
|
||||
arguments_string += " [system_color color=CONSOLE_COLOR_ERROR]<" + console_commands[command].arguments[i] + ">[/system_color]"
|
||||
else:
|
||||
arguments_string += " [system_color color=CONSOLE_COLOR_INFO]<" + console_commands[command].arguments[i] + ">[/system_color]"
|
||||
rich_label.append_text(" [system_color color=CONSOLE_COLOR_LITERAL]%s[/system_color]%s: %s\n" % [command, arguments_string, description])
|
||||
rich_label.append_text("\n")
|
||||
|
||||
|
||||
func cvars() -> void:
|
||||
var names := console_cvars.keys()
|
||||
names.sort()
|
||||
for cvar_name in names:
|
||||
var cvar : ConsoleCvar = console_cvars[cvar_name]
|
||||
var value_string := "<invalid>"
|
||||
if (cvar.is_alive()):
|
||||
value_string = str(cvar.get_value())
|
||||
rich_label.append_text(" [system_color color=CONSOLE_COLOR_LITERAL]%s[/system_color] = [system_color color=CONSOLE_COLOR_INFO]%s[/system_color] %s\n" % [cvar_name, value_string, cvar.description])
|
||||
rich_label.append_text("\n")
|
||||
|
||||
|
||||
func add_input_history(text : String) -> void:
|
||||
if (!console_history.size() || text != console_history.back()): # Don't add consecutive duplicates
|
||||
console_history.append(text)
|
||||
console_history_index = console_history.size()
|
||||
|
||||
|
||||
func set_enable_on_release_build(enable : bool):
|
||||
enable_on_release_build = enable
|
||||
if (!enable_on_release_build):
|
||||
if (!OS.is_debug_build()):
|
||||
disable()
|
||||
|
||||
|
||||
func pause() -> void:
|
||||
get_tree().paused = true
|
||||
|
||||
|
||||
func unpause() -> void:
|
||||
get_tree().paused = false
|
||||
|
||||
|
||||
func exec(filename : String) -> void:
|
||||
var path := "user://%s.txt" % [filename]
|
||||
var script := FileAccess.open(path, FileAccess.READ)
|
||||
if (script):
|
||||
while (!script.eof_reached()):
|
||||
_on_text_entered(script.get_line())
|
||||
else:
|
||||
print_error("File %s not found." % [path])
|
||||
@@ -0,0 +1 @@
|
||||
uid://ouiu5xh1cs8n
|
||||
@@ -0,0 +1,108 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const SINGLETON_NAME = &"Console"
|
||||
|
||||
##FIXME: These should be maintained in one place.
|
||||
const CONSOLE_THEME : String = &"console/theme"
|
||||
const CONSOLE_SCALE : String = &"console/scale"
|
||||
const CONSOLE_HEIGHT : String = &"console/height"
|
||||
const CONSOLE_COLOR_WARNING : String = &"console/color_warning"
|
||||
const CONSOLE_COLOR_ERROR : String = &"console/color_error"
|
||||
const CONSOLE_COLOR_INFO : String = &"console/color_info"
|
||||
const CONSOLE_COLOR_LITERAL : String = &"console/color_literal"
|
||||
const CONSOLE_TABSTOP : String = &"console/tabstop"
|
||||
const CONSOLE_CANVAS_LAYER : String = &"console/canvas_layer"
|
||||
|
||||
##FIXME: This is here because project settings do no return the default value naturally
|
||||
##This should be fixed in 4.5
|
||||
const color_dictionary : Dictionary[String, Color] = {
|
||||
CONSOLE_COLOR_ERROR: Color.LIGHT_CORAL,
|
||||
CONSOLE_COLOR_INFO: Color.LIGHT_BLUE,
|
||||
CONSOLE_COLOR_LITERAL: Color.PALE_GREEN,
|
||||
CONSOLE_COLOR_WARNING: Color.LIGHT_GOLDENROD
|
||||
}
|
||||
|
||||
func add_setting(setting_name: String, property_info: Dictionary, default: Variant) -> void:
|
||||
if not ProjectSettings.has_setting(setting_name):
|
||||
ProjectSettings.set_setting(setting_name, default)
|
||||
ProjectSettings.add_property_info(property_info)
|
||||
ProjectSettings.set_initial_value(setting_name, default)
|
||||
ProjectSettings.set_as_basic(setting_name, true)
|
||||
|
||||
func _setup_project_settings() -> void:
|
||||
## Configure Console Theme
|
||||
add_setting(CONSOLE_THEME, {
|
||||
"name": CONSOLE_THEME,
|
||||
"type": TYPE_STRING,
|
||||
"hint": PROPERTY_HINT_FILE,
|
||||
"hint_string": "*.tres",
|
||||
}, "")
|
||||
|
||||
## Configure Console Scale
|
||||
add_setting(CONSOLE_SCALE, {
|
||||
"name": CONSOLE_SCALE,
|
||||
"type": TYPE_FLOAT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,10,0.1,or_greater"
|
||||
}, 1.0)
|
||||
|
||||
## Configure Console Height
|
||||
add_setting(CONSOLE_HEIGHT, {
|
||||
"name": CONSOLE_HEIGHT,
|
||||
"type": TYPE_FLOAT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,1,0.1"
|
||||
}, 0.5)
|
||||
|
||||
## Configure Tab Spaces
|
||||
add_setting(CONSOLE_TABSTOP, {
|
||||
"name": CONSOLE_TABSTOP,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_RANGE,
|
||||
"hint_string": "0,8,1,or_greater"
|
||||
},4)
|
||||
|
||||
## Configure Canvas Layer
|
||||
add_setting(CONSOLE_CANVAS_LAYER, {
|
||||
"name": CONSOLE_CANVAS_LAYER,
|
||||
"type": TYPE_INT,
|
||||
}, 3)
|
||||
|
||||
#Configure Colors
|
||||
add_setting(CONSOLE_COLOR_ERROR, {
|
||||
"name": CONSOLE_COLOR_ERROR,
|
||||
"type": TYPE_COLOR,
|
||||
"hint": PROPERTY_HINT_COLOR_NO_ALPHA,
|
||||
}, color_dictionary[CONSOLE_COLOR_ERROR])
|
||||
|
||||
add_setting(CONSOLE_COLOR_INFO, {
|
||||
"name": CONSOLE_COLOR_INFO,
|
||||
"type": TYPE_COLOR,
|
||||
"hint": PROPERTY_HINT_COLOR_NO_ALPHA,
|
||||
}, color_dictionary[CONSOLE_COLOR_INFO])
|
||||
|
||||
add_setting(CONSOLE_COLOR_WARNING, {
|
||||
"name": CONSOLE_COLOR_WARNING,
|
||||
"type": TYPE_COLOR,
|
||||
"hint": PROPERTY_HINT_COLOR_NO_ALPHA,
|
||||
}, color_dictionary[CONSOLE_COLOR_WARNING])
|
||||
|
||||
add_setting(CONSOLE_COLOR_LITERAL, {
|
||||
"name": CONSOLE_COLOR_LITERAL,
|
||||
"type": TYPE_COLOR,
|
||||
"hint": PROPERTY_HINT_COLOR_NO_ALPHA
|
||||
}, color_dictionary[CONSOLE_COLOR_LITERAL])
|
||||
|
||||
ProjectSettings.save()
|
||||
|
||||
|
||||
func _enable_plugin() -> void:
|
||||
add_autoload_singleton(SINGLETON_NAME, "res://addons/console/console.gd")
|
||||
_setup_project_settings()
|
||||
print("Console plugin enabled.")
|
||||
|
||||
|
||||
func _disable_plugin() -> void:
|
||||
remove_autoload_singleton(SINGLETON_NAME)
|
||||
print("Console plugin disabled.")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cv2joe2dgkub1
|
||||
@@ -0,0 +1,65 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
public partial class Console : Node{
|
||||
static GodotObject console = (GodotObject)GD.Load<GDScript>("uid://ouiu5xh1cs8n").New();
|
||||
|
||||
public static bool Enabled{ get{return console.Get("enabled").AsBool();} }
|
||||
public static bool EnabledOnReleaseBuild{ get{return console.Get("enabled_on_release_build").AsBool();} }
|
||||
public static bool PauseEnabled{ get{return console.Get("pause_enabled").AsBool();} set{console.Set("pause_enabled", value);} }
|
||||
public static int FontSize{ get{return console.Get("font_size").AsInt32();} set{console.Set("font_size", value);} }
|
||||
|
||||
public static RichTextLabel RichLabel{ get{return (RichTextLabel)console.Get("rich_label").AsGodotObject();} set{console.Set("rich_label", value);} }
|
||||
public static LineEdit LineEdit{ get{return (LineEdit)console.Get("line_edit").AsGodotObject();} set{console.Set("line_edit", value);} }
|
||||
|
||||
public override void _Ready(){
|
||||
AddChild((Node)console);
|
||||
}
|
||||
|
||||
public static void ConnectConsoleOpened(Callable callable) => console.Connect("console_opened", callable);
|
||||
public static void DisconnectConsoleOpened(Callable callable) => console.Disconnect("console_opened", callable);
|
||||
public static void ConnectConsoleClosed(Callable callable) => console.Connect("console_closed", callable);
|
||||
public static void DisconnectConsoleClosed(Callable callable) => console.Disconnect("console_closed", callable);
|
||||
public static void ConnectConsoleUnknownCommand(Callable callable) => console.Connect("console_unknown_command", callable);
|
||||
public static void DisconnectConsoleUnknownCommand(Callable callable) => console.Disconnect("console_unknown_command", callable);
|
||||
public static void ConnectConsoleCvarChanged(Callable callable) => console.Connect("console_cvar_changed", callable);
|
||||
public static void DisconnectConsoleCvarChanged(Callable callable) => console.Disconnect("console_cvar_changed", callable);
|
||||
|
||||
public static void AddCommand(string commandName, Callable function, Array<string> arguments, int required = 0, string description = "") => console.Call("add_command", commandName, function, arguments, required, description);
|
||||
public static void AddHiddenCommand(string commandName, Callable function, Array<string> arguments, int required) => console.Call("add_hidden_command", commandName, function, arguments, required);
|
||||
public static void RemoveCommand(string commandName) => console.Call("remove_command", commandName);
|
||||
public static void AddCvar(string cvarName, Variant defaultValue, string description = "", bool save = false) => console.Call("add_cvar", cvarName, defaultValue, description, save);
|
||||
public static void AddCvarReference(string cvarName, GodotObject @object, string property, string description = "", bool save = false) => console.Call("add_cvar_reference", cvarName, @object, property, description, save);
|
||||
public static void RemoveCvar(string cvarName) => console.Call("remove_cvar", cvarName);
|
||||
public static Variant GetCvar(string cvarName) => console.Call("get_cvar", cvarName);
|
||||
public static void SetCvar(string cvarName, Variant value) => console.Call("set_cvar", cvarName, value);
|
||||
public static void AddCommandAutocompleteList(string commandName, string[] paramList) => console.Call("add_command_autocomplete_list", commandName, paramList);
|
||||
public static void Disable() => console.Call("disable");
|
||||
public static void Enable() =>console.Call("enable");
|
||||
public static void ToggleConsole() => console.Call("toggle_console");
|
||||
public static void IsVisible() => console.Call("is_visible");
|
||||
public static void ScrollToBottom() => console.Call("scroll_to_bottom");
|
||||
public static void PrintError(Variant text, bool printGodot = false) => console.Call("print_error", text, printGodot);
|
||||
public static void PrintInfo(Variant text, bool printGodot = false) => console.Call("print_info", text, printGodot);
|
||||
public static void PrintWarning(Variant text, bool printGodot = false) => console.Call("print_warning", text, printGodot);
|
||||
public static void PrintLine(Variant text, bool printGodot = false) => console.Call("print_line", text, printGodot);
|
||||
public static void SetEnableOnReleaseBuild(bool enable) => console.Call("set_enable_on_release_build", enable);
|
||||
|
||||
/*
|
||||
public static void Autocomplete() => console.Call("autocomplete");
|
||||
public static void ResetAutocomplete() => console.Call("reset_autocomplete");
|
||||
public static void ToggleSize() => console.Call("toggle_size");
|
||||
public static string[] ParseLineInput(string text){ return console.Call("parse_line_input", text).AsStringArray(); }
|
||||
public static void Quit() => console.Call("quit");
|
||||
public static void Clear() => console.Call("clear");
|
||||
public static void DeleteHistory() => console.Call("delete_history");
|
||||
public static void Help() => console.Call("help");
|
||||
public static void Calculate() => console.Call("calculate");
|
||||
public static void Commands() => console.Call("commands");
|
||||
public static void CommandsList() => console.Call("commands_list");
|
||||
public static void Cvars() => console.Call("cvars");
|
||||
public static void Pause() => console.Call("pause");
|
||||
public static void Unpause() => console.Call("unpause");
|
||||
public static void Exec(string fileName) => console.Call("exec", filename);
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bdi2oxut3dsra
|
||||
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="Developer Console"
|
||||
description="Developer console. Press ~ to activate it in game and execute commands."
|
||||
author="jitspoe"
|
||||
version="1.5.0"
|
||||
script="console_plugin.gd"
|
||||
@@ -0,0 +1,29 @@
|
||||
@tool
|
||||
extends RichTextEffect
|
||||
class_name SystemColor
|
||||
|
||||
var bbcode : String = "system_color"
|
||||
|
||||
func _process_custom_fx(char_fx : CharFXTransform):
|
||||
var _system_color = char_fx.env.get("color")
|
||||
|
||||
if not _system_color:
|
||||
return false
|
||||
|
||||
if not _system_color in Console:
|
||||
return false
|
||||
|
||||
# Convert string to const value
|
||||
_system_color = Console.get_script().get_script_constant_map()[_system_color]
|
||||
|
||||
var _color : Color = Console.color_dictionary[_system_color]
|
||||
|
||||
if ProjectSettings.has_setting(_system_color):
|
||||
_color = ProjectSettings.get_setting(_system_color)
|
||||
|
||||
if not _color:
|
||||
return false
|
||||
|
||||
char_fx.color = _color
|
||||
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://cw62jxvqhw333
|
||||
@@ -1,6 +1,8 @@
|
||||
[runnable_presets]
|
||||
|
||||
Web="Web"
|
||||
Linux="Linux"
|
||||
"Windows Desktop"="Windows Desktop"
|
||||
|
||||
[preset.0]
|
||||
|
||||
@@ -53,3 +55,120 @@ threads/godot_pool_size=4
|
||||
dotnet/include_scripts_content=true
|
||||
dotnet/include_debug_symbols=true
|
||||
dotnet/embed_build_outputs=true
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="Linux"
|
||||
platform="Linux"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="Build/*"
|
||||
export_path="Build/scavgame.x86_64"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
shader_baker/enabled=true
|
||||
binary_format/architecture="x86_64"
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="#!/usr/bin/env bash
|
||||
export DISPLAY=:0
|
||||
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
||||
\"{temp_dir}/{exe_name}\" {cmd_args}"
|
||||
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||
pkill -x -f \"{temp_dir}/{exe_name} {cmd_args}\"
|
||||
rm -rf \"{temp_dir}\""
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="Windows Desktop"
|
||||
platform="Windows Desktop"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="Build/*"
|
||||
export_path="Build/scavgame.exe"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.2.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
shader_baker/enabled=false
|
||||
binary_format/architecture="x86_64"
|
||||
codesign/enable=false
|
||||
codesign/timestamp=true
|
||||
codesign/timestamp_server_url=""
|
||||
codesign/digest_algorithm=1
|
||||
codesign/description=""
|
||||
codesign/custom_options=PackedStringArray()
|
||||
application/modify_resources=true
|
||||
application/icon=""
|
||||
application/console_wrapper_icon=""
|
||||
application/icon_interpolation=4
|
||||
application/file_version=""
|
||||
application/product_version=""
|
||||
application/company_name=""
|
||||
application/product_name=""
|
||||
application/file_description=""
|
||||
application/copyright=""
|
||||
application/trademarks=""
|
||||
application/export_angle=0
|
||||
application/export_d3d12=0
|
||||
application/d3d12_agility_sdk_multiarch=true
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||
Start-ScheduledTask -TaskName godot_remote_debug
|
||||
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||
|
||||
+16
-1
@@ -11,21 +11,32 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="scavgame"
|
||||
config/version="1.0.0"
|
||||
run/main_scene="uid://cjwfyvtoeaqid"
|
||||
config/use_custom_user_dir=true
|
||||
config/custom_user_dir_name="Astronand/scavgamedemo"
|
||||
config/features=PackedStringArray("4.7")
|
||||
boot_splash/show_image=false
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
Console="*uid://ouiu5xh1cs8n"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
window/size/borderless=true
|
||||
|
||||
[dotnet]
|
||||
|
||||
project/assembly_name="Scavenger"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/console/plugin.cfg")
|
||||
|
||||
[layer_names]
|
||||
|
||||
3d_physics/layer_1="Physics"
|
||||
@@ -38,4 +49,8 @@ project/assembly_name="Scavenger"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="mobile"
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
|
||||
[webrtc]
|
||||
|
||||
default_extension="res://addons/webrtc_native/webrtc_native.gdextension"
|
||||
|
||||
Reference in New Issue
Block a user