Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mission selector, mod status colours #144

Draft
wants to merge 18 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
29d3219
Make gradlew executable
TheTimidShade Oct 19, 2024
7eab399
Fix failing export service test
TheTimidShade Oct 19, 2024
cb10b06
Add colors to mod status
TheTimidShade Oct 19, 2024
03719af
Add first iteration of scenarios form for Arma 3 server
TheTimidShade Oct 20, 2024
5c6c535
Rename Arma3ScenariosForm to Arma3ScenarioRotationForm
TheTimidShade Oct 20, 2024
dec8ee1
Remove unused imports
TheTimidShade Oct 20, 2024
838dd21
Refactor Arma 3 scenario autocomplete into separate component
TheTimidShade Oct 20, 2024
7292888
Add reordering controls to Arma 3 scenario rotation form
TheTimidShade Oct 20, 2024
b164409
Change reordering icons to chevrons
TheTimidShade Oct 20, 2024
567449d
Swap elements in array instead of using array.map
TheTimidShade Oct 21, 2024
ecfa131
Change remove scenario button to X instead of trash bin
TheTimidShade Oct 21, 2024
0246ce4
Update Arma 3 server form to store scenario rotation as string instea…
TheTimidShade Oct 21, 2024
6ee2964
Update Arma 3 server DTO and entity in backend
TheTimidShade Oct 21, 2024
1211d9f
Add schema migration for scenario selection
TheTimidShade Nov 3, 2024
6c6faff
Update scenario_rotation column in schema to be not null and use defa…
TheTimidShade Nov 3, 2024
36f2735
Fix getting blank scenario when splitting empty scenario rotation string
TheTimidShade Nov 3, 2024
8dd417d
Update Arma 3 server freemarker template to include scenario rotation
TheTimidShade Nov 3, 2024
b332967
Expand Arma 3 scenario rotation form with scenario parameter selectio…
TheTimidShade Nov 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class Arma3ServerDto implements ServerDto {
private boolean vonEnabled;
private boolean verifySignatures;

private String scenarioRotation;

private String additionalOptions;

private List<ServerWorkshopModDto> activeMods;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public class Arma3Server extends Server {
private boolean vonEnabled;
private boolean verifySignatures;

private String scenarioRotation;

@Column(columnDefinition = "LONGTEXT")
private String additionalOptions;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE arma3server
ADD scenario_rotation VARCHAR(2048) NOT NULL DEFAULT '';
17 changes: 17 additions & 0 deletions backend/src/main/resources/templates/serverConfigArma3.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,21 @@ onUnsignedData = "kick (_this select 0)";
onHackedData = "kick (_this select 0)";
onDifferentData = "";

<#if scenarioRotation?has_content>
// SCENARIO ROTATION
<#assign scenarios = scenarioRotation?split(";")>
<#assign index = 0>
class Missions
{
<#list scenarios as scenario>
class Mission${index}
{
template = "${scenario?replace(".pbo", "")}";
difficulty = "CUSTOM";
};
<#assign index = index + 1>
</#list>
};
</#if>

${additionalOptions!}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ void whenExportModPresetToFile_thenCorrectByteArrayReturned() throws IOException
InOrder inOrder = inOrder(freeMarkerConfigurer, configuration, template);
inOrder.verify(freeMarkerConfigurer).getConfiguration();
inOrder.verify(configuration).getTemplate(TEMPLATE_FILE_NAME);
inOrder.verify(template).setNumberFormat("computer");
inOrder.verify(template).process(eq(expectedModel), any());
verifyNoMoreInteractions(freeMarkerConfigurer, configuration, template);
}
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/mods/ModsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,21 @@ const ModsTable = (props: ModsTableProps) => {

if (status === "INSTALLATION_IN_PROGRESS") {
if (modItemInfo?.status === SteamCmdStatus.IN_QUEUE) {
return <Tooltip title="In queue"><HourglassBottomIcon/></Tooltip>;
return <Tooltip title="In queue"><HourglassBottomIcon color='info'/></Tooltip>;
}
if (modItemInfo?.status === SteamCmdStatus.FINISHED) {
return <Tooltip title="Waiting for installation"><DownloadDoneIcon/></Tooltip>;
return <Tooltip title="Waiting for installation"><DownloadDoneIcon color='info'/></Tooltip>;
}

return <CircularProgress size={20}/>;
}
if (status === "ERROR") {
return <Tooltip
title={workshopErrorStatusMap.get(ErrorStatus[error as keyof typeof ErrorStatus])}><ReportProblemIcon/></Tooltip>
title={workshopErrorStatusMap.get(ErrorStatus[error as keyof typeof ErrorStatus])}><ReportProblemIcon color='error'/></Tooltip>
}

if (status === "FINISHED") {
return <Tooltip title="Installed"><CheckIcon/></Tooltip>;
return <Tooltip title="Installed"><CheckIcon color='success'/></Tooltip>;
}
};

Expand Down
175 changes: 175 additions & 0 deletions frontend/src/components/servers/Arma3ScenarioRotationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Add, ArrowDropDown, Close, KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, AutocompleteValue, Button, Card, Chip, Grid, IconButton, Stack, TextField, Typography } from "@mui/material";
import { FormikProps } from "formik";
import { useState } from "react";
import { Arma3ScenarioDto } from "../../dtos/Arma3ScenarioDto";
import { Arma3ServerDto } from "../../dtos/ServerDto";
import { Arma3ScenariosAutocomplete } from "./Arma3ScenariosAutocomplete";

