Passed
Push — main ( c9cf3d...0d512c )
by Etienne
01:21
created

buildTableRow(StringBuilder,int,String,long)   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 8
rs 10
cc 1
1
package de.kleiner3.lasertag.lasertaggame.statistics;
2
3
import com.google.gson.Gson;
4
import de.kleiner3.lasertag.LasertagMod;
5
import de.kleiner3.lasertag.resource.WebResourceManager;
6
import de.kleiner3.lasertag.common.util.FileIO;
7
import de.kleiner3.lasertag.lasertaggame.statistics.mojangsessionaccess.PlayerInfoDto;
8
import de.kleiner3.lasertag.lasertaggame.statistics.mojangsessionaccess.ProfileTextureDto;
9
import de.kleiner3.lasertag.lasertaggame.statistics.mojangsessionaccess.SessionPlayerProfileDto;
10
import net.fabricmc.loader.api.FabricLoader;
11
import net.minecraft.util.Identifier;
12
13
import java.io.IOException;
14
import java.io.InputStreamReader;
15
import java.net.URL;
16
import java.nio.charset.StandardCharsets;
17
import java.nio.file.Files;
18
import java.nio.file.Path;
19
import java.nio.file.StandardCopyOption;
20
import java.util.Base64;
21
22
/**
23
 * Builds a web page to visualize game statistics
24
 *
25
 * @author Étienne Muser
26
 */
