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

Add replay semantics that are used by the client #151

Merged
merged 5 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -7,9 +7,9 @@
/**
* A container of all the information that a replay may hold once parsed.
*
* @param metadata
* @param header
* @param registeredEvents
* @param metadata Metadata that is attached to the FAF replay format.
* @param header The header of the replay that represents the scenario.
* @param registeredEvents The events of the replay that represent the input of players.
*/
public record ReplayContainer(ReplayMetadata metadata, ReplayHeader header, List<RegisteredEvent> registeredEvents) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.faforever.commons.replay;

import com.faforever.commons.replay.body.Event;
import com.faforever.commons.replay.header.GameMod;
import com.faforever.commons.replay.header.Source;
import com.faforever.commons.replay.shared.LuaData;

Expand All @@ -9,15 +10,26 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReplaySemantics {

private static final Pattern invalidCharacters = Pattern.compile("[?@*%{}<>|\"]");

/**
* Small utility function to convert a tick to the equivalent game time.
*
* @param tick
* @return
*/
public static Duration tickToDuration(int tick) {
return Duration.ofSeconds(tick / 10);
}

/**
* Registers the events by attaching a tick and a source to them.
*
* @param sources All input sources of the replay
* @param events All events of the replay
* @return All events with the tick and input source attached
Expand All @@ -42,12 +54,44 @@ public static List<RegisteredEvent> registerEvents(List<Source> sources, List<Ev
}

/**
* Retrieves all events that are chat messages
* Retrieves the UID of all mods.
*
* @param replayContainer The replay to retrieve the UIDs from
* @return A list of UIDs
*/
public static List<String> getModUIDs(ReplayContainer replayContainer) {
return replayContainer.header().mods().stream().map(GameMod::uid).toList();
}

/**
* Retrieves the directory that contains the scenario file.
*
* @param replayContainer The replay to retrieve the folder from
* @return The directory that contains the scenario file
*/
public static String getMapFolder (ReplayContainer replayContainer) throws IllegalArgumentException {
// /maps/SCMP_026/SCMP_026_script.lua
String pathToScenario = replayContainer.header().pathToScenario();
if (pathToScenario == null) {
return null;
}

Matcher matcher = invalidCharacters.matcher(pathToScenario);
if (matcher.find()) {
throw new IllegalArgumentException();
}

// SCMP_026
return pathToScenario.split("/")[2];
}

/**
* Retrieves all events that are chat messages.
*
* @param events A list of events
* @return A list of events that are chat messages
*/
public static List<ChatMessage> findChatMessages(List<Source> sources, List<RegisteredEvent> events) {
public static List<ChatMessage> getChatMessages(List<RegisteredEvent> events) {
return events.stream().map((registeredEvent) -> switch (registeredEvent.event()) {

// TODO: the fact that we piggy-back on the 'GiveResourcesToPlayer' callback to embed chat messages is all wrong! We should instead introduce an alternative callback with the sole purpose to send messages.
Expand Down Expand Up @@ -100,12 +144,12 @@ public static List<ChatMessage> findChatMessages(List<Source> sources, List<Regi
}

/**
* Retrieves all events that are moderator related
* Retrieves all events that are related to moderation.
*
* @param events A list of events
* @return A list of events that are moderator related
* @return A list of events that are related to moderation
*/
public static List<ModeratorEvent> findModeratorMessages(List<Source> sources, List<RegisteredEvent> events) {
public static List<ModeratorEvent> getModeratorMessages(List<Source> sources, List<RegisteredEvent> events) {
return events.stream().map((registeredEvent) -> switch (registeredEvent.event()) {

// TODO: also read other interesting events, such as:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ public record ReplayBodyToken(TokenId tokenId, int tokenSize, byte[] tokenConten

public enum TokenId {
// Order is crucial
CMDST_ADVANCE,
CMDST_SET_COMMAND_SOURCE,
CMDST_ADVANCE, // advances the tick for the next tokens in the stream
CMDST_SET_COMMAND_SOURCE, // sets the command source for the next tokens in the stream
CMDST_COMMAND_SOURCE_TERMINATED,
CMDST_VERIFY_CHECKSUM,
CMDST_REQUEST_PAUSE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,28 @@

import java.util.Map;

public record GameScenario(String mapPath, Integer mapVersion, String mapDescription, String mapScript, String mapSave,
String mapName, Integer mapSizeX, Integer mapSizeZ, Integer reclaimMassValue,
Integer reclaimEnergyValue, GameOptions gameOptions,
Map<String, String> modOptions) {
/**
* @param mapScenarioPath "/maps/scmp_026/scmp_026_scenario.lua" , as defined in the game options
* @param mapBinaryPath "/maps/SCMP_026/SCMP_026.scmap" , as defined by the `map` field in `_scenario.lua`
* @param mapVersion Can be null for maps with no versioning (gpg maps)
* @param mapDescription Description of the map
* @param mapScriptPath "/maps/SCMP_026/SCMP_026_script.lua", as defined by the `script` field in `_scenario.lua`
* @param mapSavePath "/maps/SCMP_026/SCMP_026_save.lua", as defined by the `save` field in `_scenario.lua`
* @param mapName Name of the map
* @param mapSizeX Width of the (height)map, does not take into account reductions of playable area via map script
* @param mapSizeZ Height of the (height)map, does not take into account reductions of playable area via map script
* @param reclaimMassValue Total initial mass reclaim value of props, does not take into account wrecks
* @param reclaimEnergyValue Total initial energy reclaim value of props
* @param gameOptions Standard game options
* @param modOptions Options that originate from mods
*/
public record GameScenario(
String mapScenarioPath,
String mapBinaryPath,
Integer mapVersion,
String mapDescription,
String mapScriptPath, String mapSavePath,
String mapName, Integer mapSizeX, Integer mapSizeZ, Integer reclaimMassValue,
Integer reclaimEnergyValue, GameOptions gameOptions,
Map<String, String> modOptions) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,12 @@ public static ReplayHeader parse(LittleEndianDataInputStream dataStream) throws
if (gameScenario instanceof LuaData.Table table) {

// retrieve and manage the game options
String scenarioFile = null;
GameOptions gameOptions = null;
Map<String, String> modOptions = null;
if (table.value().get("Options") instanceof LuaData.Table optionsTable) {

scenarioFile = optionsTable.getString("ScenarioFile");
gameOptions = new GameOptions(
GameOptions.AutoTeams.findByKey(optionsTable.getString("AutoTeams")),
GameOptions.TeamLock.findByKey(optionsTable.getString("TeamLock")),
Expand Down Expand Up @@ -151,6 +153,7 @@ public static ReplayHeader parse(LittleEndianDataInputStream dataStream) throws
}

return new GameScenario(
scenarioFile,
table.getString("map"),
table.getInteger("map_version"),
table.getString("description"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

class LoadReplayLoaderTest {

Expand All @@ -41,8 +43,9 @@ public void parseBinary01() throws CompressorException, IOException {

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("neroxis_map_generator_1.11.0_wvmzfgnlgiebu_bqgaeb3bgzldwstbaa", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(106, chatMessages.size());
}
);
Expand All @@ -58,8 +61,9 @@ public void parseBinary02() throws CompressorException, IOException {

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("SCMP_039", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(2, chatMessages.size());
}
);
Expand All @@ -75,8 +79,9 @@ public void parseBinary03() throws CompressorException, IOException {

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("dualgap_fix_adaptive.v0007", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(1, chatMessages.size());
}
);
Expand All @@ -92,8 +97,9 @@ public void parseBinary04() throws CompressorException, IOException {

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("SCMP_009", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(7, chatMessages.size());
}
);
Expand All @@ -109,8 +115,9 @@ public void parseBinary05() throws CompressorException, IOException {

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("SCMP_026", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(0, chatMessages.size());
}
);
Expand All @@ -126,8 +133,9 @@ public void parseBinary06() throws CompressorException, IOException {

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("SCMP_026", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(0, chatMessages.size());
}
);
Expand All @@ -143,8 +151,9 @@ public void parseBinary07() throws CompressorException, IOException {

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("SCMP_026", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(3, chatMessages.size());
}
);
Expand All @@ -160,10 +169,16 @@ public void parseBinary08() throws CompressorException, IOException {

assertEquals(2, fafReplayContainer.header().mods().size());

List<String> modUIDs = new ArrayList<>();
modUIDs.add("fnewm028-v096-55b4-92b6-64398e7ge43f");
modUIDs.add("d883189d-c556-4d68-b1c8-6ad201b3f7ad");
assertLinesMatch(modUIDs , ReplaySemantics.getModUIDs(fafReplayContainer));

assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("project_tumulus.v0004", ReplaySemantics.getMapFolder(fafReplayContainer));

List<ChatMessage> chatMessages = ReplaySemantics.findChatMessages(fafReplayContainer.header().sources(), fafReplayContainer.registeredEvents());
List<ChatMessage> chatMessages = ReplaySemantics.getChatMessages(fafReplayContainer.registeredEvents());
assertEquals(0, chatMessages.size());
}
);
Expand All @@ -180,10 +195,12 @@ public void compareBinary01() throws CompressorException, IOException {
ReplayContainer fafReplayContainer = ReplayLoader.loadFAFReplayFromDisk(fafReplayFile);
assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("SCMP_026", ReplaySemantics.getMapFolder(fafReplayContainer));

ReplayContainer scfaReplayContainer = ReplayLoader.loadSCFAReplayFromDisk(scfaReplayFile);
assertNoUnprocessedTokens(scfaReplayContainer);
assertNoErrorTokens(scfaReplayContainer);
assertEquals("SCMP_026", ReplaySemantics.getMapFolder(fafReplayContainer));

assertEquals(scfaReplayContainer.registeredEvents().size(), fafReplayContainer.registeredEvents().size());
assertArrayEquals( scfaReplayContainer.registeredEvents().toArray(), fafReplayContainer.registeredEvents().toArray());
Expand All @@ -200,10 +217,12 @@ public void compareBinary02() throws CompressorException, IOException {
ReplayContainer fafReplayContainer = ReplayLoader.loadFAFReplayFromDisk(fafReplayFile);
assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("open_palms_-_faf_version.v0002", ReplaySemantics.getMapFolder(fafReplayContainer));

ReplayContainer scfaReplayContainer = ReplayLoader.loadSCFAReplayFromDisk(scfaReplayFile);
assertNoUnprocessedTokens(scfaReplayContainer);
assertNoErrorTokens(scfaReplayContainer);
assertEquals("open_palms_-_faf_version.v0002", ReplaySemantics.getMapFolder(fafReplayContainer));

assertEquals(scfaReplayContainer.registeredEvents().size(), fafReplayContainer.registeredEvents().size());
assertArrayEquals( scfaReplayContainer.registeredEvents().toArray(), fafReplayContainer.registeredEvents().toArray());
Expand All @@ -221,10 +240,12 @@ public void compareBinary03() throws CompressorException, IOException {
ReplayContainer fafReplayContainer = ReplayLoader.loadFAFReplayFromDisk(fafReplayFile);
assertNoUnprocessedTokens(fafReplayContainer);
assertNoErrorTokens(fafReplayContainer);
assertEquals("SCMP_026", ReplaySemantics.getMapFolder(fafReplayContainer));

ReplayContainer scfaReplayContainer = ReplayLoader.loadSCFAReplayFromDisk(scfaReplayFile);
assertNoUnprocessedTokens(scfaReplayContainer);
assertNoErrorTokens(scfaReplayContainer);
assertEquals("SCMP_026", ReplaySemantics.getMapFolder(fafReplayContainer));

assertEquals(scfaReplayContainer.registeredEvents().size(), fafReplayContainer.registeredEvents().size());
assertArrayEquals( scfaReplayContainer.registeredEvents().toArray(), fafReplayContainer.registeredEvents().toArray());
Expand Down