Implement friends list

This commit is contained in:
Melledy
2025-11-14 04:39:48 -08:00
parent 1c52ec011f
commit ecc2ef2315
15 changed files with 666 additions and 10 deletions
@@ -0,0 +1,311 @@
package emu.nebula.game.friends;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.game.GameContext;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.FriendListGet.FriendListGetResp;
import emu.nebula.proto.Public.FriendDetail;
import emu.nebula.proto.Public.FriendState;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class FriendList extends PlayerManager {
private final Int2ObjectMap<Friendship> friends;
private final Int2ObjectMap<Friendship> pendingFriends;
private long cacheCooldown;
private FriendListGetResp cachedProto;
public FriendList(Player player) {
super(player);
this.friends = new Int2ObjectOpenHashMap<Friendship>();
this.pendingFriends = new Int2ObjectOpenHashMap<Friendship>();
}
private GameContext getGameContext() {
return Nebula.getGameContext();
}
public boolean isLoaded() {
return this.getPlayer().isLoaded();
}
private synchronized Friendship getFriendById(int id) {
if (this.isLoaded()) {
return this.getFriends().get(id);
} else {
return Nebula.getGameDatabase().getObjectByUid(Friendship.class, Friendship.generateUniqueKey(getPlayerUid(), id));
}
}
private synchronized Friendship getPendingFriendById(int id) {
if (this.isLoaded()) {
return this.getPendingFriends().get(id);
} else {
return Nebula.getGameDatabase().getObjectByUid(Friendship.class, Friendship.generateUniqueKey(getPlayerUid(), id));
}
}
public synchronized boolean hasPendingRequests() {
return this.getPendingFriends().values()
.stream()
.filter(f -> f.getAskerUid() != this.getPlayerUid())
.findAny()
.isPresent();
}
private void addFriendship(Friendship friendship) {
getFriends().put(friendship.getFriendUid(), friendship);
this.cacheCooldown = 0;
}
private void addPendingFriendship(Friendship friendship) {
getPendingFriends().put(friendship.getFriendUid(), friendship);
this.cacheCooldown = 0;
}
private void removeFriendship(int uid) {
getFriends().remove(uid);
this.cacheCooldown = 0;
}
private void removePendingFriendship(int uid) {
getPendingFriends().remove(uid);
this.cacheCooldown = 0;
}
/**
* Gets total amount of potential friends
*/
public int getFullFriendCount() {
return this.getPendingFriends().size() + this.getFriends().size();
}
public synchronized Player handleFriendRequest(int targetUid, boolean action) {
// Make sure we have enough room
if (this.getFriends().size() >= GameConstants.MAX_FRIENDSHIPS) {
return null;
}
// Check if player has sent friend request
Friendship myFriendship = this.getPendingFriendById(targetUid);
if (myFriendship == null) {
return null;
}
// Make sure this player is not the asker
if (myFriendship.getAskerUid() == this.getPlayer().getUid()) {
return null;
}
// Get target player
Player target = getGameContext().getPlayerModule().getPlayer(targetUid);
if (target == null) return null;
// Get target player's friendship
Friendship theirFriendship = target.getFriendList().getPendingFriendById(getPlayer().getUid());
if (theirFriendship == null) {
// They dont have us on their friends list anymore, rip
this.removePendingFriendship(target.getUid());
myFriendship.delete();
return null;
}
// Handle action
if (action) {
// Request accepted
myFriendship.setFriend(true);
theirFriendship.setFriend(true);
this.removePendingFriendship(myFriendship.getFriendUid());
this.addFriendship(myFriendship);
if (target.isLoaded()) {
target.getFriendList().removePendingFriendship(this.getPlayer().getUid());
target.getFriendList().addFriendship(theirFriendship);
}
// Save friendships to the database
myFriendship.save();
theirFriendship.save();
} else {
// Request declined - Delete from my pending friends
this.removePendingFriendship(myFriendship.getOwnerUid());
if (target.isLoaded()) {
target.getFriendList().removePendingFriendship(getPlayer().getUid());
}
// Delete friendships from the database
myFriendship.delete();
theirFriendship.delete();
}
// Success
return target;
}
public List<Player> acceptAll() {
// Results
List<Player> results = new ArrayList<>();
// Get list of friendships to accept
List<Friendship> list = getPendingFriends().values()
.stream()
.toList();
for (var invite : list) {
var target = this.handleFriendRequest(invite.getFriendUid(), true);
if (target != null) {
results.add(target);
}
}
return results;
}
public synchronized boolean sendFriendRequest(int targetUid) {
// Get target and sanity check
Player target = getGameContext().getPlayerModule().getPlayer(targetUid);
if (target == null || target == this.getPlayer()) {
return false;
}
// Check if friend already exists
if (getPendingFriends().containsKey(targetUid) || getFriends().containsKey(targetUid)) {
return false;
}
// Create friendships
Friendship myFriendship = new Friendship(getPlayer(), target, getPlayer());
Friendship theirFriendship = new Friendship(target, getPlayer(), getPlayer());
// Add to our pending friendship list
this.addPendingFriendship(myFriendship);
if (target.isLoaded()) {
target.getFriendList().addPendingFriendship(theirFriendship);
// Send message to notify target
target.addNextPackage(
NetMsgId.friend_state_notify,
FriendState.newInstance()
.setId(this.getPlayerUid())
.setAction(1)
);
}
// Save friendships to the database
myFriendship.save();
theirFriendship.save();
// Success
return true;
}
public synchronized boolean deleteFriend(int targetUid) {
// Get friendship
Friendship myFriendship = this.getFriendById(targetUid);
if (myFriendship == null) return false;
// Remove from friends list
this.removeFriendship(targetUid);
myFriendship.delete();
// Delete from friend's friend list
Player friend = getGameContext().getPlayerModule().getPlayer(targetUid);
if (friend != null) {
// Friend online
Friendship theirFriendship = friend.getFriendList().getFriendById(this.getPlayer().getUid());
if (theirFriendship != null) {
// Delete friendship on friends side
theirFriendship.delete();
if (friend.isLoaded()) {
// Remove from online friend's friend list
friend.getFriendList().removeFriendship(theirFriendship.getFriendUid());
}
}
}
// Success
return true;
}
// Database
public synchronized void loadFromDatabase() {
var friendships = Nebula.getGameDatabase().getObjects(Friendship.class, "ownerUid", this.getPlayer().getUid());
friendships.forEach(friendship -> {
// Set ownership first
friendship.setOwner(getPlayer());
// Finally, load to our friends list
if (friendship.isFriend()) {
getFriends().put(friendship.getFriendUid(), friendship);
} else {
getPendingFriends().put(friendship.getFriendUid(), friendship);
}
});
}
// Proto
public synchronized FriendListGetResp toProto() {
if (this.cachedProto == null || System.currentTimeMillis() > this.cacheCooldown) {
this.cachedProto = this.updateCache();
this.cacheCooldown = System.currentTimeMillis() + 60_000;
}
return this.cachedProto;
}
private FriendListGetResp updateCache() {
var proto = FriendListGetResp.newInstance();
// Encode friends list
for (var friend : getFriends().values()) {
// Get base friend info
var base = friend.toProto();
if (base == null) continue;
// Create info
var info = FriendDetail.newInstance()
.setBase(base)
.setGetEnergy(friend.getEnergy());
// Add
proto.addFriends(info);
}
// Encode pending invites
for (var friend : getPendingFriends().values()) {
// Skip if this is us
if (friend.getAskerUid() == this.getPlayerUid()) {
continue;
}
// Get base friend info
var base = friend.toProto();
if (base == null) continue;
// Add
proto.addInvites(base);
}
return proto;
}
}
@@ -0,0 +1,65 @@
package emu.nebula.game.friends;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.Friend;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(value = "friendships", useDiscriminator = false)
public class Friendship implements GameDatabaseObject {
@Id private long key;
@Indexed private int ownerUid;
@Indexed private int friendUid;
private int askerUid;
@Setter
private boolean isFriend;
private boolean star;
private int energy;
@Setter private transient Player owner;
@Deprecated // Morphia use only
public Friendship() { }
public Friendship(Player owner, Player friend, Player asker) {
this.owner = owner;
this.ownerUid = owner.getUid();
this.friendUid = friend.getUid();
this.askerUid = asker.getUid();
this.key = Friendship.generateUniqueKey(owner.getUid(), friend.getUid());
}
// Database functions
public void delete() {
Nebula.getGameDatabase().delete(this);
}
// Proto
public Friend toProto() {
// Get target player
var target = Nebula.getGameContext().getPlayerModule().getPlayer(this.getFriendUid());
if (target == null) return null;
// Encode player to simple friend proto
return target.getFriendProto();
}
// Extra
/**
* Creates an unique key for a friendship object using 2 player uids
*/
public static long generateUniqueKey(int ownerUid, int targetUid) {
return ((long) ownerUid << 32) + targetUid;
}
}