Passed
Branch dev (9bb1d9)
by Etienne
01:47
created

build(GameStats,TeamDto,GameMode,WebResourceManager)   B

Complexity

Conditions 7

Size

Total Lines 55
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 30
dl 0
loc 55
rs 7.76
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
package de.pewpewproject.lasertag.lasertaggame.statistics;
2
3
import com.google.gson.Gson;
4
import de.pewpewproject.lasertag.LasertagMod;
5
import de.pewpewproject.lasertag.common.util.DurationUtils;
6
import de.pewpewproject.lasertag.lasertaggame.gamemode.GameMode;
7
import de.pewpewproject.lasertag.lasertaggame.statistics.mojangsessionaccess.PlayerInfoDto;
8
import de.pewpewproject.lasertag.lasertaggame.statistics.mojangsessionaccess.ProfileTextureDto;
9
import de.pewpewproject.lasertag.lasertaggame.statistics.mojangsessionaccess.SessionPlayerProfileDto;
10
import de.pewpewproject.lasertag.lasertaggame.team.TeamDto;
11
import de.pewpewproject.lasertag.resource.WebResourceManager;
12
import net.fabricmc.loader.api.FabricLoader;
13
import net.minecraft.client.resource.language.I18n;
14
import net.minecraft.util.Identifier;
15
16
import java.io.IOException;
17
import java.io.InputStreamReader;
18
import java.net.URL;
19
import java.nio.charset.StandardCharsets;
20
import java.nio.file.Files;
21
import java.nio.file.Path;
22
import java.nio.file.StandardCopyOption;
23
import java.nio.file.StandardOpenOption;
24
import java.time.Duration;
25
import java.util.Base64;
26
import java.util.stream.Collectors;
27
28
/**
29
 * Builds a web page to visualize game statistics
30
 *
31
 * @author Étienne Muser
32
 */
