diff --git a/FiveM/nyx_core/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql b/FiveM/nyx_core/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql new file mode 100644 index 0000000..da082f2 --- /dev/null +++ b/FiveM/nyx_core/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql @@ -0,0 +1,99 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_properties, nyx_treasury & nyx_vehicles +-- Migration: 067_municipal_water_utility_and_hydrant_pressure.sql +-- Description: Municipal reservoirs, contamination diffusion, pump station power +-- dependencies, and fire hydrant pressure dynamics. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal water reservoirs table +CREATE TABLE IF NOT EXISTS `nyx_water_reservoirs` ( + `reservoir_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `label` VARCHAR(64) NOT NULL, + `water_level_liters` DECIMAL(15, 2) NOT NULL DEFAULT 5000000.00, + `max_capacity_liters` DECIMAL(15, 2) NOT NULL DEFAULT 10000000.00, + `purity_percentage` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `contamination_ppm` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `substation_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_res_substation` (`substation_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal pump stations table +CREATE TABLE IF NOT EXISTS `nyx_water_pump_stations` ( + `pump_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `substation_id` VARCHAR(32) NOT NULL, + `pump_status` ENUM('ONLINE', 'OFFLINE_POWER', 'FAULT') NOT NULL DEFAULT 'ONLINE', + `flow_rate_lps` DECIMAL(8, 2) NOT NULL DEFAULT 150.00, + `power_draw_kw` DECIMAL(8, 2) NOT NULL DEFAULT 750.00, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_pump_res` (`reservoir_id`), + INDEX `idx_pump_sub` (`substation_id`), + INDEX `idx_pump_status` (`pump_status`), + CONSTRAINT `fk_pump_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create municipal fire hydrants table +CREATE TABLE IF NOT EXISTS `nyx_fire_hydrants` ( + `hydrant_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `pressure_psi` DECIMAL(6, 2) NOT NULL DEFAULT 85.00, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `last_inspected_at` TIMESTAMP NULL DEFAULT NULL, + INDEX `idx_hydrant_res` (`reservoir_id`), + INDEX `idx_hydrant_status` (`is_active`), + CONSTRAINT `fk_hydrant_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Create water contamination incident log table +CREATE TABLE IF NOT EXISTS `nyx_water_contamination_events` ( + `event_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `contaminant_type` VARCHAR(64) NOT NULL, + `ppm_added` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `reported_by` VARCHAR(64) NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_contam_res` (`reservoir_id`), + INDEX `idx_contam_time` (`created_at` DESC), + CONSTRAINT `fk_contam_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Seed default reservoirs +INSERT INTO `nyx_water_reservoirs` + (`reservoir_id`, `label`, `water_level_liters`, `max_capacity_liters`, `purity_percentage`, `contamination_ppm`, `substation_id`, `coords_x`, `coords_y`, `coords_z`) +VALUES + ('res_vinewood_lake', 'Vinewood Hills Main Reservoir', 8500000.00, 10000000.00, 100.00, 0.00, 'grid_central_ls', 212.4500, 715.3200, 185.1000), + ('res_zancudo_aquifer', 'Fort Zancudo Subterranean Aquifer', 6000000.00, 8000000.00, 99.50, 0.50, 'grid_zancudo', -2250.1000, 3210.4500, 15.2000), + ('res_paleto_basin', 'Paleto Bay Coastal Basin', 4500000.00, 5000000.00, 100.00, 0.00, 'grid_paleto', -280.5000, 6210.8000, 30.0000) +ON DUPLICATE KEY UPDATE `label` = VALUES(`label`); + +-- Seed default pump stations +INSERT INTO `nyx_water_pump_stations` + (`pump_id`, `reservoir_id`, `substation_id`, `pump_status`, `flow_rate_lps`, `power_draw_kw`) +VALUES + ('pump_vinewood_01', 'res_vinewood_lake', 'grid_central_ls', 'ONLINE', 250.00, 1200.00), + ('pump_zancudo_01', 'res_zancudo_aquifer', 'grid_zancudo', 'ONLINE', 180.00, 900.00), + ('pump_paleto_01', 'res_paleto_basin', 'grid_paleto', 'ONLINE', 120.00, 600.00) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +-- Seed default fire hydrants +INSERT INTO `nyx_fire_hydrants` + (`hydrant_id`, `reservoir_id`, `coords_x`, `coords_y`, `coords_z`, `pressure_psi`, `is_active`) +VALUES + ('hydrant_vinewood_01', 'res_vinewood_lake', 220.1000, 700.5000, 180.2000, 85.00, 1), + ('hydrant_legion_01', 'res_vinewood_lake', 150.0000, -900.0000, 30.0000, 90.00, 1), + ('hydrant_zancudo_01', 'res_zancudo_aquifer', -2200.0000, 3200.0000, 15.0000, 80.00, 1), + ('hydrant_paleto_01', 'res_paleto_basin', -270.0000, 6200.0000, 30.0000, 75.00, 1) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_core/sql/migrations/index.json b/FiveM/nyx_core/sql/migrations/index.json index 4697762..56b42d6 100644 --- a/FiveM/nyx_core/sql/migrations/index.json +++ b/FiveM/nyx_core/sql/migrations/index.json @@ -65,5 +65,5 @@ "064_industrial_refining_and_functional_parts_manufacturing.sql", "065_judicial_bail_bonds_surety_escrow_and_fugitive_recovery.sql", "066_municipal_toll_booths_and_commercial_weigh_stations.sql", - "067_highway_degradation_and_transit_repair.sql" + "067_municipal_water_utility_and_hydrant_pressure.sql" ] diff --git a/FiveM/nyx_properties/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql b/FiveM/nyx_properties/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql new file mode 100644 index 0000000..da082f2 --- /dev/null +++ b/FiveM/nyx_properties/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql @@ -0,0 +1,99 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_properties, nyx_treasury & nyx_vehicles +-- Migration: 067_municipal_water_utility_and_hydrant_pressure.sql +-- Description: Municipal reservoirs, contamination diffusion, pump station power +-- dependencies, and fire hydrant pressure dynamics. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal water reservoirs table +CREATE TABLE IF NOT EXISTS `nyx_water_reservoirs` ( + `reservoir_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `label` VARCHAR(64) NOT NULL, + `water_level_liters` DECIMAL(15, 2) NOT NULL DEFAULT 5000000.00, + `max_capacity_liters` DECIMAL(15, 2) NOT NULL DEFAULT 10000000.00, + `purity_percentage` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `contamination_ppm` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `substation_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_res_substation` (`substation_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal pump stations table +CREATE TABLE IF NOT EXISTS `nyx_water_pump_stations` ( + `pump_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `substation_id` VARCHAR(32) NOT NULL, + `pump_status` ENUM('ONLINE', 'OFFLINE_POWER', 'FAULT') NOT NULL DEFAULT 'ONLINE', + `flow_rate_lps` DECIMAL(8, 2) NOT NULL DEFAULT 150.00, + `power_draw_kw` DECIMAL(8, 2) NOT NULL DEFAULT 750.00, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_pump_res` (`reservoir_id`), + INDEX `idx_pump_sub` (`substation_id`), + INDEX `idx_pump_status` (`pump_status`), + CONSTRAINT `fk_pump_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create municipal fire hydrants table +CREATE TABLE IF NOT EXISTS `nyx_fire_hydrants` ( + `hydrant_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `pressure_psi` DECIMAL(6, 2) NOT NULL DEFAULT 85.00, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `last_inspected_at` TIMESTAMP NULL DEFAULT NULL, + INDEX `idx_hydrant_res` (`reservoir_id`), + INDEX `idx_hydrant_status` (`is_active`), + CONSTRAINT `fk_hydrant_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Create water contamination incident log table +CREATE TABLE IF NOT EXISTS `nyx_water_contamination_events` ( + `event_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `contaminant_type` VARCHAR(64) NOT NULL, + `ppm_added` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `reported_by` VARCHAR(64) NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_contam_res` (`reservoir_id`), + INDEX `idx_contam_time` (`created_at` DESC), + CONSTRAINT `fk_contam_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Seed default reservoirs +INSERT INTO `nyx_water_reservoirs` + (`reservoir_id`, `label`, `water_level_liters`, `max_capacity_liters`, `purity_percentage`, `contamination_ppm`, `substation_id`, `coords_x`, `coords_y`, `coords_z`) +VALUES + ('res_vinewood_lake', 'Vinewood Hills Main Reservoir', 8500000.00, 10000000.00, 100.00, 0.00, 'grid_central_ls', 212.4500, 715.3200, 185.1000), + ('res_zancudo_aquifer', 'Fort Zancudo Subterranean Aquifer', 6000000.00, 8000000.00, 99.50, 0.50, 'grid_zancudo', -2250.1000, 3210.4500, 15.2000), + ('res_paleto_basin', 'Paleto Bay Coastal Basin', 4500000.00, 5000000.00, 100.00, 0.00, 'grid_paleto', -280.5000, 6210.8000, 30.0000) +ON DUPLICATE KEY UPDATE `label` = VALUES(`label`); + +-- Seed default pump stations +INSERT INTO `nyx_water_pump_stations` + (`pump_id`, `reservoir_id`, `substation_id`, `pump_status`, `flow_rate_lps`, `power_draw_kw`) +VALUES + ('pump_vinewood_01', 'res_vinewood_lake', 'grid_central_ls', 'ONLINE', 250.00, 1200.00), + ('pump_zancudo_01', 'res_zancudo_aquifer', 'grid_zancudo', 'ONLINE', 180.00, 900.00), + ('pump_paleto_01', 'res_paleto_basin', 'grid_paleto', 'ONLINE', 120.00, 600.00) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +-- Seed default fire hydrants +INSERT INTO `nyx_fire_hydrants` + (`hydrant_id`, `reservoir_id`, `coords_x`, `coords_y`, `coords_z`, `pressure_psi`, `is_active`) +VALUES + ('hydrant_vinewood_01', 'res_vinewood_lake', 220.1000, 700.5000, 180.2000, 85.00, 1), + ('hydrant_legion_01', 'res_vinewood_lake', 150.0000, -900.0000, 30.0000, 90.00, 1), + ('hydrant_zancudo_01', 'res_zancudo_aquifer', -2200.0000, 3200.0000, 15.0000, 80.00, 1), + ('hydrant_paleto_01', 'res_paleto_basin', -270.0000, 6200.0000, 30.0000, 75.00, 1) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_services/client/water_utility.lua b/FiveM/nyx_services/client/water_utility.lua new file mode 100644 index 0000000..411a1b2 --- /dev/null +++ b/FiveM/nyx_services/client/water_utility.lua @@ -0,0 +1,125 @@ +--[[ + Nyxframework: nyx_services + File: client/water_utility.lua + Description: Client-side spatial interactions, target hooks via nyx_interact, + and UI telemetry bridge via nyx_ui for fire hydrants and water reservoirs. +]] + +Nyx = Nyx or {} + +-- Initialize spatial interaction points for fire hydrants and reservoirs +local function SetupWaterUtilityTargets() + if not exports['nyx_interact'] then return end + + local hydrants = GlobalState.fireHydrants or {} + for hydrantId, h in pairs(hydrants) do + if h.coords then + exports['nyx_interact']:AddTargetCoords("fire_hydrant_" .. hydrantId, vec3(h.coords.x, h.coords.y, h.coords.z), 2.5, { + options = { + { + icon = "fa-solid fa-fire-hydrant", + label = string.format("Inspect Hydrant Pressure (ID: %s)", hydrantId), + action = function() + TriggerEvent("nyx_services:client:inspectHydrant", hydrantId) + end + }, + { + icon = "fa-solid fa-hose", + label = "Connect Tender Hose / Draft Water", + action = function() + TriggerEvent("nyx_services:client:draftHydrantMenu", hydrantId) + end + } + ], + distance = 2.5 + }) + end + end + + local reservoirs = GlobalState.waterReservoirs or {} + for resId, r in pairs(reservoirs) do + if r.coords then + exports['nyx_interact']:AddTargetCoords("reservoir_valve_" .. resId, vec3(r.coords.x, r.coords.y, r.coords.z), 3.0, { + options = { + { + icon = "fa-solid fa-faucet-drip", + label = string.format("Access Intake Valve (%s)", r.label or resId), + action = function() + TriggerEvent("nyx_services:client:openReservoirMenu", resId) + end + } + ], + distance = 3.0 + }) + end + end +end + +-- Event handler for inspecting hydrant status +RegisterNetEvent("nyx_services:client:inspectHydrant", function(hydrantId) + local hydrants = GlobalState.fireHydrants or {} + local h = hydrants[hydrantId] + if not h then + if exports['nyx_notify'] then + exports['nyx_notify']:SendNotification("Hydrant telemetry unavailable.", "error") + end + return + end + + local statusText = string.format("Hydrant %s | Pressure: %.1f PSI | Active: %s", hydrantId, h.pressurePsi or 0.0, h.isActive and "YES" or "NO") + if exports['nyx_notify'] then + exports['nyx_notify']:SendNotification(statusText, "info") + else + print("[Nyx:WaterUtility] " .. statusText) + end +end) + +-- Event handler for opening reservoir intake menu +RegisterNetEvent("nyx_services:client:openReservoirMenu", function(reservoirId) + local reservoirs = GlobalState.waterReservoirs or {} + local r = reservoirs[reservoirId] + if not r then + if exports['nyx_notify'] then + exports['nyx_notify']:SendNotification("Reservoir telemetry unavailable.", "error") + end + return + end + + local statusText = string.format("Reservoir: %s | Level: %.0f/%.0f L | Purity: %.1f%% | Contam: %.2f PPM", + r.label or reservoirId, r.waterLevelLiters or 0, r.maxCapacityLiters or 0, r.purityPercentage or 100, r.contaminationPpm or 0) + if exports['nyx_notify'] then + exports['nyx_notify']:SendNotification(statusText, "info") + else + print("[Nyx:WaterUtility] " .. statusText) + end +end) + +-- Event handler for opening drafting menu +RegisterNetEvent("nyx_services:client:draftHydrantMenu", function(hydrantId) + if exports['nyx_core'] and exports['nyx_core'].TriggerRPC then + exports['nyx_core']:TriggerRPC("services:draftHydrant", function(success, res) + if success and res then + local msg = string.format("Drafted %sL. Reservoir Level: %sL | Pressure: %.1f PSI | Purity: %.1f%%", + res.litersDrawn, res.remainingLevel, res.pressurePsi, res.purity) + if exports['nyx_notify'] then + exports['nyx_notify']:SendNotification(msg, "success") + end + else + local err = string.format("Failed to draft from hydrant: %s", tostring(res)) + if exports['nyx_notify'] then + exports['nyx_notify']:SendNotification(err, "error") + end + end + end, hydrantId, 500.0) -- Draft 500L standard batch + end +end) + +CreateThread(function() + Wait(2000) + SetupWaterUtilityTargets() +end) + +RegisterNetEvent("nyx:core:client:dbReady", function() + Wait(1000) + SetupWaterUtilityTargets() +end) diff --git a/FiveM/nyx_services/fxmanifest.lua b/FiveM/nyx_services/fxmanifest.lua index d07cc09..6b67a40 100644 --- a/FiveM/nyx_services/fxmanifest.lua +++ b/FiveM/nyx_services/fxmanifest.lua @@ -22,7 +22,8 @@ shared_scripts { 'shared/sabotage.lua', 'shared/metering.lua', 'shared/hospital_beds.lua', - 'shared/chemical_labs.lua' + 'shared/chemical_labs.lua', + 'shared/water_utility.lua' } server_scripts { @@ -36,6 +37,7 @@ server_scripts { 'server/chemlabs.lua', 'server/agriculture.lua', 'server/repair_stations.lua', + 'server/water_utility.lua', 'server/main.lua', 'server/exports.lua' } @@ -50,6 +52,7 @@ client_scripts { 'client/beds.lua', 'client/chemlabs.lua', 'client/agriculture.lua', + 'client/water_utility.lua', 'client/main.lua', 'client/exports.lua' } diff --git a/FiveM/nyx_services/server/exports.lua b/FiveM/nyx_services/server/exports.lua index 7852885..111311e 100644 --- a/FiveM/nyx_services/server/exports.lua +++ b/FiveM/nyx_services/server/exports.lua @@ -302,3 +302,23 @@ exports("SterilizeChemicalLab", function(src, labId) return Nyx.Services.StartLabSterilization(src, labId) end) +-- Municipal Water Utility & Hydrant Pressure +exports("GetWaterReservoirs", function() + return Nyx.Services.WaterReservoirs or {} +end) + +exports("GetFireHydrants", function() + return Nyx.Services.FireHydrants or {} +end) + +exports("ContaminateWaterReservoir", function(reservoirId, contaminantType, grams, citizenId) + return Nyx.Services.ContaminateReservoir(reservoirId, contaminantType, grams, citizenId) +end) + +exports("PurifyWaterReservoir", function(reservoirId, treatmentKg, chemicalEfficiency, citizenId) + return Nyx.Services.PurifyReservoir(reservoirId, treatmentKg, chemicalEfficiency, citizenId) +end) + +exports("DraftFromFireHydrant", function(hydrantId, requestedLiters) + return Nyx.Services.DraftFromHydrant(hydrantId, requestedLiters) +end) diff --git a/FiveM/nyx_services/server/water_utility.lua b/FiveM/nyx_services/server/water_utility.lua new file mode 100644 index 0000000..e1678a6 --- /dev/null +++ b/FiveM/nyx_services/server/water_utility.lua @@ -0,0 +1,211 @@ +--[[ + Nyxframework: nyx_services + File: server/water_utility.lua + Description: Authoritative server engine for municipal water reservoirs, contamination + diffusion, pump station power grid monitoring, and fire hydrant pressure dynamics. +]] + +Nyx = Nyx or {} +Nyx.WaterUtility = Nyx.WaterUtility or {} + +Nyx.Services = Nyx.Services or {} +Nyx.Services.WaterReservoirs = {} +Nyx.Services.WaterPumpStations = {} +Nyx.Services.FireHydrants = {} +Nyx.Services.ActiveDrafts = {} -- [hydrantId] = activeDraftCount + +-- Load active reservoirs, pump stations, and hydrants from DB +local function LoadWaterUtilityState() + MySQL.query("SELECT * FROM `nyx_water_reservoirs`", {}, function(reservoirs) + if reservoirs then + for _, r in ipairs(reservoirs) do + Nyx.Services.WaterReservoirs[r.reservoir_id] = { + id = r.reservoir_id, + label = r.label, + waterLevelLiters = tonumber(r.water_level_liters) or 5000000.00, + maxCapacityLiters = tonumber(r.max_capacity_liters) or 10000000.00, + purityPercentage = tonumber(r.purity_percentage) or 100.00, + contaminationPpm = tonumber(r.contamination_ppm) or 0.00, + substationId = r.substation_id, + coords = vec3(tonumber(r.coords_x) or 0.0, tonumber(r.coords_y) or 0.0, tonumber(r.coords_z) or 0.0) + } + end + end + + MySQL.query("SELECT * FROM `nyx_water_pump_stations`", {}, function(pumps) + if pumps then + for _, p in ipairs(pumps) do + Nyx.Services.WaterPumpStations[p.pump_id] = { + id = p.pump_id, + reservoirId = p.reservoir_id, + substationId = p.substation_id, + pumpStatus = p.pump_status or "ONLINE", + flowRateLps = tonumber(p.flow_rate_lps) or 150.00, + powerDrawKw = tonumber(p.power_draw_kw) or 750.00 + } + end + end + + MySQL.query("SELECT * FROM `nyx_fire_hydrants`", {}, function(hydrants) + if hydrants then + for _, h in ipairs(hydrants) do + Nyx.Services.FireHydrants[h.hydrant_id] = { + id = h.hydrant_id, + reservoirId = h.reservoir_id, + coords = vec3(tonumber(h.coords_x) or 0.0, tonumber(h.coords_y) or 0.0, tonumber(h.coords_z) or 0.0), + pressurePsi = tonumber(h.pressure_psi) or 85.00, + isActive = (h.is_active == 1 or h.is_active == true) + } + end + end + + Nyx.Services.UpdateWaterSystemTelemetry() + end) + end) + end) +end + +-- Refresh pump statuses based on power grid substations & compute hydrant pressures +function Nyx.Services.UpdateWaterSystemTelemetry() + for pumpId, pump in pairs(Nyx.Services.WaterPumpStations) do + local sub = Nyx.Services.Substations and Nyx.Services.Substations[pump.substationId] + if sub and (sub.isBlackout or sub.breakerTripped or sub.conditionHealth <= 0) then + pump.pumpStatus = "OFFLINE_POWER" + else + if pump.pumpStatus == "OFFLINE_POWER" then + pump.pumpStatus = "ONLINE" + end + end + + MySQL.update("UPDATE `nyx_water_pump_stations` SET `pump_status` = ? WHERE `pump_id` = ?", { + pump.pumpStatus, pumpId + }) + end + + for hydrantId, hydrant in pairs(Nyx.Services.FireHydrants) do + local res = Nyx.Services.WaterReservoirs[hydrant.reservoirId] + if res then + -- Find linked pump station status + local pumpStatus = "ONLINE" + for _, pump in pairs(Nyx.Services.WaterPumpStations) do + if pump.reservoirId == res.id then + pumpStatus = pump.pumpStatus + break + end + end + + local draftCount = Nyx.Services.ActiveDrafts[hydrantId] or 0 + hydrant.pressurePsi = Nyx.WaterUtility.CalculateHydrantPressure(res.waterLevelLiters, res.maxCapacityLiters, pumpStatus, draftCount) + + MySQL.update("UPDATE `nyx_fire_hydrants` SET `pressure_psi` = ? WHERE `hydrant_id` = ?", { + hydrant.pressurePsi, hydrantId + }) + end + end + + GlobalState.waterReservoirs = Nyx.Services.WaterReservoirs + GlobalState.fireHydrants = Nyx.Services.FireHydrants +end + +--- Add contaminant grams to reservoir +function Nyx.Services.ContaminateReservoir(reservoirId, contaminantType, grams, sourceCitizenId) + local res = Nyx.Services.WaterReservoirs[reservoirId] + if not res then return false, "RESERVOIR_NOT_FOUND" end + + local newPpm, newPurity = Nyx.WaterUtility.CalculateContaminationDiffusion(res.contaminationPpm, res.waterLevelLiters, grams) + res.contaminationPpm = newPpm + res.purityPercentage = newPurity + + MySQL.update("UPDATE `nyx_water_reservoirs` SET `contamination_ppm` = ?, `purity_percentage` = ? WHERE `reservoir_id` = ?", { + res.contaminationPpm, res.purityPercentage, reservoirId + }) + + MySQL.insert("INSERT INTO `nyx_water_contamination_events` (`reservoir_id`, `contaminant_type`, `ppm_added`, `reported_by`) VALUES (?, ?, ?, ?)", { + reservoirId, contaminantType, grams, sourceCitizenId or "SYSTEM" + }) + + Nyx.Services.UpdateWaterSystemTelemetry() + return true, { ppm = newPpm, purity = newPurity } +end + +--- Purify reservoir with chemical agent +function Nyx.Services.PurifyReservoir(reservoirId, treatmentKg, chemicalEfficiency, sourceCitizenId) + local res = Nyx.Services.WaterReservoirs[reservoirId] + if not res then return false, "RESERVOIR_NOT_FOUND" end + + local newPpm, newPurity = Nyx.WaterUtility.CalculatePurificationTreatment(res.contaminationPpm, treatmentKg, chemicalEfficiency or 50.0, res.waterLevelLiters) + res.contaminationPpm = newPpm + res.purityPercentage = newPurity + + MySQL.update("UPDATE `nyx_water_reservoirs` SET `contamination_ppm` = ?, `purity_percentage` = ? WHERE `reservoir_id` = ?", { + res.contaminationPpm, res.purityPercentage, reservoirId + }) + + Nyx.Services.UpdateWaterSystemTelemetry() + return true, { ppm = newPpm, purity = newPurity } +end + +--- Draft water from a hydrant into a fire vehicle or tank +function Nyx.Services.DraftFromHydrant(hydrantId, requestedLiters) + local hydrant = Nyx.Services.FireHydrants[hydrantId] + if not hydrant then return false, "HYDRANT_NOT_FOUND" end + if not hydrant.isActive then return false, "HYDRANT_INACTIVE" end + + local res = Nyx.Services.WaterReservoirs[hydrant.reservoirId] + if not res then return false, "RESERVOIR_NOT_FOUND" end + + if hydrant.pressurePsi < 5.0 then + return false, "INSUFFICIENT_PRESSURE" + end + + local actualDrawn = math.min(requestedLiters, res.waterLevelLiters) + res.waterLevelLiters = Nyx.WaterUtility.CalculateWaterConsumption(res.waterLevelLiters, actualDrawn, 0, 0) + + MySQL.update("UPDATE `nyx_water_reservoirs` SET `water_level_liters` = ? WHERE `reservoir_id` = ?", { + res.waterLevelLiters, res.id + }) + + Nyx.Services.UpdateWaterSystemTelemetry() + return true, { litersDrawn = actualDrawn, remainingLevel = res.waterLevelLiters, pressurePsi = hydrant.pressurePsi, purity = res.purityPercentage } +end + +-- Hook database readiness +AddEventHandler("nyx:core:dbReady", function() + LoadWaterUtilityState() +end) + +CreateThread(function() + while true do + Wait(60000) -- Refresh water system telemetry every 60s + if Nyx.Services.WaterReservoirs and next(Nyx.Services.WaterReservoirs) then + Nyx.Services.UpdateWaterSystemTelemetry() + end + end +end) + +-- RPC Registrations +if exports['nyx_core'] and exports['nyx_core'].RegisterRPC then + exports['nyx_core']:RegisterRPC("services:getWaterReservoirs", function(src) + return Nyx.Services.WaterReservoirs + end) + + exports['nyx_core']:RegisterRPC("services:getFireHydrants", function(src) + return Nyx.Services.FireHydrants + end) + + exports['nyx_core']:RegisterRPC("services:draftHydrant", function(src, hydrantId, liters) + return Nyx.Services.DraftFromHydrant(hydrantId, liters) + end) + + exports['nyx_core']:RegisterRPC("services:contaminateReservoir", function(src, reservoirId, contaminantType, grams) + local player = Nyx.GetPlayer and Nyx.GetPlayer(src) + local citizenId = player and player.citizenId or "UNKNOWN" + return Nyx.Services.ContaminateReservoir(reservoirId, contaminantType, grams, citizenId) + end) + + exports['nyx_core']:RegisterRPC("services:purifyReservoir", function(src, reservoirId, treatmentKg) + local player = Nyx.GetPlayer and Nyx.GetPlayer(src) + local citizenId = player and player.citizenId or "UNKNOWN" + return Nyx.Services.PurifyReservoir(reservoirId, treatmentKg, 50.0, citizenId) + end) +end diff --git a/FiveM/nyx_services/shared/water_utility.lua b/FiveM/nyx_services/shared/water_utility.lua new file mode 100644 index 0000000..751c4e6 --- /dev/null +++ b/FiveM/nyx_services/shared/water_utility.lua @@ -0,0 +1,102 @@ +--[[ + Nyxframework: nyx_services + File: shared/water_utility.lua + Description: Pure, deterministic Lua calculations for municipal water utility systems, + reservoir contamination diffusion, pump station power dependencies, + and fire hydrant pressure dynamics. +]] + +Nyx = Nyx or {} +Nyx.WaterUtility = Nyx.WaterUtility or {} + +-- Constants +Nyx.WaterUtility.BASE_HYDRANT_PSI = 100.0 -- Nominal operating pressure at 100% reservoir & pumps online +Nyx.WaterUtility.MIN_HYDRANT_PSI = 10.0 -- Low gravity residual pressure when pumps are offline +Nyx.WaterUtility.MAX_HYDRANT_PSI = 120.0 -- Maximum pressure ceiling +Nyx.WaterUtility.DRAFTING_RATE_LPS = 15.0 -- Liters per second drafted by a fire tender + +--- Calculates active fire hydrant PSI based on reservoir water ratio, pump station power status, and draft load. +--- @param waterLevelLiters number Current reservoir volume in liters +--- @param maxCapacityLiters number Maximum reservoir capacity in liters +--- @param pumpStatus string Status of linked pump station ('ONLINE', 'OFFLINE_POWER', 'FAULT') +--- @param activeDrafts number Number of active fire tenders drafting from this reservoir zone +--- @return number Hydrant PSI (0.0 to 120.0) +function Nyx.WaterUtility.CalculateHydrantPressure(waterLevelLiters, maxCapacityLiters, pumpStatus, activeDrafts) + local volumeRatio = 0.0 + if maxCapacityLiters and maxCapacityLiters > 0 then + volumeRatio = math.max(0.0, math.min(1.0, (waterLevelLiters or 0.0) / maxCapacityLiters)) + end + + local basePsi = Nyx.WaterUtility.BASE_HYDRANT_PSI * volumeRatio + + if pumpStatus == "OFFLINE_POWER" or pumpStatus == "FAULT" then + basePsi = math.min(basePsi, Nyx.WaterUtility.MIN_HYDRANT_PSI) + end + + activeDrafts = math.max(0, tonumber(activeDrafts) or 0) + local loadPenalty = activeDrafts * 5.0 -- 5 PSI pressure drop per simultaneous drafting tender + local finalPsi = math.max(0.0, basePsi - loadPenalty) + + return math.min(Nyx.WaterUtility.MAX_HYDRANT_PSI, math.floor(finalPsi * 100) / 100) +end + +--- Calculates contamination diffusion into a reservoir when hazardous materials are introduced. +--- @param currentPpm number Current contamination parts-per-million in reservoir +--- @param currentLevelLiters number Current water volume in liters +--- @param contaminantGrams number Mass of contaminant introduced in grams +--- @return number newPpm, number newPurityPercentage +function Nyx.WaterUtility.CalculateContaminationDiffusion(currentPpm, currentLevelLiters, contaminantGrams) + currentPpm = math.max(0.0, tonumber(currentPpm) or 0.0) + currentLevelLiters = math.max(1.0, tonumber(currentLevelLiters) or 1.0) + contaminantGrams = math.max(0.0, tonumber(contaminantGrams) or 0.0) + + -- 1 gram in 1,000 liters (1 m^3) = 1 PPM + local addedPpm = (contaminantGrams / currentLevelLiters) * 1000.0 + local newPpm = currentPpm + addedPpm + + -- Purity drops logarithmically with PPM: 0 PPM = 100%, 100 PPM = 90%, 1,000 PPM = 0% + local purity = math.max(0.0, math.min(100.0, 100.0 - (newPpm / 10.0))) + + return math.floor(newPpm * 100) / 100, math.floor(purity * 100) / 100 +end + +--- Calculates chemical purification dosage required to treat contaminated reservoir water. +--- @param currentPpm number Current contamination PPM +--- @param treatmentKg number Mass of purifying chemical added in kilograms +--- @param chemicalEfficiency number PPM neutralized per kg per 1,000,000 liters (default 50.0) +--- @param currentLevelLiters number Current water volume in liters +--- @return number newPpm, number newPurityPercentage +function Nyx.WaterUtility.CalculatePurificationTreatment(currentPpm, treatmentKg, chemicalEfficiency, currentLevelLiters) + currentPpm = math.max(0.0, tonumber(currentPpm) or 0.0) + treatmentKg = math.max(0.0, tonumber(treatmentKg) or 0.0) + chemicalEfficiency = math.max(1.0, tonumber(chemicalEfficiency) or 50.0) + currentLevelLiters = math.max(1.0, tonumber(currentLevelLiters) or 1.0) + + -- Neutralization formula normalized by water volume in millions of liters + local millionLiters = currentLevelLiters / 1000000.0 + local ppmReduction = (treatmentKg * chemicalEfficiency) / millionLiters + local newPpm = math.max(0.0, currentPpm - ppmReduction) + + local purity = math.max(0.0, math.min(100.0, 100.0 - (newPpm / 10.0))) + + return math.floor(newPpm * 100) / 100, math.floor(purity * 100) / 100 +end + +--- Calculates municipal water volume depletion based on property metered consumption and fire tender drafting. +--- @param currentLevelLiters number Current volume in reservoir +--- @param meteredConsumptionLiters number Volume consumed by residential/commercial grid meters +--- @param draftingDurationSeconds number Duration of active hydrant drafting in seconds +--- @param activeDraftTenders number Count of active drafting tenders +--- @return number newLevelLiters +function Nyx.WaterUtility.CalculateWaterConsumption(currentLevelLiters, meteredConsumptionLiters, draftingDurationSeconds, activeDraftTenders) + currentLevelLiters = math.max(0.0, tonumber(currentLevelLiters) or 0.0) + meteredConsumptionLiters = math.max(0.0, tonumber(meteredConsumptionLiters) or 0.0) + draftingDurationSeconds = math.max(0.0, tonumber(draftingDurationSeconds) or 0.0) + activeDraftTenders = math.max(0, tonumber(activeDraftTenders) or 0) + + local draftedVolume = draftingDurationSeconds * Nyx.WaterUtility.DRAFTING_RATE_LPS * activeDraftTenders + local totalDepletion = meteredConsumptionLiters + draftedVolume + + local newLevel = math.max(0.0, currentLevelLiters - totalDepletion) + return math.floor(newLevel * 100) / 100 +end diff --git a/FiveM/nyx_services/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql b/FiveM/nyx_services/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql new file mode 100644 index 0000000..da082f2 --- /dev/null +++ b/FiveM/nyx_services/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql @@ -0,0 +1,99 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_properties, nyx_treasury & nyx_vehicles +-- Migration: 067_municipal_water_utility_and_hydrant_pressure.sql +-- Description: Municipal reservoirs, contamination diffusion, pump station power +-- dependencies, and fire hydrant pressure dynamics. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal water reservoirs table +CREATE TABLE IF NOT EXISTS `nyx_water_reservoirs` ( + `reservoir_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `label` VARCHAR(64) NOT NULL, + `water_level_liters` DECIMAL(15, 2) NOT NULL DEFAULT 5000000.00, + `max_capacity_liters` DECIMAL(15, 2) NOT NULL DEFAULT 10000000.00, + `purity_percentage` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `contamination_ppm` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `substation_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_res_substation` (`substation_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal pump stations table +CREATE TABLE IF NOT EXISTS `nyx_water_pump_stations` ( + `pump_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `substation_id` VARCHAR(32) NOT NULL, + `pump_status` ENUM('ONLINE', 'OFFLINE_POWER', 'FAULT') NOT NULL DEFAULT 'ONLINE', + `flow_rate_lps` DECIMAL(8, 2) NOT NULL DEFAULT 150.00, + `power_draw_kw` DECIMAL(8, 2) NOT NULL DEFAULT 750.00, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_pump_res` (`reservoir_id`), + INDEX `idx_pump_sub` (`substation_id`), + INDEX `idx_pump_status` (`pump_status`), + CONSTRAINT `fk_pump_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create municipal fire hydrants table +CREATE TABLE IF NOT EXISTS `nyx_fire_hydrants` ( + `hydrant_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `pressure_psi` DECIMAL(6, 2) NOT NULL DEFAULT 85.00, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `last_inspected_at` TIMESTAMP NULL DEFAULT NULL, + INDEX `idx_hydrant_res` (`reservoir_id`), + INDEX `idx_hydrant_status` (`is_active`), + CONSTRAINT `fk_hydrant_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Create water contamination incident log table +CREATE TABLE IF NOT EXISTS `nyx_water_contamination_events` ( + `event_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `contaminant_type` VARCHAR(64) NOT NULL, + `ppm_added` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `reported_by` VARCHAR(64) NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_contam_res` (`reservoir_id`), + INDEX `idx_contam_time` (`created_at` DESC), + CONSTRAINT `fk_contam_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Seed default reservoirs +INSERT INTO `nyx_water_reservoirs` + (`reservoir_id`, `label`, `water_level_liters`, `max_capacity_liters`, `purity_percentage`, `contamination_ppm`, `substation_id`, `coords_x`, `coords_y`, `coords_z`) +VALUES + ('res_vinewood_lake', 'Vinewood Hills Main Reservoir', 8500000.00, 10000000.00, 100.00, 0.00, 'grid_central_ls', 212.4500, 715.3200, 185.1000), + ('res_zancudo_aquifer', 'Fort Zancudo Subterranean Aquifer', 6000000.00, 8000000.00, 99.50, 0.50, 'grid_zancudo', -2250.1000, 3210.4500, 15.2000), + ('res_paleto_basin', 'Paleto Bay Coastal Basin', 4500000.00, 5000000.00, 100.00, 0.00, 'grid_paleto', -280.5000, 6210.8000, 30.0000) +ON DUPLICATE KEY UPDATE `label` = VALUES(`label`); + +-- Seed default pump stations +INSERT INTO `nyx_water_pump_stations` + (`pump_id`, `reservoir_id`, `substation_id`, `pump_status`, `flow_rate_lps`, `power_draw_kw`) +VALUES + ('pump_vinewood_01', 'res_vinewood_lake', 'grid_central_ls', 'ONLINE', 250.00, 1200.00), + ('pump_zancudo_01', 'res_zancudo_aquifer', 'grid_zancudo', 'ONLINE', 180.00, 900.00), + ('pump_paleto_01', 'res_paleto_basin', 'grid_paleto', 'ONLINE', 120.00, 600.00) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +-- Seed default fire hydrants +INSERT INTO `nyx_fire_hydrants` + (`hydrant_id`, `reservoir_id`, `coords_x`, `coords_y`, `coords_z`, `pressure_psi`, `is_active`) +VALUES + ('hydrant_vinewood_01', 'res_vinewood_lake', 220.1000, 700.5000, 180.2000, 85.00, 1), + ('hydrant_legion_01', 'res_vinewood_lake', 150.0000, -900.0000, 30.0000, 90.00, 1), + ('hydrant_zancudo_01', 'res_zancudo_aquifer', -2200.0000, 3200.0000, 15.0000, 80.00, 1), + ('hydrant_paleto_01', 'res_paleto_basin', -270.0000, 6200.0000, 30.0000, 75.00, 1) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_treasury/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql b/FiveM/nyx_treasury/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql new file mode 100644 index 0000000..da082f2 --- /dev/null +++ b/FiveM/nyx_treasury/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql @@ -0,0 +1,99 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_properties, nyx_treasury & nyx_vehicles +-- Migration: 067_municipal_water_utility_and_hydrant_pressure.sql +-- Description: Municipal reservoirs, contamination diffusion, pump station power +-- dependencies, and fire hydrant pressure dynamics. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal water reservoirs table +CREATE TABLE IF NOT EXISTS `nyx_water_reservoirs` ( + `reservoir_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `label` VARCHAR(64) NOT NULL, + `water_level_liters` DECIMAL(15, 2) NOT NULL DEFAULT 5000000.00, + `max_capacity_liters` DECIMAL(15, 2) NOT NULL DEFAULT 10000000.00, + `purity_percentage` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `contamination_ppm` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `substation_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_res_substation` (`substation_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal pump stations table +CREATE TABLE IF NOT EXISTS `nyx_water_pump_stations` ( + `pump_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `substation_id` VARCHAR(32) NOT NULL, + `pump_status` ENUM('ONLINE', 'OFFLINE_POWER', 'FAULT') NOT NULL DEFAULT 'ONLINE', + `flow_rate_lps` DECIMAL(8, 2) NOT NULL DEFAULT 150.00, + `power_draw_kw` DECIMAL(8, 2) NOT NULL DEFAULT 750.00, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_pump_res` (`reservoir_id`), + INDEX `idx_pump_sub` (`substation_id`), + INDEX `idx_pump_status` (`pump_status`), + CONSTRAINT `fk_pump_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create municipal fire hydrants table +CREATE TABLE IF NOT EXISTS `nyx_fire_hydrants` ( + `hydrant_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `coords_x` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_y` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `coords_z` DECIMAL(10, 4) NOT NULL DEFAULT 0.0000, + `pressure_psi` DECIMAL(6, 2) NOT NULL DEFAULT 85.00, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `last_inspected_at` TIMESTAMP NULL DEFAULT NULL, + INDEX `idx_hydrant_res` (`reservoir_id`), + INDEX `idx_hydrant_status` (`is_active`), + CONSTRAINT `fk_hydrant_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Create water contamination incident log table +CREATE TABLE IF NOT EXISTS `nyx_water_contamination_events` ( + `event_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `reservoir_id` VARCHAR(32) NOT NULL, + `contaminant_type` VARCHAR(64) NOT NULL, + `ppm_added` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `reported_by` VARCHAR(64) NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_contam_res` (`reservoir_id`), + INDEX `idx_contam_time` (`created_at` DESC), + CONSTRAINT `fk_contam_reservoir` FOREIGN KEY (`reservoir_id`) REFERENCES `nyx_water_reservoirs`(`reservoir_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Seed default reservoirs +INSERT INTO `nyx_water_reservoirs` + (`reservoir_id`, `label`, `water_level_liters`, `max_capacity_liters`, `purity_percentage`, `contamination_ppm`, `substation_id`, `coords_x`, `coords_y`, `coords_z`) +VALUES + ('res_vinewood_lake', 'Vinewood Hills Main Reservoir', 8500000.00, 10000000.00, 100.00, 0.00, 'grid_central_ls', 212.4500, 715.3200, 185.1000), + ('res_zancudo_aquifer', 'Fort Zancudo Subterranean Aquifer', 6000000.00, 8000000.00, 99.50, 0.50, 'grid_zancudo', -2250.1000, 3210.4500, 15.2000), + ('res_paleto_basin', 'Paleto Bay Coastal Basin', 4500000.00, 5000000.00, 100.00, 0.00, 'grid_paleto', -280.5000, 6210.8000, 30.0000) +ON DUPLICATE KEY UPDATE `label` = VALUES(`label`); + +-- Seed default pump stations +INSERT INTO `nyx_water_pump_stations` + (`pump_id`, `reservoir_id`, `substation_id`, `pump_status`, `flow_rate_lps`, `power_draw_kw`) +VALUES + ('pump_vinewood_01', 'res_vinewood_lake', 'grid_central_ls', 'ONLINE', 250.00, 1200.00), + ('pump_zancudo_01', 'res_zancudo_aquifer', 'grid_zancudo', 'ONLINE', 180.00, 900.00), + ('pump_paleto_01', 'res_paleto_basin', 'grid_paleto', 'ONLINE', 120.00, 600.00) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +-- Seed default fire hydrants +INSERT INTO `nyx_fire_hydrants` + (`hydrant_id`, `reservoir_id`, `coords_x`, `coords_y`, `coords_z`, `pressure_psi`, `is_active`) +VALUES + ('hydrant_vinewood_01', 'res_vinewood_lake', 220.1000, 700.5000, 180.2000, 85.00, 1), + ('hydrant_legion_01', 'res_vinewood_lake', 150.0000, -900.0000, 30.0000, 90.00, 1), + ('hydrant_zancudo_01', 'res_zancudo_aquifer', -2200.0000, 3200.0000, 15.0000, 80.00, 1), + ('hydrant_paleto_01', 'res_paleto_basin', -270.0000, 6200.0000, 30.0000, 75.00, 1) +ON DUPLICATE KEY UPDATE `reservoir_id` = VALUES(`reservoir_id`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/tests/test_municipal_water_utility_and_hydrant_pressure.py b/tests/test_municipal_water_utility_and_hydrant_pressure.py new file mode 100644 index 0000000..14339ac --- /dev/null +++ b/tests/test_municipal_water_utility_and_hydrant_pressure.py @@ -0,0 +1,42 @@ +import os +import json +import pytest + +MIGRATION_FILE = "FiveM/nyx_core/sql/migrations/067_municipal_water_utility_and_hydrant_pressure.sql" +INDEX_FILE = "FiveM/nyx_core/sql/migrations/index.json" +SHARED_WATER_FILE = "FiveM/nyx_services/shared/water_utility.lua" +SERVER_WATER_FILE = "FiveM/nyx_services/server/water_utility.lua" + +def test_migration_file_exists_and_registered(): + assert os.path.exists(MIGRATION_FILE) + with open(INDEX_FILE, "r") as f: + data = json.load(f) + assert "067_municipal_water_utility_and_hydrant_pressure.sql" in data + +def test_sql_schema_content(): + with open(MIGRATION_FILE, "r") as f: + sql = f.read() + assert "CREATE TABLE IF NOT EXISTS `nyx_water_reservoirs`" in sql + assert "CREATE TABLE IF NOT EXISTS `nyx_water_pump_stations`" in sql + assert "CREATE TABLE IF NOT EXISTS `nyx_fire_hydrants`" in sql + assert "CREATE TABLE IF NOT EXISTS `nyx_water_contamination_events`" in sql + assert "res_vinewood_lake" in sql + assert "pump_vinewood_01" in sql + assert "hydrant_vinewood_01" in sql + +def test_shared_lua_formulas(): + with open(SHARED_WATER_FILE, "r") as f: + lua = f.read() + assert "function Nyx.WaterUtility.CalculateHydrantPressure" in lua + assert "function Nyx.WaterUtility.CalculateContaminationDiffusion" in lua + assert "function Nyx.WaterUtility.CalculatePurificationTreatment" in lua + assert "function Nyx.WaterUtility.CalculateWaterConsumption" in lua + +def test_server_water_utility_logic(): + with open(SERVER_WATER_FILE, "r") as f: + lua = f.read() + assert "Nyx.Services.ContaminateReservoir" in lua + assert "Nyx.Services.PurifyReservoir" in lua + assert "Nyx.Services.DraftFromHydrant" in lua + assert "OFFLINE_POWER" in lua + assert "services:draftHydrant" in lua