Passed
Push — main ( 6b9c84...d238e2 )
by Etienne
01:40
created

startGame(boolean)   C

Complexity

Conditions 10

Size

Total Lines 60
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 29
c 1
b 0
f 0
dl 0
loc 60
rs 5.9999
cc 10

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like de.kleiner3.lasertag.mixin.MinecraftServerMixin.startGame(boolean) often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
package de.kleiner3.lasertag.mixin;
2
3
import com.google.gson.Gson;
4
import de.kleiner3.lasertag.LasertagConfig;
5
import de.kleiner3.lasertag.LasertagMod;
6
import de.kleiner3.lasertag.block.entity.LaserTargetBlockEntity;
7
import de.kleiner3.lasertag.item.Items;
8
import de.kleiner3.lasertag.item.LasertagVestItem;
9
import de.kleiner3.lasertag.item.LasertagWeaponItem;
10
import de.kleiner3.lasertag.lasertaggame.ILasertagGame;
11
import de.kleiner3.lasertag.lasertaggame.ITickable;
12
import de.kleiner3.lasertag.lasertaggame.PlayerDeactivatedManager;
13
import de.kleiner3.lasertag.lasertaggame.statistics.GameStats;
14
import de.kleiner3.lasertag.lasertaggame.statistics.StatsCalculator;
15
import de.kleiner3.lasertag.lasertaggame.timing.GameTickTimerTask;
16
import de.kleiner3.lasertag.networking.NetworkingConstants;
17
import de.kleiner3.lasertag.networking.server.ServerEventSending;
18
import de.kleiner3.lasertag.types.Colors;
19
import de.kleiner3.lasertag.util.Tuple;
20
import de.kleiner3.lasertag.util.serialize.LasertagColorSerializer;
21
import io.netty.buffer.Unpooled;
22
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
23
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
24
import net.minecraft.entity.EquipmentSlot;
25
import net.minecraft.entity.player.PlayerEntity;
26
import net.minecraft.item.ItemStack;
27
import net.minecraft.network.PacketByteBuf;
28
import net.minecraft.server.MinecraftServer;
29
import net.minecraft.server.network.ServerPlayerEntity;
30
import net.minecraft.server.world.ServerWorld;
31
import net.minecraft.util.math.BlockPos;
32
import net.minecraft.world.World;
33
import org.spongepowered.asm.mixin.Mixin;
34
import org.spongepowered.asm.mixin.Shadow;
35
import org.spongepowered.asm.mixin.injection.At;
36
import org.spongepowered.asm.mixin.injection.Inject;
37
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
38
39
import java.util.*;
40
import java.util.concurrent.Executors;
41
import java.util.concurrent.ScheduledExecutorService;
42
import java.util.concurrent.TimeUnit;
43
44
/**
45
 * Interface injection into MinecraftServer to implement the lasertag game
46
 *
47
 * @author Étienne Muser
48
 */
