Passed
Push — main ( 0a34c4...a3bd4e )
by Etienne
01:29
created

initSpawnpointCache()   B

Complexity

Conditions 5

Size

Total Lines 46
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 46
rs 8.8853
c 0
b 0
f 0
cc 5
1
package de.kleiner3.lasertag.mixin;
2
3
import com.google.gson.Gson;
4
import com.google.gson.GsonBuilder;
5
import de.kleiner3.lasertag.LasertagMod;
6
import de.kleiner3.lasertag.block.entity.LaserTargetBlockEntity;
7
import de.kleiner3.lasertag.common.types.Tuple;
8
import de.kleiner3.lasertag.common.util.ThreadUtil;
9
import de.kleiner3.lasertag.item.Items;
10
import de.kleiner3.lasertag.item.LasertagVestItem;
11
import de.kleiner3.lasertag.item.LasertagWeaponItem;
12
import de.kleiner3.lasertag.lasertaggame.ILasertagGame;
13
import de.kleiner3.lasertag.lasertaggame.ITickable;
14
import de.kleiner3.lasertag.lasertaggame.management.LasertagGameManager;
15
import de.kleiner3.lasertag.lasertaggame.management.settings.SettingNames;
16
import de.kleiner3.lasertag.lasertaggame.management.team.TeamDto;
17
import de.kleiner3.lasertag.lasertaggame.management.team.serialize.TeamDtoSerializer;
18
import de.kleiner3.lasertag.lasertaggame.statistics.GameStats;
19
import de.kleiner3.lasertag.lasertaggame.statistics.StatsCalculator;
20
import de.kleiner3.lasertag.lasertaggame.timing.GameTickTimerTask;
21
import de.kleiner3.lasertag.networking.NetworkingConstants;
22
import de.kleiner3.lasertag.networking.server.ServerEventSending;
23
import io.netty.buffer.Unpooled;
24
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
25
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
26
import net.minecraft.entity.EquipmentSlot;
27
import net.minecraft.entity.effect.StatusEffect;
28
import net.minecraft.entity.effect.StatusEffectInstance;
29
import net.minecraft.entity.player.PlayerEntity;
30
import net.minecraft.item.ItemStack;
31
import net.minecraft.network.PacketByteBuf;
32
import net.minecraft.server.MinecraftServer;
33
import net.minecraft.server.network.ServerPlayerEntity;
34
import net.minecraft.server.world.ServerWorld;
35
import net.minecraft.util.math.BlockPos;
36
import net.minecraft.world.World;
37
import org.spongepowered.asm.mixin.Mixin;
38
import org.spongepowered.asm.mixin.Shadow;
39
import org.spongepowered.asm.mixin.injection.At;
40
import org.spongepowered.asm.mixin.injection.Inject;
41
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
42
43
import java.util.*;
44
import java.util.concurrent.Executors;
45
import java.util.concurrent.ScheduledExecutorService;
46
import java.util.concurrent.TimeUnit;
47
48
/**
49
 * Interface injection into MinecraftServer to implement the lasertag game
50
 *
51
 * @author Étienne Muser
52
 */
