Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Player join offer related changes #299

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,10 @@ However! Before we give functionality to our brilliant example game, we need to
An example offer listener may look like:
```java
activity.listen(GamePlayerEvents.OFFER, offer -> {
ServerPlayerEntity player = offer.player();
return offer.accept(world, new Vec3d(0.0, 64.0, 0.0))
.and(() -> {
player.changeGameMode(GameMode.ADVENTURE);
});
return offer.accept(world, new Vec3d(0.0, 65.0, 0.0))
.thenRunForEach(player -> {
player.changeGameMode(GameMode.ADVENTURE);
});
});
```

Expand Down Expand Up @@ -271,9 +270,8 @@ public final class ExampleGame {
}

private PlayerOfferResult onPlayerOffer(PlayerOffer offer) {
ServerPlayerEntity player = offer.player();
return offer.accept(this.world, new Vec3d(0.0, 64.0, 0.0))
.and(() -> {
return offer.accept(this.world, new Vec3d(0.0, 65.0, 0.0))
.thenRunForEach(player -> {
player.changeGameMode(GameMode.ADVENTURE);
});
}
Expand Down
10 changes: 3 additions & 7 deletions src/main/java/xyz/nucleoid/plasmid/command/GameCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,9 @@ private static void joinAllPlayersToGame(ServerCommandSource source, GameSpace g
.collect(Collectors.toList());

var intent = JoinIntent.ANY;
var screen = gameSpace.getPlayers().screenJoins(players, intent);
if (screen.isOk()) {
for (var player : players) {
gameSpace.getPlayers().offer(player, intent);
}
} else {
source.sendError(screen.errorCopy().formatted(Formatting.RED));
var result = gameSpace.getPlayers().offer(players, intent);
if (result.isError()) {
source.sendError(result.errorCopy().formatted(Formatting.RED));
}
}

Expand Down
19 changes: 9 additions & 10 deletions src/main/java/xyz/nucleoid/plasmid/game/GameSpacePlayers.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,31 @@
*/
public interface GameSpacePlayers extends PlayerSet {
/**
* Screens a group of players and returns whether the collective group should be allowed into the game.
* Simulates offer to join a player or group of players and returns whether they should be allowed into the game.
* <p>
* This logic is controlled through the active {@link GameActivity} through {@link GamePlayerEvents#SCREEN_JOINS}.
* This logic is controlled through the active {@link GameActivity} through {@link GamePlayerEvents#OFFER}.
*
* @param players the group of players trying to join
* @param intent the intent of the players trying to join, such as whether they want to participate or spectate
* @return a {@link GameResult} describing whether this group can join this game, or an error if not
* @see GamePlayerEvents#SCREEN_JOINS
* @see GameSpacePlayers#offer(ServerPlayerEntity, JoinIntent)
* @return a {@link GameResult} describing whether these players can join this game, or an error if not
* @see GameSpacePlayers#offer(Collection, JoinIntent)
* @see xyz.nucleoid.plasmid.game.player.GamePlayerJoiner
*/
GameResult screenJoins(Collection<ServerPlayerEntity> players, JoinIntent intent);
GameResult simulateOffer(Collection<ServerPlayerEntity> players, JoinIntent intent);

/**
* Offers an individual player to join this game. If accepted, they will be teleported into the game, and if not
* Offers a player or group of players to join this game. If accepted, they will be teleported into the game, and if not
* an error {@link GameResult} will be returned.
* <p>
* This logic is controlled through the active {@link GameActivity} through {@link GamePlayerEvents#OFFER}.
*
* @param player the player trying to join
* @param players the players trying to join
* @param intent the intent of the players trying to join, such as whether they want to participate or spectate
* @return a {@link GameResult} describing whether this player joined the game, or an error if not
* @return a {@link GameResult} describing whether these players joined the game, or an error if not
* @see GamePlayerEvents#OFFER
* @see xyz.nucleoid.plasmid.game.player.GamePlayerJoiner
*/
GameResult offer(ServerPlayerEntity player, JoinIntent intent);
GameResult offer(Collection<ServerPlayerEntity> players, JoinIntent intent);

/**
* Attempts to remove the given {@link ServerPlayerEntity} from this {@link GameSpace}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import xyz.nucleoid.plasmid.game.event.GameActivityEvents;
import xyz.nucleoid.plasmid.game.event.GamePlayerEvents;
import xyz.nucleoid.plasmid.game.manager.GameSpaceManager;
import xyz.nucleoid.plasmid.game.player.PlayerOffer;
import xyz.nucleoid.plasmid.game.player.PlayerOfferResult;
import xyz.nucleoid.plasmid.game.player.JoinOffer;
import xyz.nucleoid.plasmid.game.player.JoinOfferResult;
import xyz.nucleoid.plasmid.game.rule.GameRuleType;
import xyz.nucleoid.plasmid.util.compatibility.AfkDisplayCompatibility;

Expand Down Expand Up @@ -89,7 +89,6 @@ public static GameWaitingLobby addTo(GameActivity activity, PlayerConfig playerC

activity.listen(GameActivityEvents.TICK, lobby::onTick);
activity.listen(GameActivityEvents.REQUEST_START, lobby::requestStart);
activity.listen(GamePlayerEvents.SCREEN_JOINS, (players, intent) -> lobby.screenJoins(players));
activity.listen(GamePlayerEvents.OFFER, lobby::offerPlayer);
activity.listen(GamePlayerEvents.REMOVE, lobby::onRemovePlayer);

Expand Down Expand Up @@ -170,17 +169,9 @@ private GameResult requestStart() {
}
}

private GameResult screenJoins(Collection<GameProfile> players) {
int newPlayerCount = this.gameSpace.getPlayers().size() + players.size();
private JoinOfferResult offerPlayer(JoinOffer offer) {
int newPlayerCount = this.gameSpace.getPlayers().size() + offer.players().size();
if (newPlayerCount > this.playerConfig.maxPlayers()) {
return GameResult.error(GameTexts.Join.gameFull());
}

return GameResult.ok();
}

private PlayerOfferResult offerPlayer(PlayerOffer offer) {
if (this.isFull()) {
return offer.reject(GameTexts.Join.gameFull());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import xyz.nucleoid.plasmid.game.GameSpace;
import xyz.nucleoid.plasmid.game.GameTexts;
import xyz.nucleoid.plasmid.game.player.JoinIntent;
import xyz.nucleoid.plasmid.game.player.PlayerOffer;
import xyz.nucleoid.plasmid.game.player.PlayerOfferResult;
import xyz.nucleoid.plasmid.game.player.JoinOffer;
import xyz.nucleoid.plasmid.game.player.JoinOfferResult;
import xyz.nucleoid.stimuli.event.StimulusEvent;

import java.util.Collection;
Expand Down Expand Up @@ -101,46 +101,22 @@ public final class GamePlayerEvents {
}
});

/**
* Called when a group of players try to join this game. This should be used to reject multiple players as a group,
* such as when a party tries to join but has too many players to fit into the game.
* <p>
* This is called before {@link GamePlayerEvents#OFFER} which handles specifically bringing a player into the game.
*
* @see GamePlayerEvents#OFFER
*/
public static final StimulusEvent<ScreenJoins> SCREEN_JOINS = StimulusEvent.create(ScreenJoins.class, ctx -> (players, intent) -> {
try {
for (var listener : ctx.getListeners()) {
var result = listener.screenJoins(players, intent);
if (result.isError()) {
return result;
}
}
return GameResult.ok();
} catch (Throwable throwable) {
ctx.handleException(throwable);
return GameResult.error(GameTexts.Join.unexpectedError());
}
});

/**
* Called when a single {@link ServerPlayerEntity} tries to join this game. This event is responsible for bringing
* the player into the {@link GameSpace} world in the correct location.
* <p>
* Games must respond to this event in order for a player to be able to join by returning either
* {@link PlayerOffer#accept(ServerWorld, Vec3d)} or {@link PlayerOffer#reject(Text)}.
* {@link JoinOffer#accept(ServerWorld)} or {@link JoinOffer#reject(Text)}.
*
* @see PlayerOffer
* @see PlayerOfferResult
* @see GamePlayerEvents#SCREEN_JOINS
* @see JoinOffer
* @see JoinOfferResult
* @see GamePlayerEvents#JOIN
*/
public static final StimulusEvent<Offer> OFFER = StimulusEvent.create(Offer.class, ctx -> offer -> {
try {
for (var listener : ctx.getListeners()) {
var result = listener.onOfferPlayer(offer);
if (!(result instanceof PlayerOfferResult.Pass)) {
if (!(result instanceof JoinOfferResult.Pass)) {
return result;
}
}
Expand Down Expand Up @@ -175,12 +151,8 @@ public interface Remove {
void onRemovePlayer(ServerPlayerEntity player);
}

public interface ScreenJoins {
GameResult screenJoins(Collection<GameProfile> players, JoinIntent intent);
}

public interface Offer {
PlayerOfferResult onOfferPlayer(PlayerOffer offer);
JoinOfferResult onOfferPlayer(JoinOffer offer);
}

public interface Name {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
import xyz.nucleoid.plasmid.game.event.GameActivityEvents;
import xyz.nucleoid.plasmid.game.event.GamePlayerEvents;
import xyz.nucleoid.plasmid.game.player.JoinIntent;
import xyz.nucleoid.plasmid.game.player.LocalPlayerOffer;
import xyz.nucleoid.plasmid.game.player.PlayerOfferResult;
import xyz.nucleoid.plasmid.game.player.LocalJoinOffer;
import xyz.nucleoid.plasmid.game.player.JoinOfferResult;

import java.util.Collection;
import java.util.Map;
Expand Down Expand Up @@ -196,30 +196,12 @@ public GameBehavior getBehavior() {
return this.state;
}

GameResult screenJoins(Collection<ServerPlayerEntity> players, JoinIntent intent) {
var result = this.attemptScreenJoins(players.stream().map(PlayerEntity::getGameProfile).toList(), intent);

if (result.isError()) {
this.players.attemptGarbageCollection();
}

return result;
}

private GameResult attemptScreenJoins(Collection<GameProfile> players, JoinIntent intent) {
if (this.closed) {
return GameResult.error(GameTexts.Join.gameClosed());
}

return this.state.invoker(GamePlayerEvents.SCREEN_JOINS).screenJoins(players, intent);
}

PlayerOfferResult offerPlayer(LocalPlayerOffer offer) {
JoinOfferResult offerPlayer(LocalJoinOffer offer) {
if (this.closed) {
return offer.reject(GameTexts.Join.gameClosed());
} else if (this.manager.inGame(offer.player())) {
} else if (offer.serverPlayers().stream().anyMatch(this.manager::inGame)) {
return offer.reject(GameTexts.Join.inOtherGame());
} else if (!Permissions.check(offer.player(), "plasmid.join_game", true)) {
} else if (offer.serverPlayers().stream().anyMatch(p -> !Permissions.check(p, "plasmid.join_game", true))) {
return offer.reject(GameTexts.Join.notAllowed());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
import xyz.nucleoid.plasmid.game.GameResult;
import xyz.nucleoid.plasmid.game.GameSpacePlayers;
import xyz.nucleoid.plasmid.game.GameTexts;
import xyz.nucleoid.plasmid.game.player.JoinIntent;
import xyz.nucleoid.plasmid.game.player.LocalPlayerOffer;
import xyz.nucleoid.plasmid.game.player.MutablePlayerSet;
import xyz.nucleoid.plasmid.game.player.PlayerOfferResult;
import xyz.nucleoid.plasmid.game.player.*;
import xyz.nucleoid.plasmid.game.player.isolation.IsolatingPlayerTeleporter;

import java.util.Collection;
Expand All @@ -28,13 +25,23 @@ public final class ManagedGameSpacePlayers implements GameSpacePlayers {
}

@Override
public GameResult screenJoins(Collection<ServerPlayerEntity> players, JoinIntent intent) {
return this.space.screenJoins(players, intent);
public GameResult simulateOffer(Collection<ServerPlayerEntity> players, JoinIntent intent) {
if (players.stream().anyMatch(this.set::contains)) {
return GameResult.error(GameTexts.Join.alreadyJoined());
}

var offer = new LocalJoinOffer(players, intent);

return switch (this.space.offerPlayer(offer)) {
pufmat marked this conversation as resolved.
Show resolved Hide resolved
case JoinOfferResult.Accept accept -> GameResult.ok();
case JoinOfferResult.Reject reject -> GameResult.error(reject.reason());
default -> GameResult.error(GameTexts.Join.genericError());
};
}

@Override
public GameResult offer(ServerPlayerEntity player, JoinIntent intent) {
var result = this.attemptOffer(player, intent);
public GameResult offer(Collection<ServerPlayerEntity> players, JoinIntent intent) {
var result = this.attemptOffer(players, intent);

if (result.isError()) {
this.attemptGarbageCollection();
Expand All @@ -43,26 +50,31 @@ public GameResult offer(ServerPlayerEntity player, JoinIntent intent) {
return result;
}

private GameResult attemptOffer(ServerPlayerEntity player, JoinIntent intent) {
if (this.set.contains(player)) {
private GameResult attemptOffer(Collection<ServerPlayerEntity> players, JoinIntent intent) {
if (players.stream().anyMatch(this.set::contains)) {
return GameResult.error(GameTexts.Join.alreadyJoined());
}

var offer = new LocalPlayerOffer(player, intent);
var offer = new LocalJoinOffer(players, intent);

switch (this.space.offerPlayer(offer)) {
case LocalPlayerOffer.Accept accept -> {
case LocalJoinOffer.Accept accept -> {
try {
this.teleporter.teleportIn(player, accept::applyJoin);
this.set.add(player);
this.space.onAddPlayer(player);
var joiningSet = new MutablePlayerSet(this.space.getServer());
for (var player : players) {
this.teleporter.teleportIn(player, accept::applyJoin);
this.set.add(player);
this.space.onAddPlayer(player);
joiningSet.add(player);
}
accept.joinAll(joiningSet);

return GameResult.ok();
} catch (Throwable throwable) {
return GameResult.error(GameTexts.Join.unexpectedError());
}
}
case PlayerOfferResult.Reject reject -> {
case JoinOfferResult.Reject reject -> {
return GameResult.error(reject.reason());
}
default -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

/**
* Utility class for joining players to a {@link GameSpace}. This handles all logic such as collecting all party
* members, screening, and offering players to the {@link GameSpace}.
* members, and offering players to the {@link GameSpace}.
*/
public final class GamePlayerJoiner {
public static Results tryJoin(ServerPlayerEntity player, GameSpace gameSpace, JoinIntent intent) {
Expand All @@ -40,15 +40,9 @@ private static Set<ServerPlayerEntity> collectPlayersForJoin(ServerPlayerEntity
private static Results tryJoinAll(Collection<ServerPlayerEntity> players, GameSpace gameSpace, JoinIntent intent) {
var results = new Results();

var screenResult = gameSpace.getPlayers().screenJoins(players, intent);
if (screenResult.isError()) {
results.globalError = screenResult.error();
return results;
}

for (var player : players) {
var result = gameSpace.getPlayers().offer(player, intent);
if (result.isError()) {
var result = gameSpace.getPlayers().offer(players, intent);
if (result.isError()) {
for (var player : players) {
results.playerErrors.put(player, result.error());
pufmat marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
/**
* Represents the "intention" of a player or group of players joining a {@link GameSpace}.
* It is up to the game implementation to respect this intent in the way that is appropriate for their game. This may be
* accomplished by handling the {@link GamePlayerEvents#SCREEN_JOINS 'Screen Joins'} and
* {@link GamePlayerEvents#OFFER 'Player Offer'} events.
* accomplished by handling the {@link GamePlayerEvents#OFFER 'Join Offer'} events.
*/
public enum JoinIntent {
/**
Expand Down
Loading
Loading