49
@Mixin(MinecraftServer.class)
50
public abstract class MinecraftServerMixin implements ILasertagGame, ITickable {
51
    @Shadow
52
    public abstract ServerWorld getOverworld();
53
54
    private HashMap<Colors.Color, ArrayList<BlockPos>> spawnpointCache = null;
55
56
    /**
57
     * Map every player to their team color
58
     */
59
    private final HashMap<Colors.Color, List<PlayerEntity>> teamMap = new HashMap<>();
60
61
    private final StatsCalculator statsCalculator = new StatsCalculator(teamMap);
62
63
    private List<LaserTargetBlockEntity> lasertargetsToReset = new LinkedList<>();
64
65
    private boolean isRunning = false;
66
67
    private ScheduledExecutorService gameTickTimer = null;
68
69
    /**
70
     * Inject into constructor of MinecraftServer
71
     *
72
     * @param ci The CallbackInfo
73
     */
74
    @Inject(method = "<init>", at = @At("TAIL"))
75
    private void init(CallbackInfo ci) {
76
77
        // Initialize team map
78
        for (Colors.Color color : Colors.colorConfig.values()) {
79
            teamMap.put(color, new LinkedList<>());
80
        }
81
    }
82
83
    //region ILasertagGame
84
85
    @Override
86
    public void startGame(boolean scanSpawnpoints) {
87
        // Reset all scores
88
        for (List<PlayerEntity> team : teamMap.values()) {
89
            for (PlayerEntity player : team) {
90
                player.resetLasertagScore();
91
            }
92
        }
93
        notifyPlayersAboutUpdate();
94
95
        // If spawnpoint cache needs to be filled
96
        if (spawnpointCache == null || scanSpawnpoints) {
97
            initSpawnpointCache();
98
        }
99
100
        // Get world
101
        World world = ((MinecraftServer) (Object) this).getOverworld();
102
103
        // Teleport players
104
        // TODO: Give Error message and abort if a player is in a team without spawnpoints
105
        for (Colors.Color teamColor : Colors.colorConfig.values()) {
106
            List<PlayerEntity> team = teamMap.get(teamColor);
107
108
            for (PlayerEntity player : team) {
109
                // Get spawnpoints
110
                List<BlockPos> spawnpoints = spawnpointCache.get(teamColor);
111
112
                // If there are spawnpoints for this team
113
                if (spawnpoints.size() > 0) {
114
                    int idx = world.getRandom().nextInt(spawnpoints.size());
115
116
                    BlockPos destination = spawnpoints.get(idx);
117
                    player.requestTeleport(destination.getX() + 0.5, destination.getY() + 1, destination.getZ() + 0.5);
118
                }
119
            }
120
        }
121
122
        // Start game
123
        isRunning = true;
124
125
        var preGameDelayTimer = Executors.newSingleThreadScheduledExecutor();
126
        var preGameDelay = LasertagConfig.getInstance().getStartTime();
127
        preGameDelayTimer.schedule(() -> {
128
129
            // Activate every player
130
            for (List<PlayerEntity> team : teamMap.values()) {
131
                for (PlayerEntity player : team) {
132
                    PlayerDeactivatedManager.activate(player.getUuid(), world);
133
                    player.onActivated();
134
                }
135
            }
136
137
            // Start game tick timer
138
            gameTickTimer = Executors.newSingleThreadScheduledExecutor();
139
            gameTickTimer.scheduleAtFixedRate(new GameTickTimerTask(this), 0, 1, TimeUnit.MINUTES);
140
141
        }, preGameDelay, TimeUnit.SECONDS);
142
143
        // Notify players
144
        sendGameStartedEvent();
145
    }
146
147
    @Override
148
    public List<PlayerEntity> getPlayersOfTeam(Colors.Color color) {
149
        return teamMap.get(color);
150
    }
151
152
    @Override
153
    public void playerJoinTeam(Colors.Color newTeamColor, PlayerEntity player) {
154
        // Get new team
155
        var newTeam = teamMap.get(newTeamColor);
156
157
        // Check if team is full
158
        if (newTeam.size() >= LasertagConfig.getInstance().getMaxTeamSize()) {
159
            // If is Server
160
            if (player instanceof ServerPlayerEntity) {
161
                ServerEventSending.sendErrorMessageToClient((ServerPlayerEntity) player, "Team " + newTeamColor.getName() + " is full.");
162
            }
163
            return;
164
        }
165
166
        // Check if player is in a team already
167
        Colors.Color oldTeamColor = null;
168
        for (Colors.Color c : Colors.colorConfig.values()) {
169
            if (teamMap.get(c).contains(player)) {
170
                oldTeamColor = c;
171
                break;
172
            }
173
        }
174
175
        // If player has no team
176
        if (oldTeamColor == null) {
177
            newTeam.add(player);
178
        } else {
179
            // If player tries to join his team again
180
            if (newTeamColor == oldTeamColor) return;
181
182
            // Swap team
183
            teamMap.get(oldTeamColor).remove(player);
184
            newTeam.add(player);
185
        }
186
187
        // Set team on player
188
        player.setTeam(newTeamColor);
189
190
        // Get players inventory
191
        var inventory = player.getInventory();
192
193
        // Clear players inventory
194
        inventory.clear();
195
196
        // Give player a lasertag vest
197
        var vestStack = new ItemStack(Items.LASERTAG_VEST);
198
        ((LasertagVestItem) Items.LASERTAG_VEST).setColor(vestStack, newTeamColor.getValue());
199
        player.equipStack(EquipmentSlot.CHEST, vestStack);
200
201
        // Give player a lasertag weapon
202
        var weaponStack = new ItemStack(Items.LASERTAG_WEAPON);
203
        ((LasertagWeaponItem) Items.LASERTAG_WEAPON).setColor(weaponStack, newTeamColor.getValue());
204
        ((LasertagWeaponItem) Items.LASERTAG_WEAPON).setDeactivated(weaponStack, true);
205
        inventory.setStack(0, weaponStack);
206
207
        // Notify about change
208
        notifyPlayersAboutUpdate();
209
    }
210
211
    @Override
212
    public void playerLeaveHisTeam(PlayerEntity player) {
213
        // For each team
214
        for (List<PlayerEntity> team : teamMap.values()) {
215
            // If the player is in the team
216
            if (team.contains(player)) {
217
                // Leave the team
218
                team.remove(player);
219
                player.setTeam(null);
220
                notifyPlayersAboutUpdate();
221
                return;
222
            }
223
        }
224
    }
225
226
    @Override
227
    public void onPlayerScored(PlayerEntity player, int score) {
228
        player.increaseScore(score);
229
230
        notifyPlayersAboutUpdate();
231
    }
232
233
    @Override
234
    public boolean isLasertagGameRunning() {
235
        return isRunning;
236
    }
237
238
    @Override
239
    public void syncTeamsAndScoresToPlayer(ServerPlayerEntity player) {
240
        var simplifiedTeamMap = buildSimplifiedTeamMap();
241
242
        // Serialize team map to json
243
        String messagesString = new Gson().toJson(simplifiedTeamMap);
244
245
        // Create packet buffer
246
        PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer());
247
248
        // Write team map string to buffer
249
        buf.writeString(messagesString);
250
251
        ServerPlayNetworking.send(player, NetworkingConstants.LASERTAG_GAME_TEAM_OR_SCORE_UPDATE, buf);
252
    }
