/******************************************************************************* * Copyright (c) 2015 /// Project SWG /// www.projectswg.com * * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * Our goal is to create an emulator which will provide a server for players to * continue playing a game similar to the one they used to play. We are basing * it on the final publish of the game prior to end-game events. * * This file is part of Holocore. * * -------------------------------------------------------------------------------- * * Holocore is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Holocore is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Holocore. If not, see ******************************************************************************/ package services.chat; import intents.NotifyPlayersPacketIntent; import intents.chat.ChatRoomUpdateIntent; import intents.network.GalacticPacketIntent; import network.packets.Packet; import network.packets.swg.SWGPacket; import network.packets.swg.zone.chat.ChatCreateRoom; import network.packets.swg.zone.chat.ChatDestroyRoom; import network.packets.swg.zone.chat.ChatEnterRoomById; import network.packets.swg.zone.chat.ChatOnCreateRoom; import network.packets.swg.zone.chat.ChatOnDestroyRoom; import network.packets.swg.zone.chat.ChatOnEnteredRoom; import network.packets.swg.zone.chat.ChatOnLeaveRoom; import network.packets.swg.zone.chat.ChatOnSendRoomMessage; import network.packets.swg.zone.chat.ChatQueryRoom; import network.packets.swg.zone.chat.ChatQueryRoomResults; import network.packets.swg.zone.chat.ChatRemoveAvatarFromRoom; import network.packets.swg.zone.chat.ChatRequestRoomList; import network.packets.swg.zone.chat.ChatSendToRoom; import network.packets.swg.zone.insertion.ChatRoomList; import resources.Terrain; import resources.chat.ChatAvatar; import resources.chat.ChatResult; import resources.chat.ChatRoom; import resources.client_info.ServerFactory; import resources.client_info.visitors.DatatableData; import resources.control.Intent; import resources.control.Service; import resources.objects.player.PlayerObject; import resources.player.AccessLevel; import resources.player.Player; import services.player.PlayerManager; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author Waverunner */ public class ChatRoomService extends Service { private int maxChatRoomId; private Map roomMap; // Map to keep track of each player's recent message for a room to prevent duplicates from client private Map> messages; public ChatRoomService() { roomMap = new ConcurrentHashMap<>(); messages = new ConcurrentHashMap<>(); maxChatRoomId = 1; } @Override public boolean initialize() { // TODO: Load up persistent channels registerForIntent(ChatRoomUpdateIntent.TYPE); registerForIntent(GalacticPacketIntent.TYPE); return super.initialize(); } @Override public void onIntentReceived(Intent i) { switch(i.getType()) { case ChatRoomUpdateIntent.TYPE: processChatRoomUpdateIntent((ChatRoomUpdateIntent) i); break; case GalacticPacketIntent.TYPE: processPacket((GalacticPacketIntent) i); break; } } private void processPacket(GalacticPacketIntent intent) { Player player = intent.getPlayerManager().getPlayerFromNetworkId(intent.getNetworkId()); if (player == null) return; Packet p = intent.getPacket(); if (p instanceof SWGPacket) processSwgPacket(player, (SWGPacket) p); } private void processSwgPacket(Player player, SWGPacket p) { switch (p.getPacketType()) { case CHAT_QUERY_ROOM: if (p instanceof ChatQueryRoom) handleChatQueryRoom(player, (ChatQueryRoom) p); break; case CHAT_ENTER_ROOM_BY_ID: { if (!(p instanceof ChatEnterRoomById)) return; ChatEnterRoomById enterRoomById = (ChatEnterRoomById) p; enterChatChannel(player, enterRoomById.getRoomId(), enterRoomById.getSequence()); break; } case CHAT_REMOVE_AVATAR_FROM_ROOM: if (p instanceof ChatRemoveAvatarFromRoom) leaveChatChannel(player, ((ChatRemoveAvatarFromRoom) p).getPath()); break; case CHAT_SEND_TO_ROOM: if (p instanceof ChatSendToRoom) handleChatSendToRoom(player, (ChatSendToRoom) p); break; case CHAT_REQUEST_ROOM_LIST: if (p instanceof ChatRequestRoomList) handleChatRoomListRequest(player); break; case CHAT_CREATE_ROOM: if (p instanceof ChatCreateRoom) handleChatCreateRoom(player, (ChatCreateRoom) p); break; case CHAT_DESTROY_ROOM: if (p instanceof ChatDestroyRoom) handleChatDestroyRoom(player, (ChatDestroyRoom) p); break; default: break; } } private void processChatRoomUpdateIntent(ChatRoomUpdateIntent i) { switch(i.getUpdateType()) { case CREATE: createRoom(i.getAvatar(), i.isPublic(), i.getPath(), i.getTitle()); break; case DESTROY: notifyDestroyRoom(i.getAvatar(), i.getPath(), 0); break; default: break; } } /* Chat Rooms */ private void handleChatDestroyRoom(Player player, ChatDestroyRoom p) { ChatRoom room = roomMap.get(p.getRoomId()); ChatAvatar avatar = ChatAvatar.getFromPlayer(player); if ((room == null || !room.getCreator().equals(avatar) || !room.getOwner().equals(avatar))) { player.sendPacket(new ChatOnDestroyRoom(ChatAvatar.getFromPlayer(player), ChatResult.ROOM_AVATAR_NO_PERMISSION.getCode(), p.getRoomId(), p.getSequence())); return; } if (!notifyDestroyRoom(avatar, room.getPath(), p.getSequence())) { player.sendPacket(new ChatOnDestroyRoom(ChatAvatar.getFromPlayer(player), ChatResult.NONE.getCode(), p.getRoomId(), p.getSequence())); } } private void handleChatCreateRoom(Player player, ChatCreateRoom p) { String path = p.getRoomName(); String title = p.getRoomTitle(); ChatRoom room = getRoom(path); ChatResult result = ChatResult.SUCCESS; if (room != null) result = ChatResult.ROOM_ALREADY_EXISTS; if (result == ChatResult.SUCCESS) { room = createRoom(ChatAvatar.getFromPlayer(player), p.isPublic(), path, title); room.setMuted(p.isModerated()); } player.sendPacket(new ChatOnCreateRoom(result.getCode(), room, p.getSequence())); } private void handleChatSendToRoom(Player player, ChatSendToRoom p) { ChatResult result = ChatResult.SUCCESS; ChatRoom room = getRoom(p.getRoomId()); if (room == null) result = ChatResult.ROOM_INVALID_ID; if (result != ChatResult.SUCCESS) { player.sendPacket(new ChatOnSendRoomMessage(result.getCode(), p.getSequence())); return; } if (!incrementMessageCounter(player.getNetworkId(), room.getId(), p.getSequence())) return; ChatAvatar avatar = ChatAvatar.getFromPlayer(player); result = room.canSendMessage(avatar); // TODO: Check length of messages -- Result 16 is used for too long message player.sendPacket(new ChatOnSendRoomMessage(result.getCode(), p.getSequence())); if (result == ChatResult.SUCCESS) room.sendMessage(avatar, p.getMessage(), p.getOutOfBandPackage(), player.getPlayerManager()); } private void handleChatQueryRoom(Player player, ChatQueryRoom p) { ChatRoom room = getRoom(p.getRoomPath()); // No result code is sent for queries if (room == null) return; player.sendPacket(new ChatQueryRoomResults(room, p.getSequence())); } private void handleChatRoomListRequest(Player player) { ChatAvatar avatar = ChatAvatar.getFromPlayer(player); List rooms = new ArrayList<>(); for (ChatRoom chatRoom : roomMap.values()) { if (!chatRoom.isPublic() && !chatRoom.getInvited().contains(avatar) && !chatRoom.getOwner().equals(avatar)) continue; rooms.add(chatRoom); } ChatRoomList response = new ChatRoomList(rooms); player.sendPacket(response); } private void sendOnEnteredChatRoom(Player player, ChatAvatar avatar, ChatResult result, int id, int sequence) { ChatOnEnteredRoom onEnteredRoom = new ChatOnEnteredRoom(avatar, id, sequence); onEnteredRoom.setResult(result.getCode()); player.sendPacket(onEnteredRoom); } public void enterChatChannels(Player player, List channels) { for (String s : channels) { enterChatChannel(player, s); } } public void enterPlanetaryChatChannels(Player player) { PlayerObject ghost = player.getPlayerObject(); if (ghost == null) return; // Leave old zone-only chat channels String planetEndPath = ".Planet"; for (String channel : ghost.getJoinedChannels()) { if (channel.endsWith(planetEndPath)) { leaveChatChannel(player, channel); } else { // Better way of doing this? String[] split = channel.split("\\."); if (split.length == 3 && split[2].equals("system")) leaveChatChannel(player, channel); } } Terrain terrain = player.getCreatureObject().getLocation().getTerrain(); // Enter the new zone-only chat channels String planetPath = "SWG." + player.getGalaxyName() + "." + terrain.getNameCapitalized() + "."; if (getRoom(planetPath + "Planet") == null) return; enterChatChannel(player, planetPath + "Planet"); enterChatChannel(player, planetPath + "system"); } /** * Attempts to join the specified chat channel * @param player Player joining the chat channel * @param room Chat room to enter */ public void enterChatChannel(Player player, ChatRoom room, int sequence) { ChatAvatar avatar = ChatAvatar.getFromPlayer(player); PlayerObject ghost = player.getPlayerObject(); if (ghost == null) { sendOnEnteredChatRoom(player, avatar, ChatResult.NONE, room.getId(), sequence); return; } ChatResult result = room.canJoinRoom(avatar); if (result != ChatResult.SUCCESS && player.getAccessLevel() != AccessLevel.PLAYER) { sendOnEnteredChatRoom(player, avatar, result, room.getId(), sequence); return; } // TODO: Check if player is appropriate faction for the room (Rebel and imperial chat rooms) // Server-based list so we can join chat channels automatically ghost.addJoinedChannel(room.getPath()); // Re-send the player the room list with just this room as it could have been private/hidden // This also "refreshes" the client, not sending this will cause a Chat channel unavailable message. if (!room.isPublic()) player.sendPacket(new ChatRoomList(room)); // Notify players of success, it's ChatResult.SUCCESS at this point player.sendPacket(new ChatOnEnteredRoom(avatar, result.getCode(), room.getId(), sequence)); PlayerManager manager = player.getPlayerManager(); // Notify everyone that a player entered the room room.sendPacketToMembers(manager, new ChatOnEnteredRoom(avatar, result.getCode(), room.getId(), 0)); room.getMembers().add(avatar); } public void enterChatChannel(Player player, int id, int sequence) { ChatRoom room = getRoom(id); if (room == null) { sendOnEnteredChatRoom(player, ChatAvatar.getFromPlayer(player), ChatResult.NONE, id, sequence); return; } enterChatChannel(player, room, sequence); } public void enterChatChannel(Player player, String path) { for (ChatRoom room : roomMap.values()) { if (room.getPath().equals(path)) { enterChatChannel(player, room, 0); return; } } // Channel was not found, attempt to remove it from this players list of channels if it exists. // This can happen if a channel was deleted while the player was offline PlayerObject ghost = player.getPlayerObject(); if (ghost == null) { System.err.println("Tried to join a room with a path that does not exist: " + path); return; } ghost.removeJoinedChannel(path); } public void leaveChatChannel(Player player, ChatRoom room, int sequence) { ChatAvatar avatar = ChatAvatar.getFromPlayer(player); PlayerObject ghost = player.getPlayerObject(); if (ghost == null) return; // ChatOnLeaveRoom doesn't do anything other than for a ChatResult.SUCCESS, so no need to send a fail if (!room.getMembers().remove(avatar) && !ghost.removeJoinedChannel(room.getPath())) return; player.sendPacket(new ChatOnLeaveRoom(avatar, ChatResult.SUCCESS.getCode(), room.getId(), sequence)); room.sendPacketToMembers(player.getPlayerManager(), new ChatOnLeaveRoom(avatar, ChatResult.SUCCESS.getCode(), room.getId(), 0)); } public void leaveChatChannel(Player player, String path) { for (ChatRoom chatRoom : roomMap.values()) { if (chatRoom.getPath().equals(path)) leaveChatChannel(player, chatRoom, 0); } } /** * Creates a new, non-persistent, chat room with the specified address path. * @param creator Room creator who will also become the owner of this room * @param isPublic Determines if the room should be publicly displayed in the channel listing * @param path Address for the channel (Ex: SWG.serverName.Imperial) * @param title Descriptive name of the chat channel (Ex: Imperial chat for this galaxy) * @return {@link ChatRoom} */ public ChatRoom createRoom(ChatAvatar creator, boolean isPublic, String path, String title) { if (path.isEmpty() || path.endsWith(".")) return null; String base = "SWG." + creator.getGalaxy(); if (!path.startsWith(base) || path.equals(base)) return null; // All paths should have parents, lets validate to make sure they exist first. Create them if they don't. int lastIndex = path.lastIndexOf("."); if (lastIndex != -1) { String parentPath = path.substring(0, lastIndex); if (getRoom(parentPath) == null) { createRoom(creator, isPublic, parentPath, ""); } } ChatRoom room = new ChatRoom(); int id = maxChatRoomId; maxChatRoomId++; room.setId(id); room.setOwner(creator); room.setCreator(creator); room.setIsPublic(isPublic); room.setPath(path); room.setTitle(title); roomMap.put(id, room); return room; } public boolean notifyDestroyRoom(ChatAvatar destroyer, String roomPath, int sequence) { ChatRoom room = getRoom(roomPath); if (roomPath == null) return false; if (!destroyRoom(room)) return false; // Send the ChatOnDestroyRoom packet to every else in the room besides the person destroying the packet List networkIds = new ArrayList<>(); room.getMembers().forEach(member -> { if (!destroyer.equals(member)) networkIds.add(member.getNetworkId()); }); new NotifyPlayersPacketIntent(new ChatOnDestroyRoom(destroyer, ChatResult.SUCCESS.getCode(), room.getId(), 0), networkIds).broadcast(); if (!destroyer.equals(ChatAvatar.getSystemAvatar(destroyer.getGalaxy()))) { new NotifyPlayersPacketIntent(new ChatOnDestroyRoom(destroyer, ChatResult.SUCCESS.getCode(), room.getId(), sequence), Collections.singletonList(destroyer.getNetworkId())).broadcast(); } return true; } public boolean destroyRoom(ChatRoom room) { return room != null && roomMap.remove(room.getId()) != null; } public void createSystemChannels(String galaxy) { /** Channel Notes * Group channels: created by System * - SWG.serverName.group.GroupObjectId.GroupChat title = GroupId * Guild channels: created by System * - SWG.serverName.guild.GuildId.GuildChat title = GuildId * City channels: created by System * - SWG.serverName.city.CityId.CityChat title = CityId */ ChatAvatar systemAvatar = ChatAvatar.getSystemAvatar(galaxy); String basePath = "SWG." + galaxy + "."; DatatableData rooms = ServerFactory.getDatatable("chat/default_rooms.iff"); rooms.handleRows((r) -> createRoom(systemAvatar, true, basePath + rooms.getCell(r, 0), (String) rooms.getCell(r, 1))); createPlanetChannels(systemAvatar, basePath); /* Battlefield Room path examples: SWG.Bria.corellia.battlefield SWG.Bria.corellia.battlefield.corellia_mountain_fortress.allFactions SWG.Bria.corellia.battlefield.corellia_pvp.allFactions / Imperial / Rebel SWG.Bria.corellia.battlefield.corellia_rebel_riverside_fort.allFactions */ } private void createPlanetChannels(ChatAvatar systemAvatar, String basePath) { DatatableData planets = ServerFactory.getDatatable("chat/planets.iff"); planets.handleRows((r) -> { String path = basePath + planets.getCell(r, 0) + "."; createRoom(systemAvatar, true, path + "Planet", "public chat for this planet, cannot create rooms here"); createRoom(systemAvatar, true, path + "system", "system messages for this planet, cannot create rooms here"); createRoom(systemAvatar, true, path + "Chat", "public chat for this planet, can create rooms here"); }); } private boolean incrementMessageCounter(long networkId, int roomId, int messageId) { if (networkId == -1) // System ChatAvatar uses -1 for networkId return true; Map messageHistory = messages.get(networkId); if (messageHistory != null) { if (messageHistory.containsKey(roomId) && messageHistory.get(roomId) == messageId) return false; } else { messageHistory = new HashMap<>(); messages.put(networkId, messageHistory); } messageHistory.put(roomId, messageId); return true; } public ChatRoom getRoom(String path) { for (ChatRoom room : roomMap.values()) { if (room.getPath().equals(path)) return room; } return null; } public ChatRoom getRoom(int roomId) { return roomMap.get(roomId); } }