type Arma3ScenarioRotationFormProps = {
formik: FormikProps<Arma3ServerDto>
}

export const Arma3ScenarioRotationForm = ({ formik }: Arma3ScenarioRotationFormProps) => {
const [selectedScenario, setSelectedScenario] = useState<string>();

const getSplitScenarios = () => {
return formik.values.scenarioRotation.split(";").filter(scenario => scenario.trim().length > 0);
}

const setScenarios = (scenarios: string[]) => {
formik.setFieldValue("scenarioRotation", scenarios.join(";"));
}

const setScenarioAutocomplete = (_: any, value: AutocompleteValue<Arma3ScenarioDto | string, false, false, true> | null): void => {
if (!value) {
setSelectedScenario("");
return;
}
setSelectedScenario(typeof value === "string" ? value : value.name);
};

const addScenario = (scenarioName: string) => {
if (!scenarioName) {
return;
}

setScenarios([...getSplitScenarios(), scenarioName]);
}

const removeScenario = (scenarioName: string) => {
if (!scenarioName) {
return;
}

setScenarios(getSplitScenarios().filter(scenario => scenario !== scenarioName));
}

const shiftScenarioUp = (index: number) => {
let scenarios = getSplitScenarios();
if (index <= 0 || index >= scenarios.length) {
return;
}

let temp = scenarios[index];
scenarios[index] = scenarios[index - 1];
scenarios[index - 1] = temp;

setScenarios(scenarios);
}

const shiftScenarioDown = (index: number) => {
let scenarios = getSplitScenarios();
if (index < 0 || index >= scenarios.length - 1) {
return;
}

let temp = scenarios[index];
scenarios[index] = scenarios[index + 1];
scenarios[index + 1] = temp;

setScenarios(scenarios);
}

return (
<Grid container spacing={1}>
<Grid item xs={12}>
<Typography>Scenario rotation</Typography>
</Grid>
<Grid item xs={12}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={1}
sx={{ flexGrow: 1 }}>
<Arma3ScenariosAutocomplete onChange={setScenarioAutocomplete} />
<Button
variant="contained"
startIcon={<Add />}
onClick={() => addScenario(selectedScenario ?? "")}
disabled={selectedScenario ? Boolean(getSplitScenarios().find((scenario) => scenario === selectedScenario)) : true}>
Add
</Button>
</Stack>
</Grid>
<Grid item xs={12}>
{getSplitScenarios().length > 0 &&
<Card>
{getSplitScenarios().map((value, i) => {
return (
<Accordion >
<AccordionSummary expandIcon={<ArrowDropDown />} >
<Stack direction="row" alignItems="center" justifyContent="space-between" flex="1" paddingRight={1}>
<Typography>{value}</Typography>
<Stack direction="row">
<IconButton
disabled={i <= 0}
aria-label="shift-up"
onClick={() => shiftScenarioUp(i)}>
<KeyboardArrowUp />
</IconButton>
<IconButton
disabled={i >= getSplitScenarios().length - 1}
aria-label="shift-down"
onClick={() => shiftScenarioDown(i)}>
<KeyboardArrowDown />
</IconButton>
<IconButton
aria-label="remove"
onClick={() => removeScenario(value)}>
<Close />
</IconButton>
</Stack>
</Stack>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={1}>
<Grid item xs={5}>
<TextField
fullWidth
id="param-name"
type="text"
label="Param name"
size="small">
</TextField>
</Grid>
<Grid item xs={5}>
<TextField
fullWidth
id="param-value"
type="number"
label="Param value"
size="small">
</TextField>
</Grid>
<Grid item xs={2}>
<Button aria-label="add" variant="contained" startIcon={<Add />}>
<Typography>Add</Typography>
</Button>
</Grid>
<Grid item xs={12}>
<Stack direction="row" spacing={1} rowGap={1} flexWrap="wrap" >
{[
{name: "testParam1", value: 123},
{name: "test2", value: 1},
{name: "testParameterLong3", value: 1512}
].map((param) => {
return (
<Chip label={`${param.name} = ${param.value}`} onDelete={() => {}} />
);
})}
</Stack>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
)
})}
</Card>
}
</Grid>
</Grid>
);
};

