Add world settings viewing with legacy fallback

- Store map generation settings in DB when creating new worlds
- Add info button to view settings for each saved world
- Show legacy warning for worlds created before this feature

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alexander Zielonka
2026-01-05 15:51:35 +01:00
parent ff6adb093b
commit be3e915980
2 changed files with 159 additions and 0 deletions

View File

@@ -179,3 +179,9 @@ export async function getFactorioCurrentSave(token) {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}) })
} }
export async function getFactorioWorldSettings(token, saveName) {
return fetchAPI(`/servers/factorio/saves/${encodeURIComponent(saveName)}/settings`, {
headers: { Authorization: `Bearer ${token}` },
})
}

View File

@@ -8,6 +8,7 @@ import {
deleteFactorioTemplate, deleteFactorioTemplate,
createFactorioWorld, createFactorioWorld,
deleteFactorioSave, deleteFactorioSave,
getFactorioWorldSettings,
serverAction serverAction
} from '../api' } from '../api'
import WorldGenForm from './WorldGenForm' import WorldGenForm from './WorldGenForm'
@@ -25,6 +26,9 @@ export default function FactorioWorldManager({ server, token, onServerAction })
const [creating, setCreating] = useState(false) const [creating, setCreating] = useState(false)
const [deleteConfirm, setDeleteConfirm] = useState(null) const [deleteConfirm, setDeleteConfirm] = useState(null)
const [actionLoading, setActionLoading] = useState(null) const [actionLoading, setActionLoading] = useState(null)
const [viewingSettings, setViewingSettings] = useState(null)
const [settingsData, setSettingsData] = useState(null)
const [settingsLoading, setSettingsLoading] = useState(false)
const fetchData = async () => { const fetchData = async () => {
try { try {
@@ -137,6 +141,35 @@ export default function FactorioWorldManager({ server, token, onServerAction })
} }
} }
const handleViewSettings = async (saveName) => {
try {
setSettingsLoading(true)
setViewingSettings(saveName)
const data = await getFactorioWorldSettings(token, saveName)
setSettingsData(data)
} catch (err) {
setError(err.message)
setViewingSettings(null)
} finally {
setSettingsLoading(false)
}
}
const closeSettingsModal = () => {
setViewingSettings(null)
setSettingsData(null)
}
// Format setting value for display
const formatSettingValue = (value) => {
if (value === 0) return 'None'
if (value <= 0.2) return 'Very Low'
if (value <= 0.6) return 'Low'
if (value <= 1.2) return 'Normal'
if (value <= 2.5) return 'High'
return 'Very High'
}
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
@@ -245,6 +278,117 @@ export default function FactorioWorldManager({ server, token, onServerAction })
</div> </div>
)} )}
{/* View Settings Modal */}
{viewingSettings && (
<div className="modal-backdrop" onClick={closeSettingsModal}>
<div className="modal max-w-2xl fade-in-scale" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h3 className="modal-title">World Settings: {viewingSettings}</h3>
<button onClick={closeSettingsModal} className="btn btn-ghost p-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="modal-body">
{settingsLoading ? (
<div className="text-center py-8 text-neutral-400">Loading settings...</div>
) : settingsData?.legacy ? (
<div className="bg-neutral-800/50 rounded-lg p-6 text-center">
<div className="text-amber-400 mb-2"> Legacy World</div>
<p className="text-neutral-400 text-sm">
{settingsData.message}
</p>
</div>
) : settingsData?.settings ? (
<div className="space-y-4">
{settingsData.createdAt && (
<div className="text-neutral-500 text-xs mb-4">
Created: {new Date(settingsData.createdAt).toLocaleString()}
</div>
)}
{/* Terrain Settings */}
<div>
<h4 className="text-sm font-medium text-neutral-300 mb-2">Terrain</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex justify-between p-2 bg-neutral-800/30 rounded">
<span className="text-neutral-400">Water</span>
<span className="text-white">{formatSettingValue(settingsData.settings.autoplace_controls?.water?.frequency)}</span>
</div>
<div className="flex justify-between p-2 bg-neutral-800/30 rounded">
<span className="text-neutral-400">Trees</span>
<span className="text-white">{formatSettingValue(settingsData.settings.autoplace_controls?.trees?.frequency)}</span>
</div>
<div className="flex justify-between p-2 bg-neutral-800/30 rounded">
<span className="text-neutral-400">Cliffs</span>
<span className="text-white">{formatSettingValue(settingsData.settings.cliff_settings?.richness)}</span>
</div>
<div className="flex justify-between p-2 bg-neutral-800/30 rounded">
<span className="text-neutral-400">Starting Area</span>
<span className="text-white">{formatSettingValue(settingsData.settings.starting_area)}</span>
</div>
</div>
</div>
{/* Resource Settings */}
<div>
<h4 className="text-sm font-medium text-neutral-300 mb-2">Resources</h4>
<div className="grid grid-cols-1 gap-2 text-sm">
{['iron-ore', 'copper-ore', 'coal', 'stone', 'uranium-ore', 'crude-oil'].map((resource) => {
const ctrl = settingsData.settings.autoplace_controls?.[resource]
const displayName = resource.replace('-ore', '').replace('-', ' ').replace(/\b\w/g, c => c.toUpperCase())
return (
<div key={resource} className="flex justify-between p-2 bg-neutral-800/30 rounded">
<span className="text-neutral-400">{displayName}</span>
<span className="text-white text-xs">
Freq: {formatSettingValue(ctrl?.frequency)} · Size: {formatSettingValue(ctrl?.size)} · Rich: {formatSettingValue(ctrl?.richness)}
</span>
</div>
)
})}
</div>
</div>
{/* Enemy Settings */}
<div>
<h4 className="text-sm font-medium text-neutral-300 mb-2">Enemies</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex justify-between p-2 bg-neutral-800/30 rounded">
<span className="text-neutral-400">Biter Bases</span>
<span className="text-white">{formatSettingValue(settingsData.settings.autoplace_controls?.['enemy-base']?.frequency)}</span>
</div>
<div className="flex justify-between p-2 bg-neutral-800/30 rounded">
<span className="text-neutral-400">Peaceful Mode</span>
<span className="text-white">{settingsData.settings.peaceful_mode ? 'Yes' : 'No'}</span>
</div>
</div>
</div>
{/* Seed */}
{settingsData.settings.seed !== undefined && (
<div>
<h4 className="text-sm font-medium text-neutral-300 mb-2">Advanced</h4>
<div className="p-2 bg-neutral-800/30 rounded text-sm">
<span className="text-neutral-400">Seed: </span>
<span className="text-white font-mono">{settingsData.settings.seed || 'Random'}</span>
</div>
</div>
)}
</div>
) : (
<div className="text-center py-8 text-neutral-400">No settings data available</div>
)}
</div>
<div className="modal-footer">
<button onClick={closeSettingsModal} className="btn btn-secondary">
Close
</button>
</div>
</div>
</div>
)}
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-neutral-300">Saved Worlds</h3> <h3 className="text-sm font-medium text-neutral-300">Saved Worlds</h3>
@@ -280,6 +424,15 @@ export default function FactorioWorldManager({ server, token, onServerAction })
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button
onClick={() => handleViewSettings(save.name)}
className="btn btn-ghost text-neutral-400 hover:text-white"
title="View world settings"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<button <button
onClick={() => handleStartWithSave(save.name)} onClick={() => handleStartWithSave(save.name)}
disabled={actionLoading === save.name || server.running} disabled={actionLoading === save.name || server.running}