/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.data.io;
import org.lanternpowered.server.data.persistence.nbt.NbtStreamUtils;
import org.lanternpowered.server.game.Lantern;
import org.lanternpowered.server.game.registry.type.scoreboard.DisplaySlotRegistryModule;
import org.lanternpowered.server.scoreboard.LanternDisplaySlot;
import org.lanternpowered.server.scoreboard.LanternObjective;
import org.lanternpowered.server.scoreboard.LanternScore;
import org.lanternpowered.server.scoreboard.LanternScoreboard;
import org.lanternpowered.server.scoreboard.LanternTeam;
import org.lanternpowered.server.text.LanternTexts;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.MemoryDataContainer;
import org.spongepowered.api.scoreboard.CollisionRule;
import org.spongepowered.api.scoreboard.CollisionRules;
import org.spongepowered.api.scoreboard.Score;
import org.spongepowered.api.scoreboard.Scoreboard;
import org.spongepowered.api.scoreboard.Team;
import org.spongepowered.api.scoreboard.Visibilities;
import org.spongepowered.api.scoreboard.Visibility;
import org.spongepowered.api.scoreboard.critieria.Criteria;
import org.spongepowered.api.scoreboard.critieria.Criterion;
import org.spongepowered.api.scoreboard.objective.Objective;
import org.spongepowered.api.scoreboard.objective.displaymode.ObjectiveDisplayMode;
import org.spongepowered.api.scoreboard.objective.displaymode.ObjectiveDisplayModes;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.format.TextColor;
import org.spongepowered.api.text.format.TextColors;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
public class ScoreboardIO {
private final static String SCOREBOARD_DATA = "scoreboard.dat";
private final static DataQuery DATA = DataQuery.of("data");
private final static DataQuery OBJECTIVES = DataQuery.of("Objectives");
private final static DataQuery OBJECTIVE = DataQuery.of("Objective");
private final static DataQuery EXTRA_OBJECTIVES = DataQuery.of("EObjectives"); // Lantern
private final static DataQuery NAME = DataQuery.of("Name");
private final static DataQuery DISPLAY_NAME = DataQuery.of("DisplayName");
private final static DataQuery CRITERION_NAME = DataQuery.of("CriteriaName");
private final static DataQuery DISPLAY_MODE = DataQuery.of("RenderType");
private final static DataQuery SCORES = DataQuery.of("PlayerScores");
private final static DataQuery SCORE = DataQuery.of("Score");
private final static DataQuery INVALID = DataQuery.of("Invalid"); // Lantern
private final static DataQuery LOCKED = DataQuery.of("Locked");
private final static DataQuery ALLOW_FRIENDLY_FIRE = DataQuery.of("AllowFriendlyFire");
private final static DataQuery CAN_SEE_FRIENDLY_INVISIBLES = DataQuery.of("SeeFriendlyInvisibles");
private final static DataQuery NAME_TAG_VISIBILITY = DataQuery.of("NameTagVisibility");
private final static DataQuery DEATH_MESSAGE_VISIBILITY = DataQuery.of("DeathMessageVisibility");
private final static DataQuery COLLISION_RULE = DataQuery.of("CollisionRule");
private final static DataQuery PREFIX = DataQuery.of("Prefix");
private final static DataQuery SUFFIX = DataQuery.of("Suffix");
private final static DataQuery TEAM_COLOR = DataQuery.of("TeamColor");
private final static DataQuery MEMBERS = DataQuery.of("Players");
private final static DataQuery TEAMS = DataQuery.of("Teams");
private final static DataQuery DISPLAY_SLOTS = DataQuery.of("DisplaySlots");
private final static Pattern DISPLAY_SLOT_PATTERN = Pattern.compile("^slot_([0-9]+)$");
public static Scoreboard read(Path worldFolder) throws IOException {
DataView dataView = IOHelper.<DataView>read(worldFolder.resolve(SCOREBOARD_DATA), file -> {
try {
return NbtStreamUtils.read(Files.newInputStream(file), true);
} catch (IOException e) {
throw new IOException("Unable to access " + file.getFileName() + "!", e);
}
}).orElse(null);
final Scoreboard.Builder scoreboardBuilder = Scoreboard.builder();
if (dataView == null) {
return scoreboardBuilder.build();
}
final Map<String, Objective> objectives = new HashMap<>();
dataView = dataView.getView(DATA).orElseThrow(() -> new IllegalStateException("Unable to find the data compound."));
dataView.getViewList(OBJECTIVES).ifPresent(list -> list.forEach(entry -> {
final String name = entry.getString(NAME).get();
final Text displayName = LanternTexts.fromLegacy(entry.getString(DISPLAY_NAME).get());
final Criterion criterion = Sponge.getRegistry().getType(Criterion.class, entry.getString(CRITERION_NAME).get())
.orElseGet(() -> {
Lantern.getLogger().warn("Unable to find a criterion with id: {}, default to dummy.",
entry.getString(CRITERION_NAME).get());
return Criteria.DUMMY;
});
final ObjectiveDisplayMode objectiveDisplayMode = Sponge.getRegistry().getType(ObjectiveDisplayMode.class,
entry.getString(DISPLAY_MODE).get()).orElseGet(() -> {
Lantern.getLogger().warn("Unable to find a display mode with id: {}, default to integer.",
entry.getString(CRITERION_NAME).get());
return ObjectiveDisplayModes.INTEGER;
});
objectives.put(name, Objective.builder()
.name(name)
.displayName(displayName)
.criterion(criterion)
.objectiveDisplayMode(objectiveDisplayMode)
.build());
}));
dataView.getViewList(SCORES).ifPresent(list -> list.forEach(entry -> {
// The invalid state is added by lantern, it means that there is already
// a other score with the same name and target objective
// We have to keep all the entries to remain compatible with vanilla mc.
if (entry.getInt(INVALID).orElse(0) > 0) {
return;
}
final Text name = LanternTexts.fromLegacy(entry.getString(NAME).get());
final int value = entry.getInt(SCORE).get();
final boolean locked = entry.getInt(LOCKED).orElse(0) > 0; // TODO
final String objectiveName = entry.getString(OBJECTIVE).get();
Score score = null;
Objective objective = objectives.get(objectiveName);
if (objective != null) {
score = addToObjective(objective, null, name, value);
}
final List<String> extraObjectives = entry.getStringList(EXTRA_OBJECTIVES).orElse(null);
if (extraObjectives != null) {
for (String extraObjective : extraObjectives) {
objective = objectives.get(extraObjective);
if (objective != null) {
score = addToObjective(objective, score, name, value);
}
}
}
}));
final List<Team> teams = new ArrayList<>();
dataView.getViewList(TEAMS).ifPresent(list -> list.forEach(entry -> {
final Team.Builder builder = Team.builder()
.allowFriendlyFire(entry.getInt(ALLOW_FRIENDLY_FIRE).orElse(0) > 0)
.canSeeFriendlyInvisibles(entry.getInt(CAN_SEE_FRIENDLY_INVISIBLES).orElse(0) > 0)
.name(entry.getString(NAME).get())
.displayName(LanternTexts.fromLegacy(entry.getString(DISPLAY_NAME).get()))
.prefix(LanternTexts.fromLegacy(entry.getString(PREFIX).get()))
.suffix(LanternTexts.fromLegacy(entry.getString(SUFFIX).get()))
.members(entry.getStringList(MEMBERS).get().stream().map(LanternTexts::fromLegacy).collect(Collectors.toSet()));
entry.getString(NAME_TAG_VISIBILITY).ifPresent(value -> builder.nameTagVisibility(Sponge.getRegistry().getAllOf(Visibility.class)
.stream().filter(visibility -> visibility.getName().equals(value)).findFirst().orElseGet(() -> {
Lantern.getLogger().warn("Unable to find a name tag visibility with id: {}, default to always.", value);
return Visibilities.ALWAYS;
})));
entry.getString(DEATH_MESSAGE_VISIBILITY).ifPresent(value -> builder.deathTextVisibility(Sponge.getRegistry().getAllOf(Visibility.class)
.stream().filter(visibility -> visibility.getName().equals(value)).findFirst().orElseGet(() -> {
Lantern.getLogger().warn("Unable to find a death message visibility with id: {}, default to always.", value);
return Visibilities.ALWAYS;
})));
entry.getString(COLLISION_RULE).ifPresent(value -> builder.collisionRule(Sponge.getRegistry().getAllOf(CollisionRule.class)
.stream().filter(visibility -> visibility.getName().equals(value)).findFirst().orElseGet(() -> {
Lantern.getLogger().warn("Unable to find a collision rule with id: {}, default to never.", value);
return CollisionRules.NEVER;
})));
entry.getString(TEAM_COLOR).ifPresent(color -> {
TextColor textColor = Sponge.getRegistry().getType(TextColor.class, color).orElseGet(() -> {
Lantern.getLogger().warn("Unable to find a team color with id: {}, default to none.", color);
return TextColors.NONE;
});
if (textColor != TextColors.NONE && textColor != TextColors.RESET) {
builder.color(textColor);
}
});
teams.add(builder.build());
}));
final Scoreboard scoreboard = scoreboardBuilder.objectives(new ArrayList<>(objectives.values())).teams(teams).build();
dataView.getView(DISPLAY_SLOTS).ifPresent(displaySlots -> {
for (DataQuery key : displaySlots.getKeys(false)) {
final Matcher matcher = DISPLAY_SLOT_PATTERN.matcher(key.getParts().get(0));
if (matcher.matches()) {
final int internalId = Integer.parseInt(matcher.group(1));
Lantern.getRegistry().getRegistryModule(DisplaySlotRegistryModule.class).get().getByInternalId(internalId).ifPresent(slot -> {
final Objective objective = objectives.get(displaySlots.getString(key).get());
if (objective != null) {
scoreboard.updateDisplaySlot(objective, slot);
}
});
}
}
});
return scoreboard;
}
private static Score addToObjective(Objective objective, @Nullable Score score, Text name, int value) {
if (score == null) {
score = objective.getOrCreateScore(name);
score.setScore(value);
} else {
objective.addScore(score);
}
return score;
}
public static void write(Path folder, Scoreboard scoreboard) throws IOException {
final List<DataView> objectives = scoreboard.getObjectives().stream().map(objective -> new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED)
.set(NAME, objective.getName())
.set(DISPLAY_NAME, ((LanternObjective) objective).getLegacyDisplayName())
.set(CRITERION_NAME, objective.getCriterion().getId())
.set(DISPLAY_MODE, objective.getDisplayMode().getId())).collect(Collectors.toList());
final List<DataView> scores = new ArrayList<>();
for (Score score : scoreboard.getScores()) {
final Iterator<Objective> it = score.getObjectives().iterator();
final DataView baseView = new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED)
.set(NAME, ((LanternScore) score).getLegacyName())
.set(SCORE, score.getScore());
// TODO: Locked state
final DataView mainView = baseView.copy()
.set(OBJECTIVE, it.next().getName());
final List<String> extraObjectives = new ArrayList<>();
while (it.hasNext()) {
final String extraObjectiveName = it.next().getName();
scores.add(baseView.copy()
.set(OBJECTIVE, extraObjectiveName)
.set(INVALID, (byte) 1));
extraObjectives.add(extraObjectiveName);
}
if (!extraObjectives.isEmpty()) {
mainView.set(EXTRA_OBJECTIVES, extraObjectives);
}
}
final List<DataView> teams = new ArrayList<>();
for (Team team : scoreboard.getTeams()) {
final DataView container = new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED)
.set(ALLOW_FRIENDLY_FIRE, (byte) (team.allowFriendlyFire() ? 1 : 0))
.set(CAN_SEE_FRIENDLY_INVISIBLES, (byte) (team.canSeeFriendlyInvisibles() ? 1 : 0))
.set(NAME_TAG_VISIBILITY, team.getNameTagVisibility().getName())
.set(NAME, team.getName())
.set(DISPLAY_NAME, ((LanternTeam) team).getLegacyDisplayName())
.set(DEATH_MESSAGE_VISIBILITY, team.getDeathMessageVisibility().getName())
.set(COLLISION_RULE, team.getCollisionRule().getName())
.set(PREFIX, ((LanternTeam) team).getLegacyPrefix())
.set(SUFFIX, ((LanternTeam) team).getLegacySuffix());
final TextColor teamColor = team.getColor();
if (teamColor != TextColors.NONE) {
container.set(TEAM_COLOR, teamColor.getId());
}
final Set<Text> members = team.getMembers();
container.set(MEMBERS, members.stream().map(LanternTexts::toLegacy).collect(Collectors.toList()));
teams.add(container);
}
final DataContainer rootDataContainer = new MemoryDataContainer(DataView.SafetyMode.NO_DATA_CLONED);
final DataView dataView = rootDataContainer.createView(DATA)
.set(OBJECTIVES, objectives)
.set(SCORES, scores)
.set(TEAMS, teams);
final DataView displaySlots = dataView.createView(DISPLAY_SLOTS);
((LanternScoreboard) scoreboard).getObjectivesInSlot().entrySet().forEach(entry ->
displaySlots.set(DataQuery.of("slot_" + ((LanternDisplaySlot) entry.getKey()).getInternalId()), entry.getValue().getName()));
IOHelper.write(folder.resolve(SCOREBOARD_DATA), file -> {
NbtStreamUtils.write(rootDataContainer, Files.newOutputStream(file), true);
return true;
});
}
}