253
254
    @Override
255
    public void registerLasertarget(LaserTargetBlockEntity target) {
256
        lasertargetsToReset.add(target);
257
    }
258
259
    //endregion
260
261
    //region ITickable
262
263
    /**
264
     * This method is called every minute when the game is running
265
     */
266
    @Override
267
    public void doTick() {
268
        System.out.println("Tick");
269
        // TODO
270
    }
271
272
    @Override
273
    public void endTick() {
274
        gameTickTimer.shutdown();
275
        gameTickTimer = null;
276
        lasertagGameOver();
277
    }
278
279
    //endregion
280
281
    //region Private methods
282
283
    /**
284
     * This method is called when the game ends
285
     */
286
    private void lasertagGameOver() {
287
        isRunning = false;
288
289
        sendGameOverEvent();
290
291
        // Get world
292
        World world = getOverworld();
293
294
        // Deactivate every player
295
        for (List<PlayerEntity> team : teamMap.values()) {
296
            for (PlayerEntity player : team) {
297
                PlayerDeactivatedManager.deactivate(player, world, true);
298
                player.onDeactivated();
299
            }
300
        }
301
302
        // Teleport players back to spawn
303
        for (var team : teamMap.values()) {
304
            for (var player : team) {
305
                player.requestTeleport(0.5F, 1, 0.5F);
306
            }
307
        }
308
309
        // Reset lasertargets
310
        for (var target : lasertargetsToReset) {
311
            target.reset();
312
        }
313
        lasertargetsToReset = new LinkedList<>();
314
315
        // Calculate stats
316
        statsCalculator.calcStats();
317
318
        // Create packet
319
        var buf = new PacketByteBuf(Unpooled.buffer());
320
321
        // Get last games stats
322
        var stats = statsCalculator.getLastGamesStats();
323
324
        // Get gson builder
325
        var gsonBuilder = LasertagColorSerializer.getSerializer();
326
327
        // To json
328
        var jsonString = gsonBuilder.create().toJson(stats, GameStats.class);
329
330
        // Write to buffer
331
        buf.writeString(jsonString);
332
333
        // Send statistics to clients
334
        ServerEventSending.sendToEveryone(getOverworld(), NetworkingConstants.GAME_STATISTICS, buf);
335
336
    }
337
338
    /**
339
     * Notifies every player of this world about a team or score update
340
     */