53
@Mixin(MinecraftServer.class)
54
public abstract class MinecraftServerMixin implements ILasertagGame, ITickable {
55
    //region Private fields
56
57
    private HashMap<TeamDto, ArrayList<BlockPos>> spawnpointCache = null;
58
59
    /**
60
     * Map every player to their team
61
     */
62
    private HashMap<TeamDto, List<UUID>> teamMap;
63
64
    private StatsCalculator statsCalculator;
65
66
    private List<LaserTargetBlockEntity> lasertargetsToReset = new LinkedList<>();
67
68
    private boolean isRunning = false;
69
70
    private ScheduledExecutorService gameTickTimer = null;
71
72
    //endregion
73
74
    @Shadow
75
    public abstract ServerWorld getOverworld();
76
77
    @Shadow
78
    public abstract boolean isDedicated();
79
80
    /**
81
     * Inject into constructor of MinecraftServer
82
     *
83
     * @param ci The CallbackInfo
84
     */
85
    @Inject(method = "<init>", at = @At("TAIL"))
86
    private void init(CallbackInfo ci) {
87
88
        // Init team map
89
        teamMap = new HashMap<>();
90
91
        // Initialize team map
92
        for (TeamDto teamDto : LasertagGameManager.getInstance().getTeamManager().teamConfig.values()) {
93
            teamMap.put(teamDto, new LinkedList<>());
94
        }
95
96
        // Init stats calculator
97
        statsCalculator = new StatsCalculator(((MinecraftServer) (Object) this));
98
    }
99
100
    /**
101
     * Inject into the stop method of the minecraft server.
102
     * This method gets called after entering the /stop command or typing stop into the server console.
103
     *
104
     * @param ci
105
     */
106
    @Inject(method = "shutdown", at = @At("HEAD"))
107
    private void atShutdown(CallbackInfo ci) {
108
        // Stop the lasertag game
109
        this.stopLasertagGame();
110
111
        // Dispose the game managers
112
        LasertagGameManager.getInstance().dispose();
113
    }
114
115
    //region ILasertagGame
116
117
    @Override
118
    public Optional<String> startGame(boolean scanSpawnpoints) {
119
        // Reset all scores
120
        LasertagGameManager.getInstance().getScoreManager().resetScores();
121
        notifyPlayersAboutUpdate();
122
123
        // If spawnpoint cache needs to be filled
124
        if (spawnpointCache == null || scanSpawnpoints) {
125
            initSpawnpointCache();
126
        }
127
128
        // Get world
129
        var world = getOverworld();
130
131
        // Check starting conditions
132
        var abortReasons = checkStartingConditions();
133
134
        // If should abort
135
        if (abortReasons.isPresent()) {
136
            // Send abort event to clients
137
            ServerEventSending.sendToEveryone(world, NetworkingConstants.GAME_START_ABORTED, new PacketByteBuf(Unpooled.buffer()));
138
            return abortReasons;
139
        }
140
141
        // Teleport players
142
        for (var teamDto : LasertagGameManager.getInstance().getTeamManager().teamConfig.values()) {
143
            var team = teamMap.get(teamDto);
144
145
            for (var playerUuid : team) {
146
                // Get spawnpoints
147
                var spawnpoints = spawnpointCache.get(teamDto);
148
149
                var player = ((MinecraftServer) (Object) this).getPlayerManager().getPlayer(playerUuid);
150
151
                int idx = world.getRandom().nextInt(spawnpoints.size());
152
153
                var destination = spawnpoints.get(idx);
154
                player.requestTeleport(destination.getX() + 0.5, destination.getY() + 1, destination.getZ() + 0.5);
155
156
                // Give player mining fatigue
157
                player.addStatusEffect(
158
                        new StatusEffectInstance(StatusEffect.byRawId(4),
159
                                (int) (((LasertagGameManager.getInstance().getSettingsManager().<Long>get(SettingNames.PLAY_TIME)) * 60 * 20) +
160
                                        ((LasertagGameManager.getInstance().getSettingsManager().<Long>get(SettingNames.START_TIME)) * 20) + 40),
161
                                Integer.MAX_VALUE,
162
                                false,
163
                                false));
164
165
166
                // Get spawn pos
167
                var spawnPos = new BlockPos(destination.getX(), destination.getY() + 1, destination.getZ());
168
169
                // Set players spawnpoint
170
                player.setSpawnPoint(
171
                        World.OVERWORLD,
172
                        spawnPos, 0.0F, true, false);
173
            }
174
        }
175
176
        // Start game
177
        isRunning = true;
178
179
        var preGameDelayTimer = ThreadUtil.createScheduledExecutor("lasertag-server-pregame-delay-timer-thread-%d");
180
        var preGameDelay = LasertagGameManager.getInstance().getSettingsManager().<Long>get(SettingNames.START_TIME);
181
        preGameDelayTimer.schedule(() -> {
182
183
            // Activate every player
184
            for (var team : teamMap.values()) {
185
                for (var playerUuid : team) {
186
                    LasertagGameManager.getInstance().getDeactivatedManager().activate(playerUuid, world);
187
                    ((MinecraftServer) (Object) this).getPlayerManager().getPlayer(playerUuid).onActivated();
188
                }
189
            }
190
191
            // Start game tick timer
192
            gameTickTimer = ThreadUtil.createScheduledExecutor("lasertag-server-game-tick-timer-thread-%d");
193
            gameTickTimer.scheduleAtFixedRate(new GameTickTimerTask(this), 0, 1, TimeUnit.MINUTES);
194
195
            // Stop the pre game delay timer
196
            ThreadUtil.attemptShutdown(preGameDelayTimer);
197
198
        }, preGameDelay, TimeUnit.SECONDS);
199
200
        // Notify players
201
        sendGameStartedEvent();
202
203
        // If is on dedicated server
204
        if (((MinecraftServer) (Object) this).isDedicated()) {
205
            // Set render data on server
206
            var renderData = LasertagGameManager.getInstance().getHudRenderManager();
207
208
            renderData.progress = 0.0;
209
            renderData.shouldRenderNameTags = false;
210
211
            // Start pregame count down timer
212
            renderData.startPreGameCountdownTimer(LasertagGameManager.getInstance().getSettingsManager().<Long>get(SettingNames.START_TIME));
213
        }
214
215
        return Optional.empty();
216
    }
217
218
    @Override
219
    public boolean stopLasertagGame() {
220
        // If there is no game running
221
        if (!this.isRunning) {
222
            return false;
223
        }
224
225
        // Stop the game
226
        this.dispose();
227
        this.lasertagGameOver();
228
229
        return true;
230
    }
231
232
    @Override
233
    public boolean playerJoinTeam(TeamDto newTeamDto, PlayerEntity player) {
234
        // Get new team
235
        var newTeam = teamMap.get(newTeamDto);
236
237
        // Check if team is full
238
        if (newTeam.size() >= LasertagGameManager.getInstance().getSettingsManager().<Long>get(SettingNames.MAX_TEAM_SIZE)) {
239
            // If is Server
240
            if (player instanceof ServerPlayerEntity) {
241
                ServerEventSending.sendErrorMessageToClient((ServerPlayerEntity) player, "Team " + newTeamDto.name() + " is full.");
242
            }
243
            return false;
244
        }
245
246
        // Check if player is in a team already
247
        TeamDto oldTeamDto = null;
248
        for (var t : LasertagGameManager.getInstance().getTeamManager().teamConfig.values()) {
249
            if (teamMap.get(t).contains(player.getUuid())) {
250
                oldTeamDto = t;
251
                break;
252
            }
253
        }
254
255
        // If player has no team
256
        if (oldTeamDto == null) {
257
            teamMap.get(newTeamDto).add(player.getUuid());
258
        } else {
259
            // If player tries to join his team again
260
            if (newTeamDto == oldTeamDto) {
261
                return true;
262
            }
263
264
            // Swap team
265
            teamMap.get(oldTeamDto).remove(player.getUuid());
266
            teamMap.get(newTeamDto).add(player.getUuid());
267
        }
268
269
        // Set team on player
270
        player.setTeam(newTeamDto);
271
272
        // Get players inventory
273
        var inventory = player.getInventory();
274
275
        // Clear players inventory
276
        inventory.clear();
277
278
        // Give player a lasertag vest
279
        var vestStack = new ItemStack(Items.LASERTAG_VEST);
280
        ((LasertagVestItem) Items.LASERTAG_VEST).setColor(vestStack, newTeamDto.color().getValue());
281
        player.equipStack(EquipmentSlot.CHEST, vestStack);
282
283
        // Give player a lasertag weapon
284
        var weaponStack = new ItemStack(Items.LASERTAG_WEAPON);
285
        ((LasertagWeaponItem) Items.LASERTAG_WEAPON).setColor(weaponStack, newTeamDto.color().getValue());
286
        ((LasertagWeaponItem) Items.LASERTAG_WEAPON).setDeactivated(weaponStack, true);
287
        inventory.setStack(0, weaponStack);
288
289
        // Notify about change
290
        notifyPlayersAboutUpdate();
291
292
        return true;
293
    }
294
295
    @Override
296
    public void playerLeaveHisTeam(PlayerEntity player) {
297
        // For each team
298
        for (var team : teamMap.values()) {
299
            // If the player is in the team
300
            if (team.contains(player.getUuid())) {
301
                // Leave the team
302
                team.remove(player.getUuid());
303
                player.setTeam(null);
304
                notifyPlayersAboutUpdate();
305
                return;
306
            }
307
        }
308
    }
309
310
    @Override
311
    public void onPlayerScored(PlayerEntity player, long score) {
312
        LasertagGameManager.getInstance().getScoreManager().increaseScore(player.getUuid(), score);
313
314
        notifyPlayersAboutUpdate();
315
    }
316
317
    @Override
318
    public boolean isLasertagGameRunning() {
319
        return isRunning;
320
    }
321
322
    @Override
323
    public void syncTeamsAndScoresToPlayer(ServerPlayerEntity player) {
324
        var simplifiedTeamMap = buildSimplifiedTeamMap();
325
326
        // Serialize team map to json
327
        var messagesString = new Gson().toJson(simplifiedTeamMap);
328
329
        // Create packet buffer
330
        var buf = new PacketByteBuf(Unpooled.buffer());
331
332
        // Write team map string to buffer
333
        buf.writeString(messagesString);
334
335
        ServerPlayNetworking.send(player, NetworkingConstants.LASERTAG_GAME_TEAM_OR_SCORE_UPDATE, buf);
336
    }
337
338
    @Override
339
    public void registerLasertarget(LaserTargetBlockEntity target) {
340
        lasertargetsToReset.add(target);
341
    }
342
343
    @Override
344
    public void notifyPlayersAboutUpdate() {
345
        var simplifiedTeamMap = buildSimplifiedTeamMap();
346
347
        // Serialize team map to json
348
        var messagesString = new Gson().toJson(simplifiedTeamMap);
349
350
        // Create packet buffer
351
        var buf = new PacketByteBuf(Unpooled.buffer());
352
353
        // Write team map string to buffer
354
        buf.writeString(messagesString);
355
356
        // Send to all clients
357
        ServerEventSending.sendToEveryone(getOverworld(), NetworkingConstants.LASERTAG_GAME_TEAM_OR_SCORE_UPDATE, buf);
358
359
        // Update simplified team map in local copy of render data
360
        LasertagGameManager.getInstance().getHudRenderManager().teamMap = simplifiedTeamMap;
361
    }
362
363
    @Override
364
    public boolean isPlayerInTeam(ServerPlayerEntity player) {
365
        // For every team
366
        return teamMap
367
                .values()
368
                .stream()
369
                .anyMatch((team) -> team
370
                        .stream()
371
                        .anyMatch((playerUuid -> playerUuid
372
                                .equals(player.getUuid())
373
                        ))
374
                );
375
    }
376
377
    @Override
378
    public void dispose() {
379
        synchronized (this) {
380
            if (gameTickTimer == null) {
381
                return;
382
            }
383
            ThreadUtil.attemptShutdown(gameTickTimer);
384
            gameTickTimer = null;
385
        }
386
    }
387
388
    @Override
389
    public HashMap<String, List<Tuple<String, Long>>> getSimplifiedTeamMap() {
390
        return buildSimplifiedTeamMap();
391
    }
392
393
    @Override
394
    public TeamDto getTeamOfPlayer(UUID playerUuid) {
395
        return teamMap
396
                .entrySet()
397
                .stream()
398
                .filter((team) -> team.getValue().contains(playerUuid))
399
                .map(Map.Entry::getKey)
400
                .findFirst()
401
                .orElse(null);
402
    }
403
404
    //endregion
405
406
    //region ITickable
407
408
    /**
409
     * This method is called every minute when the game is running
410
     */
411
    @Override
412
    public void doTick() {
413
        // Here the music can be started
414
    }
415
416
    @Override
417
    public void endTick() {
418
        synchronized (this) {
419
            ThreadUtil.attemptShutdown(gameTickTimer);
420
            gameTickTimer = null;
421
        }
422
423
        lasertagGameOver();
424
    }
425
426
    //endregion
427
428
    //region Private methods
429
430
    /**
431
     * This method is called when the game ends
432
     */
433
    private void lasertagGameOver() {
434
        isRunning = false;
435
436
        sendGameOverEvent();
437
438
        // Get world
439
        var world = getOverworld();
440
441
        // Deactivate every player
442
        for (var team : teamMap.values()) {
443
            for (var playerUuid : team) {
444
                var player = ((MinecraftServer) (Object) this).getPlayerManager().getPlayer(playerUuid);
445
446
                LasertagGameManager.getInstance().getDeactivatedManager().deactivate(player, world, true);
447
                player.onDeactivated();
448
            }
449
        }
450
451
        // Teleport players back to spawn
452
        for (var team : teamMap.values()) {
453
            for (var playerUuid : team) {
454
                var player = ((MinecraftServer) (Object) this).getPlayerManager().getPlayer(playerUuid);
455
456
                player.requestTeleport(0.5F, 1, 0.5F);
457
458
                // Create block pos
459
                var origin = new BlockPos(0, 1, 0);
460
461
                // Set players spawnpoint
462
                player.setSpawnPoint(
463
                        World.OVERWORLD,
464
                        origin, 0.0F, true, false);
465
466
            }
467
        }
468
469
        // Reset lasertargets
470
        for (var target : lasertargetsToReset) {
471
            target.reset();
472
        }
473
        lasertargetsToReset = new LinkedList<>();
474
475
        try {
476
            // Calculate stats
477
            statsCalculator.calcStats();
478
479
            // Create packet
480
            var buf = new PacketByteBuf(Unpooled.buffer());
481
482
            // Get last games stats
483
            var stats = statsCalculator.getLastGamesStats();
484
485
            // Get gson builder
486
            var gsonBuilder = new GsonBuilder();
487
488
            // Get serializer
489
            var serializer = TeamDtoSerializer.getSerializer();
490
491
            // Register type adapter
492
            gsonBuilder.registerTypeAdapter(TeamDto.class, serializer);
493
494
            // To json
495
            var jsonString = gsonBuilder.create().toJson(stats, GameStats.class);
496
497
            // Write to buffer
498
            buf.writeString(jsonString);
499
500
            // Send statistics to clients
501
            ServerEventSending.sendToEveryone(world, NetworkingConstants.GAME_STATISTICS, buf);
502
        } catch (Exception e) {
503
            LasertagMod.LOGGER.error("ERROR:", e);
504
        }
505
    }
506
507
    private HashMap<String, List<Tuple<String, Long>>> buildSimplifiedTeamMap() {
508
        // Create simplified team map
509
        final HashMap<String, List<Tuple<String, Long>>> simplifiedTeamMap = new HashMap<>();
510
511
        // For each team
512
        for (var t : LasertagGameManager.getInstance().getTeamManager().teamConfig.values()) {
513
            // Create a new list of (player name, player score) tuples
514
            List<Tuple<String, Long>> playerDatas = new LinkedList<>();
515
516
            // For every player in the team
517
            for (var playerUuid : teamMap.get(t)) {
518
                // Add his name and score to the list
519
                playerDatas.add(new Tuple<>(((MinecraftServer) (Object) this).getPlayerManager().getConsistentPlayerUsername(playerUuid),
520
                        LasertagGameManager.getInstance().getScoreManager().getScore(playerUuid)));
521
            }
522
523
            // Add the current team to the simplified team map
524
            simplifiedTeamMap.put(t.name(), playerDatas);
525
        }
526
527
        return simplifiedTeamMap;
528
    }
529
530
    /**
531
     * Notifies every player of this world about the start of a lasertag game
532
     */
533
    private void sendGameStartedEvent() {
534
        var world = getOverworld();
535
        ServerEventSending.sendToEveryone(world, NetworkingConstants.GAME_STARTED, PacketByteBufs.empty());
536
    }
537
538
    private void sendGameOverEvent() {
539
        var world = getOverworld();
540
        ServerEventSending.sendToEveryone(world, NetworkingConstants.GAME_OVER, PacketByteBufs.empty());
541
    }
542
543
    /**
544
     * Initializes the spawnpoint cache. Searches a 31 x 31 chunk area for spawnpoint blocks specified by the team.
545
     * 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!
546
     */
547
    private void initSpawnpointCache() {
548
549
        // Initialize cache
550
        spawnpointCache = new HashMap<>();
551
552
        // Initialize team lists
553
        for (var team : LasertagGameManager.getInstance().getTeamManager().teamConfig.values()) {
554
            spawnpointCache.put(team, new ArrayList<>());
555
        }
556
557
        // Get the overworld
558
        var world = getOverworld();
559
560
        // Start time measurement
561
        var startTime = System.nanoTime();
562
563
        // Iterate over blocks and find spawnpoints
564
        world.fastSearchBlock((block, pos) -> {
565
            for (var teamDto : LasertagGameManager.getInstance().getTeamManager().teamConfig.values()) {
566
                if (teamDto.spawnpointBlock().equals(block)) {
567
                    var team = spawnpointCache.get(teamDto);
568
                    synchronized (teamDto) {
569
                        team.add(pos);
570
                    }
571
                    break;
572
                }
573
            }
574
        }, (currChunk, maxChunk) -> {
575
            // Only send a progress update every second chunk to not ddos our players
576
            if (currChunk % 2 == 0) {
577
                return;
578
            }
579
580
            // Create packet buffer
581
            var buf = new PacketByteBuf(Unpooled.buffer());
582
583
            // Write progress to buffer
584
            buf.writeDouble((double) currChunk / (double) maxChunk);
585
586
            ServerEventSending.sendToEveryone(world, NetworkingConstants.PROGRESS, buf);
587
        });
588
589
        // Stop time measurement
590
        var stopTime = System.nanoTime();
591
        var duration = (stopTime - startTime) / 1000000000.0;
592
        LasertagMod.LOGGER.info("Spawnpoint search took " + duration + "s.");
593
    }
594
595
    /**
596
     * Checks if all starting conditions are met. If the game can start, this method returns an empty optional
597
     * Otherwise it returns the reasons why the game can not start as a string.
598
     * @return
599
     */
600
    private Optional<String> checkStartingConditions() {
601
        boolean abort = false;
602
        var builder = new StringBuilder();
603
604
        // For every team
605
        for (var team : teamMap.entrySet()) {
606
            // If the team contains players
607
            if (team.getValue().size() > 0) {
608
                // Get the spawnpoints for the team
609
                var spawnpoints = spawnpointCache.get(team.getKey());
610
611
                // If the team has no spawnpoints
612
                if (spawnpoints.size() == 0) {
613
                    abort = true;
614
                    builder.append("  *No spawnpoints were found for team '" + team.getKey().name() + "'\n");
615
                }
616
            }
617
        }
618
619
        if (abort) {
620
            return Optional.of(builder.toString());
621
        }
622
623
        return Optional.empty();
624
    }
625
626
    //endregion
627
}