27
public class WebStatisticsVisualizer {
28
    private static final Path TARGET_PATH;
29
30
    // Initialize target path
31
    static {
32
        Path target;
33
34
        // I have to use try/catch because for some reason FabricLoader dosn't support checking if it is initialized correctly
35
        try {
36
            target = Path.of(FabricLoader.getInstance().getGameDir().toString(), "lasertag_last_games_stats");
37
        } catch (IllegalStateException ignored) {
38
            target = Path.of(System.getProperty("user.dir"), "build");
39
        }
40
41
        TARGET_PATH = target;
42
    }
43
44
    private static final Identifier REPLACE_ID = new Identifier(LasertagMod.ID, "statistics_go_here");
45
46
    public static String build(GameStats stats, WebResourceManager resourceManager) {
47
        // The path to the generated index.html file
48
        String resultPath = null;
49
50
        // Get web page template from resource manager
51
        var template = resourceManager.getWebSite(new Identifier("web/statistics_template"));
52
53
        // Check
54
        if (template == null) {
55
            return null;
56
        }
57
58
        // Build web page
59
        for (var fileTuple : template) {
60
61
            // if it is the html file
62
            if (fileTuple.x().getPath().endsWith(".html")) {
63
                // Read the file
64
                String fileContents;
65
                try {
66
                    fileContents = FileIO.readAllFile(fileTuple.y().getInputStream());
67
                } catch (IOException ex) {
68
                    LasertagMod.LOGGER.error("Exception in WebStatisticsVisualizer.build:", ex);
69
                    return null;
70
                }
71
72
                fileContents = buildHtml(fileContents, stats);
73
74
                // Write file
75
                resultPath = Path.of(TARGET_PATH.toString(), fileTuple.x().getPath()).toString();
76
                try {
77
                    var ignored = FileIO.createNewFile(resultPath);
78
79
                    Files.writeString(Path.of(resultPath), fileContents);
80
                } catch (IOException ex) {
81
                    LasertagMod.LOGGER.error("Exception in WebStatisticsVisualizer.build:", ex);
82
                    return null;
83
                }
84
85
                continue;
86
            }
87
88
            try {
89
                var copyToPath = Path.of(TARGET_PATH.toString(), fileTuple.x().getPath());
90
91
                var ignored = FileIO.createNewFile(copyToPath.toString());
92
93
                Files.copy(fileTuple.y().getInputStream(), copyToPath, StandardCopyOption.REPLACE_EXISTING);
94
            } catch (Exception ex) {
95
                LasertagMod.LOGGER.error("Failed to copy file '" + fileTuple.y().toString() + "' of statistics visualization.", ex);
96
                return null;
97
            }
98
        }
99
100
        return resultPath;
101
    }
102
103
    private static String buildHtml(String html, GameStats stats) {
104
        // build html to insert
105
        var builder = new StringBuilder();
106
107
        buildTeamScores(builder, stats);
108
109
        buildPlayerScores(builder, stats);
110
111
        buildTeamByPlayersScores(builder, stats);
112
113
        // Find and replace
114
        return html.replaceAll("#" + REPLACE_ID + "#", builder.toString());
115
    }
116
117
    private static void buildTeamScores(StringBuilder builder, GameStats stats) {
118
        buildTableHeader(builder, "Team scores", "Team");
119
120
        int teamNo = 1;
121
        for (var team : stats.teamScores) {
122
            buildTableRow(builder, teamNo++, team.x(), team.y());
123
        }
124
125
        buildTableEnd(builder);
126
    }
127
128
    private static void buildPlayerScores(StringBuilder builder, GameStats stats) {
129
        buildTableHeader(builder, "Player scores", "Player");
130
131
        int playerNo = 1;
132
        for (var player : stats.playerScores) {
133
            var playerHtml = getSkinNameHtml(player.x());
134
135
            buildTableRow(builder, playerNo++, playerHtml, player.y());
136
        }
137
138
        buildTableEnd(builder);
139
    }
140
141
    private static void buildTeamByPlayersScores(StringBuilder builder, GameStats stats) {
142
        for (var team : stats.teamPlayerScores.entrySet()) {
143
            buildTableHeader(builder, team.getKey(), "Player");
144
145
            int playerNo = 1;
146
            for (var player : team.getValue()) {
147
                var playerHtml = getSkinNameHtml(player.x());
148
149
                buildTableRow(builder, playerNo++, playerHtml, player.y());
150
            }
151
152
            buildTableEnd(builder);
153
        }
154
    }
155
156
    private static void buildTableRow(StringBuilder builder, int place, String nameHtml, long score) {
157
        builder.append("<tr><th scope=\"row\">");
158
        builder.append(place);
159
        builder.append("</th><td>");
160
        builder.append(nameHtml);
161
        builder.append("</td><td>");
162
        builder.append(score);
163
        builder.append("</td></tr>");
164
    }
165
166
    private static void buildTableHeader(StringBuilder builder, String tableTitle, String nameColHeader) {
167
        builder.append("<div class=\"container py-5\"><h5 class=\"h5\">");
168
        builder.append(tableTitle);
169
        builder.append("</h5><table class=\"table text-light\"><col width=\"15%\"/><thead><tr><th scope=\"col\">#</th><th scope=\"col\">");
170
        builder.append(nameColHeader);
171
        builder.append("</th><th scope=\"col\">Score</th></tr></thead><tbody>");
172
    }
173
174
    private static void buildTableEnd(StringBuilder builder) {
175
        builder.append("</tbody></table></div>");
176
    }
177
178
    private static String getSkinNameHtml(String playerName) {
179
        try {
180
            var skinUrl = getSkinUrlFromPlayerName(playerName);
181
182
            return  "<div class=\"face-container\"><img src='" + skinUrl + "'></div>\n\n" + playerName;
183
184
        } catch (Exception ignored) {
185
            return "<div class=\"face-container\"></div>" + playerName;
186
        }
187
    }
188
189
    private static String getSkinUrlFromPlayerName(String playerName) throws Exception {
190
        // Url to player info
191
        var playerInfoUrl = new URL("https://api.mojang.com/users/profiles/minecraft/" + playerName);
192
193
        // Create input stream
194
        var playerInfoInputStreamReader = new InputStreamReader(playerInfoUrl.openStream());
195
196
        // Get uuid of player
197
        var uuid = new Gson().fromJson(playerInfoInputStreamReader, PlayerInfoDto.class).id;
198
199
        // Url to session profile of player
200
        var sessionProfileUrl = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
201
202
        // Create input stream
203
        var sessionProfileInputStreamReader = new InputStreamReader(sessionProfileUrl.openStream());
204
205
        // Get base64 encoded texture json
206
        var encodedTextureJson = new Gson().fromJson(sessionProfileInputStreamReader, SessionPlayerProfileDto.class).properties[0].value;
207
208
        // Decode
209
        var decodedTextureJson = new String(Base64.getDecoder().decode(encodedTextureJson), StandardCharsets.UTF_8);
210
211
        // Get skin url
212
        return new Gson().fromJson(decodedTextureJson, ProfileTextureDto.class).textures.get("SKIN").url;
213
    }
214
}
215