63 changes: 63 additions & 0 deletions frontend/src/components/servers/Arma3ScenariosAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Autocomplete, AutocompleteValue, Box, TextField } from "@mui/material";
import React, { useEffect, useState } from "react";
import { Arma3ScenarioDto } from "../../dtos/Arma3ScenarioDto.ts";
import { getScenarios } from "../../services/scenarioService.ts";

type Arma3ScenariosAutocompleteProps = {
onChange: (_: any, value: AutocompleteValue<Arma3ScenarioDto | string, false, false, true> | null) => void,
};

export function Arma3ScenariosAutocomplete({onChange}: Arma3ScenariosAutocompleteProps) {
const [scenarios, setScenarios] = useState<Array<Arma3ScenarioDto>>([]);

useEffect(() => {
const fetchScenarios = async () => {
const { data: scenariosDto } = await getScenarios();
setScenarios(scenariosDto.scenarios);
};

fetchScenarios();
onChange(null, "");
}, []);

const getScenarioDisplayName = (scenario: Arma3ScenarioDto) => {
return scenario.name; // TOOD: Can make pretty later
};

const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
onChange(null, e.target.value);
};

return <Autocomplete
id="scenario-select"
fullWidth
freeSolo
selectOnFocus
onBlur={handleBlur}
handleHomeEndKeys
options={scenarios}
autoHighlight
getOptionLabel={(option) => {
if (typeof option === "string")
return option;
return option.name;
}}
onChange={onChange}
renderOption={(props, option) => (
<Box component="li" {...props}>
{getScenarioDisplayName(option)}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
name="scenarioName"
label="Scenario name"
inputProps={{
...params.inputProps,
autoComplete: 'new-password'
}}
/>
)}
/>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ServerSettingsFormControls} from "./ServerSettingsFormControls.tsx";
import Arma3NetworkSettingsForm from "./Arma3NetworkSettingsForm.tsx";
import {useState} from "react";
import {CustomLaunchParametersInput} from "./CustomLaunchParametersInput.tsx";
import {Arma3ScenarioRotationForm} from "./Arma3ScenarioRotationForm.tsx";

type EditArma3ServerSettingsFormProps = {
server: Arma3ServerDto,
Expand All @@ -27,6 +28,7 @@ const EditArma3ServerSettingsForm = (props: EditArma3ServerSettingsFormProps) =>

values.customLaunchParameters = [...launchParameters];
props.onSubmit(values);
console.log(values);
}

const formik = useFormik<Arma3ServerDto>({
Expand Down Expand Up @@ -66,6 +68,11 @@ const EditArma3ServerSettingsForm = (props: EditArma3ServerSettingsFormProps) =>
<SwitchField id='persistent' label='Persistent' formik={formik}/>
</FormGroup>
</Grid>

<Grid item xs={6}>
<Arma3ScenarioRotationForm formik={formik}/>
</Grid>

<CustomTextField id='additionalOptions' label='Additional options' multiline
formik={formik} containerMd={12}/>

Expand Down
1 change: 1 addition & 0 deletions frontend/src/dtos/ServerDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface Arma3ServerDto extends ServerDto {
verifySignatures: boolean,
activeMods: Array<ServerWorkshopModDto>,
activeDLCs: Array<CreatorDlcDto>,
scenarioRotation: string,
additionalOptions: string,
difficultySettings: Arma3DifficultySettings,
networkSettings: Arma3NetworkSettings | null
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/NewServerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const NewServerPage = () => {
type,
activeMods: [],
activeDLCs: [],
scenarios: []
}

try {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/initialServerStateCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function arma3ServerInitialState(): Arma3ServerDto {
verifySignatures: true,
activeMods: [],
activeDLCs: [],
scenarioRotation: "",
additionalOptions: `headlessClients[] = {"127.0.0.1"};
localClient[] = { "127.0.0.1"};`,
difficultySettings: {
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.