Skip to content

Commit

Permalink
Introduce additional semantics
Browse files Browse the repository at this point in the history
  • Loading branch information
Garanas committed May 11, 2024
1 parent dbb816b commit 7d4ef66
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 199 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.faforever.commons.replay.semantics.records;
package com.faforever.commons.replay;

import lombok.Data;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.faforever.commons.replay.semantics.records;
package com.faforever.commons.replay;

import java.time.Duration;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.faforever.commons.replay;

import com.faforever.commons.replay.body.Event;
import com.faforever.commons.replay.header.Source;

/**
* Combines the tick and source of an event. The tick represents when the event was registered. The source represents who authorised the event.
*
* @param tick
* @param source
* @param event
* @see Event
*/
public record RegisteredEvent(int tick, Source source, Event event) {
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.faforever.commons.replay;

import com.faforever.commons.replay.body.Event;
import com.faforever.commons.replay.header.ReplayHeader;
import com.faforever.commons.replay.semantics.records.TrackedEvent;

import java.util.List;

public record ReplayContainer(ReplayMetadata metadata, ReplayHeader header, List<TrackedEvent> trackedEvents) {
/**
* A container of all the information that a replay may hold once parsed.
*
* @param metadata
* @param header
* @param registeredEvents
*/
public record ReplayContainer(ReplayMetadata metadata, ReplayHeader header, List<RegisteredEvent> registeredEvents) {
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.faforever.commons.replay;

import com.faforever.commons.replay.body.Event;
import com.faforever.commons.replay.semantics.records.ChatMessage;
import com.faforever.commons.replay.semantics.records.ModeratorEvent;
import com.faforever.commons.replay.shared.LuaTable;
import com.faforever.commons.replay.shared.LuaData;
import com.faforever.commons.replay.body.ReplayBodyParser;
import com.faforever.commons.replay.body.ReplayBodyToken;
import com.faforever.commons.replay.body.ReplayBodyTokenizer;
Expand Down Expand Up @@ -330,19 +328,19 @@ private void interpretEvents(List<Event> events) {
}

case Event.LuaSimCallback(
String func, LuaTable.Table parametersLua, Event.CommandUnits commandUnits
String func, LuaData.Table parametersLua, Event.CommandUnits commandUnits
) when func.equals("GiveResourcesToPlayer") -> {
parseGiveResourcesToPlayer(parametersLua);
}

case Event.LuaSimCallback(
String func, LuaTable.Table parametersLua, Event.CommandUnits commandUnits
String func, LuaData.Table parametersLua, Event.CommandUnits commandUnits
) when func.equals("ModeratorEvent") -> {
parseModeratorEvent(parametersLua, player);
}

case Event.LuaSimCallback(
String func, LuaTable parametersLua, Event.CommandUnits commandUnits
String func, LuaData parametersLua, Event.CommandUnits commandUnits
) -> {

}
Expand All @@ -355,11 +353,11 @@ private void interpretEvents(List<Event> events) {
}
}

private void parseGiveResourcesToPlayer(LuaTable.Table lua) {
private void parseGiveResourcesToPlayer(LuaData.Table lua) {
if (lua.value().containsKey("Msg") && lua.value().containsKey("From") && lua.value().containsKey("Sender")) {

// TODO: use the command source (player value) instead of the values from the callback. The values from the callback can be manipulated
if (!(lua.value().get("From") instanceof LuaTable.Number(float luaFromArmy))) {
if (!(lua.value().get("From") instanceof LuaData.Number(float luaFromArmy))) {
return;
}

Expand All @@ -368,20 +366,20 @@ private void parseGiveResourcesToPlayer(LuaTable.Table lua) {
return;
}

if (!(lua.value().get("Msg") instanceof LuaTable.Table(Map<String, LuaTable> luaMsg))) {
if (!(lua.value().get("Msg") instanceof LuaData.Table(Map<String, LuaData> luaMsg))) {
return;
}

if (!(lua.value().get("Sender") instanceof LuaTable.String(String luaSender))) {
if (!(lua.value().get("Sender") instanceof LuaData.String(String luaSender))) {
return;
}

// This can either be a player name or a Map of something, in which case it's actually giving resources
if (!(luaMsg.get("to") instanceof LuaTable.String(String luaMsgReceiver))) {
if (!(luaMsg.get("to") instanceof LuaData.String(String luaMsgReceiver))) {
return;
}

if (!(luaMsg.get("text") instanceof LuaTable.String(String luaMsgText))) {
if (!(luaMsg.get("text") instanceof LuaData.String(String luaMsgText))) {
return;
}

Expand All @@ -393,18 +391,18 @@ private void parseGiveResourcesToPlayer(LuaTable.Table lua) {
}


void parseModeratorEvent(LuaTable.Table lua, Integer player) {
void parseModeratorEvent(LuaData.Table lua, Integer player) {
String messageContent = null;
String playerNameFromArmy = null;
String playerNameFromCommandSource = null;
Integer activeCommandSource = null;
Integer fromArmy = null;

if (lua.value().get("Message") instanceof LuaTable.String(String luaMessage)) {
if (lua.value().get("Message") instanceof LuaData.String(String luaMessage)) {
messageContent = luaMessage;
}

if (lua.value().get("From") instanceof LuaTable.Number(float luaFrom)) {
if (lua.value().get("From") instanceof LuaData.Number(float luaFrom)) {
fromArmy = (int) luaFrom - 1;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import com.faforever.commons.replay.body.ReplayBodyToken;
import com.faforever.commons.replay.body.ReplayBodyTokenizer;
import com.faforever.commons.replay.header.*;
import com.faforever.commons.replay.semantics.Semantics;
import com.faforever.commons.replay.semantics.records.TrackedEvent;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.io.BaseEncoding;
import com.google.common.io.LittleEndianDataInputStream;
Expand All @@ -18,6 +16,7 @@
import org.jetbrains.annotations.NotNull;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
Expand All @@ -32,43 +31,43 @@
public class ReplayLoader {

@Contract(pure = true)
private static ReplayHeader loadSCFAReplayHeader(LittleEndianDataInputStream stream) throws IOException{
private static ReplayHeader loadSCFAReplayHeader(LittleEndianDataInputStream stream) throws IOException {
ReplayHeaderToken headerToken = ReplayHeaderTokenizer.tokenize(stream);
return ReplayHeaderParser.parseHeader(headerToken);
}

@Contract(pure = true)
private static @NotNull List<TrackedEvent> loadSCFAReplayBody(List<Source> sources, LittleEndianDataInputStream stream) throws IOException{
private static @NotNull List<RegisteredEvent> loadSCFAReplayBody(List<Source> sources, LittleEndianDataInputStream stream) throws IOException {
List<ReplayBodyToken> bodyTokens = ReplayBodyTokenizer.tokenize(stream);
List<Event> bodyEvents = ReplayBodyParser.parseTokens(bodyTokens);
return Semantics.registerEvents(sources, bodyEvents);
return ReplaySemantics.registerEvents(sources, bodyEvents);
}

@Contract(pure = true)
private static ReplayContainer loadSCFAReplayFromMemory(ReplayMetadata metadata, byte[] scfaReplayBytes) throws IOException {
try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream((new ByteArrayInputStream(scfaReplayBytes)))) {
ReplayHeader replayHeader = loadSCFAReplayHeader(stream);
List<TrackedEvent> replayBody = loadSCFAReplayBody(replayHeader.sources(), stream);
List<RegisteredEvent> replayBody = loadSCFAReplayBody(replayHeader.sources(), stream);

if(stream.available() > 0) {
if (stream.available() > 0) {
throw new EOFException();
}

return new ReplayContainer(metadata, replayHeader, replayBody);
}
}

public static ReplayContainer loadSCFAReplayFromDisk(Path scfaReplayFile) throws IOException, IllegalArgumentException {
public static ReplayContainer loadSCFAReplayFromDisk(Path scfaReplayFile) throws IOException, IllegalArgumentException {
if (!scfaReplayFile.toString().toLowerCase().endsWith("scfareplay")) {
throw new IllegalArgumentException ("Unknown file format: " + scfaReplayFile.getFileName());
throw new IllegalArgumentException("Unknown file format: " + scfaReplayFile.getFileName());
}

byte[] bytes = Files.readAllBytes(scfaReplayFile);
return loadSCFAReplayFromMemory(null, bytes);
}

@Contract(pure = true)
private static ReplayContainer loadFAFReplayFromMemory(byte[] fafReplayBytes) throws IOException, CompressorException {
private static ReplayContainer loadFAFReplayFromMemory(byte[] fafReplayBytes) throws IOException, CompressorException {
int separator = findSeparatorIndex(fafReplayBytes);
byte[] metadataBytes = Arrays.copyOfRange(fafReplayBytes, 0, separator);
String metadataString = new String(metadataBytes, StandardCharsets.UTF_8);
Expand All @@ -82,9 +81,9 @@ private static ReplayContainer loadFAFReplayFromMemory(byte[] fafReplayBytes) t
return loadSCFAReplayFromMemory(replayMetadata, scfaReplayBytes);
}

public static ReplayContainer loadFAFReplayFromDisk(Path fafReplayFile) throws IOException, CompressorException, IllegalArgumentException {
public static ReplayContainer loadFAFReplayFromDisk(Path fafReplayFile) throws IOException, CompressorException, IllegalArgumentException {
if (!fafReplayFile.toString().toLowerCase().endsWith("fafreplay")) {
throw new IllegalArgumentException ("Unknown file format: " + fafReplayFile.getFileName());
throw new IllegalArgumentException("Unknown file format: " + fafReplayFile.getFileName());
}

byte[] fafReplayBytes = Files.readAllBytes(fafReplayFile);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.faforever.commons.replay;

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

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class ReplaySemantics {

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
* @param events
* @return
*/
public static List<RegisteredEvent> registerEvents(List<Source> sources, List<Event> events) {
final int[] tick = {-1};
final int[] clientId = {-1};

return events.stream().map((event) -> switch (event) {
case Event.Advance e -> {
tick[0] = tick[0] + e.ticksToAdvance();
yield null;
}

case Event.SetCommandSource e -> {
clientId[0] = e.playerIndex();
yield null;
}

default -> new RegisteredEvent(tick[0], sources.get(clientId[0]), event);
}).filter(Objects::nonNull).toList();
}

/**
* 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) {
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.
// Requires refactoring in the game!

case Event.LuaSimCallback(
String func, LuaData.Table callbackTable, Event.CommandUnits commandUnits
) when func.equals("GiveResourcesToPlayer") -> {
// TODO: this field has no meaning and can be manipulated, instead use the authorised command source.
// Requires refactoring in the game!
if (!(callbackTable.value().get("From") instanceof LuaData.Number from)) {
yield null;
}

// focus army starts is 1-based instead of 0-based, to align it we subtract 1
if (from.value() - 1 <= -2) {
yield null;
}

// TODO: this field has no meaning and can be manipulated, instead use the authorised command source.
// Requires refactoring in the game!
if (!(callbackTable.value().get("Sender") instanceof LuaData.String sender)) {
yield null;
}

// TODO: apparently all players create a sim callback that contains the chat message. This hack is how we skip it,
// Requires refactoring in the game!
if (!Objects.equals(sender.value(), registeredEvent.source().name())) {
yield null;
}

if (!(callbackTable.value().get("Msg") instanceof LuaData.Table msgTable)) {
yield null;
}

// TODO: this is 1 out of the 2 legitimate fields
if (!(msgTable.value().get("to") instanceof LuaData.String msgTo)) {
yield null;
}

// TODO: this is 2 out of the 2 legitimate fields
if (!(msgTable.value().get("text") instanceof LuaData.String msgText)) {
yield null;
}

yield new ChatMessage(tickToDuration(registeredEvent.tick()), registeredEvent.source().name(), msgTo.value(), msgText.value());


}
default -> null;
}).filter(Objects::nonNull).toList();
}


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

// TODO: also read other interesting events, such as:
// - Ping creation callbacks

case Event.LuaSimCallback(
String func, LuaData.Table callbackTable, Event.CommandUnits commandUnits
) when func.equals("ModeratorEvent") -> {

String playerNameFromCommandSource = registeredEvent.source().name();
Integer activeCommandSource = registeredEvent.source().sourceId();

String messageContent = null;
String playerNameFromArmy = null;
Integer fromArmy = null;

// This fields only exists to function as a trap - it doesn't actually affect the messaging even though it appears it does so in-game
if ((callbackTable.value().get("From") instanceof LuaData.Number from)) {

// focus army starts is 1-based instead of 0-based, to align it we subtract 1
fromArmy = (int) from.value() - 1;

if (fromArmy != -2) {
Source source = sources.get(fromArmy);

if (source != null) {
playerNameFromArmy = (String) source.name();
}
}
}

if ((callbackTable.value().get("Message") instanceof LuaData.String content)) {
messageContent = content.value();
}

yield new ModeratorEvent(tickToDuration(registeredEvent.tick()), activeCommandSource, fromArmy, messageContent, playerNameFromArmy, playerNameFromCommandSource);
}
default -> null;
}).filter(Objects::nonNull).toList();
}
}
Loading

0 comments on commit 7d4ef66

Please sign in to comment.