') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix: don't build the vehicle HUD on a dedicated server by KeilerHirsch · Pull Request #1298 · Courseplay/Courseplay_FS25 · GitHub
Skip to content

fix: don't build the vehicle HUD on a dedicated server - #1298

Draft
KeilerHirsch wants to merge 1 commit into
Courseplay:mainfrom
KeilerHirsch:fix/hud-not-built-on-dedicated-server
Draft

fix: don't build the vehicle HUD on a dedicated server#1298
KeilerHirsch wants to merge 1 commit into
Courseplay:mainfrom
KeilerHirsch:fix/hud-not-built-on-dedicated-server

Conversation

@KeilerHirsch

Copy link
Copy Markdown

What this changes

CpHud:onPostLoad builds a CpBaseHud for every CP-capable vehicle. It's registered as a plain event listener (CpHud.lua:43), so it runs on a dedicated server too — where the HUD is never drawn.

functionCpHud:onPostLoad(savegame)
localspec=self.spec_cpHudspec.hud=CpBaseHud(self) -- :288, no isClient guardend

CpBaseHud:init (scripts/gui/hud/CpBaseHud.lua:71-309) is pure client work: g_gameSettings:getValue("uiScale") (:74), getNormalizedScreenValues (:82, :84), g_screenAspectRatio (:88), 11 Overlay.new allocations (:106, :139, :157, :185, :196, :209, :226, :241, :259, :275, :292), plus five full HUD page trees via addHudPage, each with their own overlays and button overlays.

The same file already guards this correctly 180 lines further up:

functionCpHud:onRegisterActionEvents(...)
ifself.isClientthen-- :104

This PR applies the same guard to onPostLoad.

Why it's safe

No server-side path reads spec.hud. I checked every consumer:

ConsumerWhy it can't hit a nil hud on a dedi
onEnterVehicle:304gated at :300 by self == CpUtil.getCurrentVehicle(), which returns nil when g_localPlayer is nil (CpUtil.lua:487-491)
onDrawUIInfo:359gated at :354 by the same call; also a draw path
actionEventMouse:140/145/153input action — action events are only registered under if self.isClient (:104)
getIsMouseOverCpHud:100only reached from actionEventCameraZoomInOut:317, an input action
enterVehicleRaycastClickToSwitch:90mouse raycast
closeCpHud:172only called from CpBaseHud.lua:206, i.e. from a HUD that exists
getCpHud:178only caller is Courseplay.lua:225, already nil-guarded (vehicle and vehicle.getCpHud and vehicle:getCpHud()if hud then)
onUpdate:339-350the isServer branch touches spec.status and spec.hudSettings only — never spec.hud
resetCpHud:158-167does not touch spec.hud (the line that would is commented out at :166)

luac -p passes.

Honesty about verification

This is statically verified, not measured in-game. What I have counted is the allocation sites (11 overlays + 5 page trees per vehicle) and proven that the construction runs headless and that nothing headless consumes it. What I have not done is measure the actual memory or load-time delta on a server — so I'm deliberately not putting a number on the benefit, and I'm not claiming this fixes a crash. It doesn't; CP demonstrably runs fine on dedicated servers today.

I run a dedicated server with a large mod set and I'm happy to measure a before/after load time if that's useful for judging whether this is worth taking.

Side note (not touched here)

CpHud:cpInit (:412-414) is a second, also unguarded HUD construction site, and it appears to be dead: repo-wide, cpInit only shows up at its registration (:59) and its definition (:412) — no callers. Left alone to keep this diff to the one guard, but worth a look if you're cleaning up.

onPostLoad is registered for every CP-capable vehicle and runs headless
too, where CpBaseHud allocates 11+ overlays and 5 page trees per vehicle
that are never drawn. Guard it the same way onRegisterActionEvents
already does at CpHud.lua:104.
No server path reads spec.hud: onEnterVehicle:300 and onDrawUIInfo:354
both gate on CpUtil.getCurrentVehicle(), which returns nil when
g_localPlayer is nil (CpUtil.lua:487-491); resetCpHud does not touch it;
Courseplay.lua:225 nil-guards getCpHud().
@Tensuko

Copy link
Copy Markdown
Contributor

What does this solve?

@TensukoTensuko added the waiting for user User answer needed label Aug 19, 2026
@Tensuko
Tensuko marked this pull request as draft August 25, 2026 11:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting for userUser answer needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@KeilerHirsch@Tensuko