341
    private void notifyPlayersAboutUpdate() {
342
        var simplifiedTeamMap = buildSimplifiedTeamMap();
343
344
        // Serialize team map to json
345
        String messagesString = new Gson().toJson(simplifiedTeamMap);
346
347
        // Create packet buffer
348
        PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer());
349
350
        // Write team map string to buffer
351
        buf.writeString(messagesString);
352
353
        // Send to all clients
354
        ServerEventSending.sendToEveryone(((MinecraftServer) (Object) this).getOverworld(), NetworkingConstants.LASERTAG_GAME_TEAM_OR_SCORE_UPDATE, buf);
355
    }
356
357
    private HashMap<String, List<Tuple<String, Integer>>> buildSimplifiedTeamMap() {
358
        // TODO: Crashes the game when trying to join team in LAN World
359
        // Create simplified team map
360
        final HashMap<String, List<Tuple<String, Integer>>> simplifiedTeamMap = new HashMap<>();
361
362
        // For each color
363
        for (Colors.Color c : Colors.colorConfig.values()) {
364
            // Create a new list of (player name, player score) tuples
365
            List<Tuple<String, Integer>> playerDatas = new LinkedList<>();
366
367
            // For every player in the team
368
            for (PlayerEntity player : teamMap.get(c)) {
369
                // Add his name and score to the list
370
                playerDatas.add(new Tuple<>(player.getDisplayName().getString(), player.getLasertagScore()));
371
            }
372
373
            // Add the current team to the simplified team map
374
            simplifiedTeamMap.put(c.getName(), playerDatas);
375
        }
376
377
        return simplifiedTeamMap;
378
    }
379
380
    /**
381
     * Notifies every player of this world about the start of a lasertag game
382
     */
383
    private void sendGameStartedEvent() {
384
        ServerWorld world = ((MinecraftServer) (Object) this).getOverworld();
385
        ServerEventSending.sendToEveryone(world, NetworkingConstants.GAME_STARTED, PacketByteBufs.empty());
386
    }
387
388
    private void sendGameOverEvent() {
389
        ServerWorld world = ((MinecraftServer) (Object) this).getOverworld();
390
        ServerEventSending.sendToEveryone(world, NetworkingConstants.GAME_OVER, PacketByteBufs.empty());
391
    }
392
393
    /**
394
     * Initializes the spawnpoint cache. Searches a 31 x 31 chunk area for spawnpoint blocks specified by the team color.
395
     * This method is computationally intensive, don't call too often or when responsiveness is important. The call of this method blocks the server from ticking!
396
     */
397
    private void initSpawnpointCache() {
398
399
        // Initialize cache
400
        spawnpointCache = new HashMap<>();
401
402
        // Initialize team lists
403
        for (Colors.Color team : Colors.colorConfig.values()) {
404
            spawnpointCache.put(team, new ArrayList<>());
405
        }
406
407
        // Get the overworld
408
        ServerWorld world = ((MinecraftServer) (Object) this).getOverworld();
409
410
        // Start time measurement
411
        long startTime = System.nanoTime();
412
413
        // Iterate over blocks and find spawnpoints
414
        world.fastSearchBlock((block, pos) -> {
415
            for (Colors.Color color : Colors.colorConfig.values()) {
416
                if (color.getSpawnpointBlock().equals(block)) {
417
                    var team = spawnpointCache.get(color);
418
                    synchronized (color) {
419
                        team.add(pos);
420
                    }
421
                    break;
422
                }
423
            }
424
        }, (currChunk, maxChunk) -> {
425
            // Only send a progress update every second chunk to not ddos our players
426
            if (currChunk % 2 == 0) {
427
                return;
428
            }
429
430
            // Create packet buffer
431
            PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer());
432
433
            // Write progress to buffer
434
            buf.writeDouble((double) currChunk / (double) maxChunk);
435
436
            ServerEventSending.sendToEveryone(world, NetworkingConstants.PROGRESS, buf);
437
        });
438
439
        // Stop time measurement
440
        long stopTime = System.nanoTime();
441
        double duration = (stopTime - startTime) / 1000000000.0F;
442
        LasertagMod.LOGGER.info("Spawnpoint search took " + duration + "s.");
443
    }
444
445
    //endregion
446
}