33
public class WebStatisticsVisualizer {
34
    private static final Path TARGET_PATH;
35
36
    // Initialize target path
37
    static {
38
        Path target;
39
40
        // I have to use try/catch because for some reason FabricLoader dosn't support checking if it is initialized correctly
41
        try {
42
            target = Path.of(FabricLoader.getInstance().getGameDir().toString(), "lasertag_last_games_stats");
43
        } catch (IllegalStateException ignored) {
44
            target = Path.of(System.getProperty("user.dir"), "build");
45
        }
46
47
        TARGET_PATH = target;
48
    }
49
50
    private static final Identifier REPLACE_ID = new Identifier(LasertagMod.ID, "statistics_go_here");
51
52
    public static Path build(GameStats stats, TeamDto winningTeam, GameMode gameMode, WebResourceManager resourceManager) {
53
54
        // The path to the generated index.html file
55
        Path resultPath = null;
56
57
        // Get web page template from resource manager
58
        var template = resourceManager.getWebSite(new Identifier("web/statistics_template"));
59
60
        // Check
61
        if (template == null) {
62
            return null;
63
        }
64
65
        // Build web page
66
        for (var fileTuple : template) {
67
68
            // if it is the html file
69
            if (fileTuple.x().getPath().endsWith(".html")) {
70
                // Read the file
71
                String fileContents;
72
                try {
73
                    fileContents = fileTuple.y().getReader().lines().collect(Collectors.joining());
74
                } catch (IOException ex) {
75
                    LasertagMod.LOGGER.error("Reading of statistics template failed:", ex);
76
                    return null;
77
                }
78
79
                fileContents = buildHtml(fileContents, stats, winningTeam, gameMode);
80
81
                // Write file
82
                resultPath = TARGET_PATH.resolve(fileTuple.x().getPath());
83
                try {
84
85
                    Files.createDirectories(resultPath.getParent());
86
                    Files.writeString(resultPath, fileContents, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
87
                } catch (IOException ex) {
88
                    LasertagMod.LOGGER.error("Writing of statistics file failed:", ex);
89
                    return null;
90
                }
91
92
                continue;
93
            }
94
95
            try {
96
                var copyToPath = TARGET_PATH.resolve(fileTuple.x().getPath());
97
98
                Files.createDirectories(copyToPath.getParent());
99
                Files.copy(fileTuple.y().getInputStream(), copyToPath, StandardCopyOption.REPLACE_EXISTING);
100
            } catch (Exception ex) {
101
                LasertagMod.LOGGER.error("Failed to copy file '" + fileTuple.x().getPath() + "' of statistics visualization.", ex);
102
                return null;
103
            }
104
        }
105
106
        return resultPath;
107
    }
108
109
    private static String buildHtml(String html, GameStats stats, TeamDto winningTeam, GameMode gameMode) {
110
        // build html to insert
111
        var builder = new StringBuilder();
112
113
        buildOverTableHeader(builder, winningTeam, gameMode);
114
115
        buildTeamScores(builder, stats);
116
117
        buildPlayerScores(builder, stats);
118
119
        buildTeamByPlayersScores(builder, stats);
120
121
        // Find and replace
122
        return html.replaceAll("#" + REPLACE_ID + "#", builder.toString());
123
    }
124
125
    private static void buildOverTableHeader(StringBuilder builder, TeamDto winningTeam, GameMode gameMode) {
126
127
        // Get the winning teams color
128
        var winningTeamColor = winningTeam.color();
129
130
        builder.append("<div class=\"container over-table-header\"><div><h1><i>Winner: <span style=\"color:rgb(");
131
132
        // Winner team
133
        builder.append(winningTeamColor.r());
134
        builder.append(",");
135
        builder.append(winningTeamColor.g());
136
        builder.append(",");
137
        builder.append(winningTeamColor.b());
138
        builder.append(");\"><b>");
139
        builder.append(winningTeam.name());
140
        builder.append("</b></span></i></h1></div>");
141
142
        // Game mode
143
        builder.append("<div class=\"game-mode-container\"><h2 class=\"game-mode-text\"><i>");
144
        builder.append(I18n.translate(gameMode.getTranslatableName()));
145
        builder.append("</i></h2></div>");
146
147
        builder.append("</div>");
148
    }
149
150
    private static void buildTeamScores(StringBuilder builder, GameStats stats) {
151
152
        String gameDurationString = null;
153
        if (stats.gameDurationSeconds != 0L) {
154
            gameDurationString = "Game duration: " +
155
                    DurationUtils.toMinuteString(Duration.ofSeconds(stats.gameDurationSeconds)) +
156
                    " minutes";
157
        }
158
159
        buildTableHeader(builder, "Team scores", "Team", gameDurationString);
160
161
        int teamNo = 1;
162
        for (var team : stats.teamScores) {
163
            buildTableRow(builder, teamNo++, team.x(), team.y());
164
        }
165
166
        buildTableEnd(builder);
167
    }
168
169
    private static void buildPlayerScores(StringBuilder builder, GameStats stats) {
170
        buildTableHeader(builder, "Player scores", "Player", null);
171
172
        int playerNo = 1;
173
        for (var player : stats.playerScores) {
174
            var playerHtml = getSkinNameHtml(player.x());
175
176
            buildTableRow(builder, playerNo++, playerHtml, player.y());
177
        }
178
179
        buildTableEnd(builder);
180
    }
181
182
    private static void buildTeamByPlayersScores(StringBuilder builder, GameStats stats) {
183
        for (var team : stats.teamPlayerScores.entrySet()) {
184
            buildTableHeader(builder, team.getKey(), "Player", null);
185
186
            int playerNo = 1;
187
            for (var player : team.getValue()) {
188
                var playerHtml = getSkinNameHtml(player.x());
189
190
                buildTableRow(builder, playerNo++, playerHtml, player.y());
191
            }
192
193
            buildTableEnd(builder);
194
        }
195
    }
196
197
    private static void buildTableRow(StringBuilder builder, int place, String nameHtml, String score) {
198
        builder.append("<tr><th scope=\"row\">");
199
        builder.append(place);
200
        builder.append("</th><td>");
201
        builder.append(nameHtml);
202
        builder.append("</td><td>");
203
        builder.append(score);
204
        builder.append("</td></tr>");
205
    }
206
207
    private static void buildTableHeader(StringBuilder builder, String tableTitle, String nameColHeader, String headerAddition) {
208
        builder.append("<div class=\"container py-5\">");
209
        if (headerAddition != null) {
210
            builder.append("<div class=\"header-addition\">");
211
            builder.append(headerAddition);
212
            builder.append("</div>");
213
        }
214
        builder.append("<h5 class=\"h5\">");
215
        builder.append(tableTitle);
216
        builder.append("</h5><table class=\"table text-light\"><col width=\"15%\"/><thead><tr><th scope=\"col\">#</th><th scope=\"col\">");
217
        builder.append(nameColHeader);
218
        builder.append("</th><th scope=\"col\">Score</th></tr></thead><tbody>");
219
    }
220
221
    private static void buildTableEnd(StringBuilder builder) {
222
        builder.append("</tbody></table></div>");
223
    }
224
225
    private static String getSkinNameHtml(String playerName) {
226
        try {
227
            var skinUrl = getSkinUrlFromPlayerName(playerName);
228
229
            return  "<div class=\"face-container\"><img src='" + skinUrl + "'></div>\n\n" + playerName;
230
231
        } catch (Exception ignored) {
232
            return "<div class=\"face-container\"></div>" + playerName;
233
        }
234
    }
235
236
    private static String getSkinUrlFromPlayerName(String playerName) throws Exception {
237
        // Url to player info
238
        var playerInfoUrl = new URL("https://api.mojang.com/users/profiles/minecraft/" + playerName);
239
240
        // Create input stream
241
        var playerInfoInputStreamReader = new InputStreamReader(playerInfoUrl.openStream());
242
243
        // Get uuid of player
244
        var uuid = new Gson().fromJson(playerInfoInputStreamReader, PlayerInfoDto.class).id;
245
246
        // Url to session profile of player
247
        var sessionProfileUrl = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
248
249
        // Create input stream
250
        var sessionProfileInputStreamReader = new InputStreamReader(sessionProfileUrl.openStream());
251
252
        // Get base64 encoded texture json
253
        var encodedTextureJson = new Gson().fromJson(sessionProfileInputStreamReader, SessionPlayerProfileDto.class).properties[0].value;
254
255
        // Decode
256
        var decodedTextureJson = new String(Base64.getDecoder().decode(encodedTextureJson), StandardCharsets.UTF_8);
257
258
        // Get skin url
259
        return new Gson().fromJson(decodedTextureJson, ProfileTextureDto.class).textures.get("SKIN").url;
260
    }
261
}
262