/*********************************************************************************** * 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.player; import intents.GalacticIntent; import intents.PlayerEventIntent; import intents.ZoneInIntent; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Calendar; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import main.ProjectSWG; import network.packets.Packet; import network.packets.soe.SessionRequest; import network.packets.swg.login.AccountFeatureBits; import network.packets.swg.login.ClientIdMsg; import network.packets.swg.login.ClientPermissionsMessage; import network.packets.swg.login.ServerId; import network.packets.swg.login.ServerString; import network.packets.swg.login.creation.ClientVerifyAndLockNameRequest; import network.packets.swg.login.creation.ClientVerifyAndLockNameResponse; import network.packets.swg.login.creation.ClientCreateCharacter; import network.packets.swg.login.creation.ClientVerifyAndLockNameResponse.ErrorMessage; import network.packets.swg.login.creation.CreateCharacterFailure; import network.packets.swg.login.creation.CreateCharacterFailure.NameFailureReason; import network.packets.swg.login.creation.CreateCharacterSuccess; import network.packets.swg.login.creation.RandomNameRequest; import network.packets.swg.login.creation.RandomNameResponse; import network.packets.swg.zone.CmdSceneReady; import network.packets.swg.zone.GalaxyLoopTimesRequest; import network.packets.swg.zone.GalaxyLoopTimesResponse; import network.packets.swg.zone.HeartBeatMessage; import network.packets.swg.zone.ParametersMessage; import network.packets.swg.zone.SetWaypointColor; import network.packets.swg.zone.ShowBackpack; import network.packets.swg.zone.ShowHelmet; import network.packets.swg.zone.UpdatePvpStatusMessage; import network.packets.swg.zone.chat.ChatOnConnectAvatar; import network.packets.swg.zone.chat.VoiceChatStatus; import network.packets.swg.zone.insertion.ChatServerStatus; import network.packets.swg.zone.insertion.CmdStartScene; import resources.Galaxy; import resources.Location; import resources.Race; import resources.Terrain; import resources.client_info.ClientFactory; import resources.client_info.visitors.ProfTemplateData; import resources.config.ConfigFile; import resources.control.Intent; import resources.control.Service; import resources.objects.SWGObject; import resources.objects.creature.CreatureMood; import resources.objects.creature.CreatureObject; import resources.objects.player.PlayerObject; import resources.objects.tangible.TangibleObject; import resources.objects.waypoint.WaypointObject; import resources.objects.waypoint.WaypointObject.WaypointColor; import resources.player.AccessLevel; import resources.player.Player; import resources.player.PlayerEvent; import resources.player.PlayerFlags; import resources.player.PlayerState; import resources.server_info.Config; import resources.server_info.Log; import resources.zone.NameFilter; import services.objects.ObjectManager; import utilities.namegen.SWGNameGenerator; public class ZoneService extends Service { private final Map lockedNames; private final Map profTemplates; private final ClientFactory clientFac; private final NameFilter nameFilter; private final SWGNameGenerator nameGenerator; private final CharacterCreationRestriction creationRestriction; private PreparedStatement createCharacter; private PreparedStatement getCharacter; private PreparedStatement getLikeCharacterName; public ZoneService() { lockedNames = new HashMap(); profTemplates = new ConcurrentHashMap(); clientFac = new ClientFactory(); nameFilter = new NameFilter("namegen/bad_word_list.txt", "namegen/reserved_words.txt", "namegen/fiction_reserved.txt"); nameGenerator = new SWGNameGenerator(nameFilter); creationRestriction = new CharacterCreationRestriction(2); } @Override public boolean initialize() { String createCharacterSql = "INSERT INTO characters (id, name, race, userId, galaxyId) VALUES (?, ?, ?, ?, ?)"; createCharacter = getLocalDatabase().prepareStatement(createCharacterSql); getCharacter = getLocalDatabase().prepareStatement("SELECT * FROM characters WHERE name == ?"); getLikeCharacterName = getLocalDatabase().prepareStatement("SELECT name FROM characters WHERE name ilike ?"); //NOTE: ilike is not SQL standard. It is an extension for postgres only. nameGenerator.loadAllRules(); loadProfTemplates(); if (!nameFilter.load()) System.err.println("Failed to load name filter!"); registerForIntent(ZoneInIntent.TYPE); return super.initialize(); } @Override public boolean start() { creationRestriction.setCreationsPerPeriod(getConfig(ConfigFile.PRIMARY).getInt("GALAXY-MAX-CHARACTERS", 2)); return super.start(); } @Override public void onIntentReceived(Intent i) { if (i instanceof ZoneInIntent) { ZoneInIntent zii = (ZoneInIntent) i; zoneInPlayer(zii.getPlayer(), zii.getCreature(), zii.getGalaxy()); } } public void handlePacket(GalacticIntent intent, Player player, long networkId, Packet p) { if (p instanceof SessionRequest) sendServerInfo(intent.getGalaxy(), networkId); if (p instanceof ClientIdMsg) handleClientIdMsg(player, (ClientIdMsg) p); if (p instanceof RandomNameRequest) handleRandomNameRequest(player, (RandomNameRequest) p); if (p instanceof ClientVerifyAndLockNameRequest) handleApproveNameRequest(intent.getPlayerManager(), player, (ClientVerifyAndLockNameRequest) p); if (p instanceof ClientCreateCharacter) handleCharCreation(intent.getObjectManager(), player, (ClientCreateCharacter) p); if (p instanceof GalaxyLoopTimesRequest) handleGalaxyLoopTimesRequest(player, (GalaxyLoopTimesRequest) p); if (p instanceof CmdSceneReady) handleCmdSceneReady(player, (CmdSceneReady) p); if (p instanceof SetWaypointColor) handleSetWaypointColor(player, (SetWaypointColor) p); if(p instanceof ShowBackpack) handleShowBackpack(player, (ShowBackpack) p); if(p instanceof ShowHelmet) handleShowHelmet(player, (ShowHelmet) p); } private void zoneInPlayer(Player player, CreatureObject creature, String galaxy) { PlayerObject playerObj = creature.getPlayerObject(); player.setPlayerState(PlayerState.ZONING_IN); player.setCreatureObject(creature); creature.setOwner(player); sendZonePackets(player, creature); initPlayerBeforeZoneIn(player, creature, playerObj); creature.createObject(player); System.out.printf("[%s] %s is zoning in%n", player.getUsername(), player.getCharacterName()); Log.i("ObjectManager", "Zoning in %s with character %s", player.getUsername(), player.getCharacterName()); new PlayerEventIntent(player, galaxy, PlayerEvent.PE_ZONE_IN).broadcast(); } private void sendZonePackets(Player player, CreatureObject creature) { long objId = creature.getObjectId(); Race race = creature.getRace(); Location l = creature.getLocation(); long time = (long)(ProjectSWG.getCoreTime()/1E3); sendPacket(player, new HeartBeatMessage()); sendPacket(player, new ChatServerStatus(true)); sendPacket(player, new VoiceChatStatus()); sendPacket(player, new ParametersMessage()); sendPacket(player, new ChatOnConnectAvatar()); sendPacket(player, new CmdStartScene(false, objId, race, l, time)); sendPacket(player, new UpdatePvpStatusMessage(creature.getPvpType(), creature.getPvpFactionId(), creature.getObjectId())); } private void initPlayerBeforeZoneIn(Player player, CreatureObject creatureObj, PlayerObject playerObj) { playerObj.setStartPlayTime((int) System.currentTimeMillis()); creatureObj.setMoodId(CreatureMood.NONE.getMood()); playerObj.clearFlagBitmask(PlayerFlags.LD); // Ziggy: Clear the LD flag in case it wasn't already. } private void handleShowBackpack(Player player, ShowBackpack p) { player.getPlayerObject().setShowBackpack(p.showingBackpack()); } private void handleShowHelmet(Player player, ShowHelmet p) { player.getPlayerObject().setShowHelmet(p.showingHelmet()); } private void handleSetWaypointColor(Player player, SetWaypointColor p) { // TODO Should move this to a different service, maybe make a service for other packets similar to this (ie misc.) PlayerObject ghost = (PlayerObject) player.getPlayerObject(); WaypointObject waypoint = ghost.getWaypoint(p.getObjId()); if (waypoint == null) return; switch(p.getColor()) { case "blue": waypoint.setColor(WaypointColor.BLUE); break; case "green": waypoint.setColor(WaypointColor.GREEN); break; case "orange": waypoint.setColor(WaypointColor.ORANGE); break; case "yellow": waypoint.setColor(WaypointColor.YELLOW); break; case "purple": waypoint.setColor(WaypointColor.PURPLE); break; case "white": waypoint.setColor(WaypointColor.WHITE); break; default: System.err.println("Don't know color " + p.getColor()); } ghost.updateWaypoint(waypoint); } private void sendServerInfo(Galaxy galaxy, long networkId) { Config c = getConfig(ConfigFile.NETWORK); String name = c.getString("ZONE-SERVER-NAME", galaxy.getName()); int id = c.getInt("ZONE-SERVER-ID", galaxy.getId()); sendPacket(networkId, new ServerString(name + ":" + id)); sendPacket(networkId, new ServerId(id)); } private void handleCmdSceneReady(Player player, CmdSceneReady p) { player.setPlayerState(PlayerState.ZONED_IN); player.sendPacket(p); System.out.println("[" + player.getUsername() +"] " + player.getCharacterName() + " zoned in"); Log.i("ZoneService", "%s with character %s zoned in from %s:%d", player.getUsername(), player.getCharacterName(), p.getAddress(), p.getPort()); } private void handleClientIdMsg(Player player, ClientIdMsg clientId) { System.out.println("[" + player.getUsername() + "] Connected to the zone server. IP: " + clientId.getAddress() + ":" + clientId.getPort()); Log.i("ZoneService", "%s connected to the zone server from %s:%d", player.getUsername(), clientId.getAddress(), clientId.getPort()); sendPacket(player.getNetworkId(), new HeartBeatMessage()); sendPacket(player.getNetworkId(), new AccountFeatureBits()); sendPacket(player.getNetworkId(), new ClientPermissionsMessage()); } private void handleRandomNameRequest(Player player, RandomNameRequest request) { RandomNameResponse response = new RandomNameResponse(request.getRace(), ""); String race = Race.getRaceByFile(request.getRace()).getSpecies(); response.setRandomName(nameGenerator.generateRandomName(race)); sendPacket(player.getNetworkId(), response); } private void handleApproveNameRequest(PlayerManager playerMgr, Player player, ClientVerifyAndLockNameRequest request) { String name = request.getName(); ErrorMessage err = getNameValidity(name, player.getAccessLevel() != AccessLevel.PLAYER); if (err == ErrorMessage.NAME_APPROVED_MODIFIED) name = nameFilter.cleanName(name); if (err == ErrorMessage.NAME_APPROVED || err == ErrorMessage.NAME_APPROVED_MODIFIED) { if (!lockName(name, player)) { err = ErrorMessage.NAME_DECLINED_IN_USE; } } sendPacket(player.getNetworkId(), new ClientVerifyAndLockNameResponse(name, err)); } private void handleCharCreation(ObjectManager objManager, Player player, ClientCreateCharacter create) { ErrorMessage err = getNameValidity(create.getName(), player.getAccessLevel() != AccessLevel.PLAYER); if (!creationRestriction.isAbleToCreate(player)) err = ErrorMessage.NAME_DECLINED_TOO_FAST; if (err == ErrorMessage.NAME_APPROVED) { long characterId = createCharacter(objManager, player, create); if (createCharacterInDb(characterId, create.getName(), player)) { creationRestriction.createdCharacter(player); System.out.println("[" + player.getUsername() + "] Create Character: " + create.getName() + ". IP: " + create.getAddress() + ":" + create.getPort()); Log.i("ZoneService", "%s created character %s from %s:%d", player.getUsername(), create.getName(), create.getAddress(), create.getPort()); sendPacket(player, new CreateCharacterSuccess(characterId)); new PlayerEventIntent(player, PlayerEvent.PE_CREATE_CHARACTER).broadcast(); return; } Log.e("ZoneService", "Failed to create character %s for user %s with server error from %s:%d", create.getName(), player.getUsername(), create.getAddress(), create.getPort()); objManager.deleteObject(characterId); } sendCharCreationFailure(player, create, err); } private void sendCharCreationFailure(Player player, ClientCreateCharacter create, ErrorMessage err) { NameFailureReason reason = NameFailureReason.NAME_SYNTAX; switch (err) { case NAME_APPROVED: err = ErrorMessage.NAME_DECLINED_INTERNAL_ERROR; reason = NameFailureReason.NAME_RETRY; break; case NAME_DECLINED_FICTIONALLY_INAPPROPRIATE: reason = NameFailureReason.NAME_FICTIONALLY_INAPPRORIATE; break; case NAME_DECLINED_IN_USE: reason = NameFailureReason.NAME_IN_USE; break; case NAME_DECLINED_EMPTY: reason = NameFailureReason.NAME_DECLINED_EMPTY; break; case NAME_DECLINED_RESERVED: reason = NameFailureReason.NAME_DEV_RESERVED; break; case NAME_DECLINED_TOO_FAST: reason = NameFailureReason.NAME_TOO_FAST; break; default: break; } System.err.println("ZoneService: Unable to create character [Name: " + create.getName() + " User: " + player.getUsername() + "] and put into database! Reason: " + err); Log.e("ZoneService", "Failed to create character %s for user %s with error %s and reason %s from %s:%d", create.getName(), player.getUsername(), err, reason, create.getAddress(), create.getPort()); sendPacket(player, new CreateCharacterFailure(reason)); } private boolean createCharacterInDb(long characterId, String name, Player player) { if (characterExistsForName(name)) return false; synchronized (createCharacter) { try { createCharacter.setLong(1, characterId); createCharacter.setString(2, name); createCharacter.setString(3, player.getCreatureObject().getRace().getFilename()); createCharacter.setInt(4, player.getUserId()); createCharacter.setInt(5, player.getGalaxyId()); return createCharacter.executeUpdate() == 1; } catch (SQLException e) { e.printStackTrace(); return false; } } } private ErrorMessage getNameValidity(String name, boolean admin) { String modified = nameFilter.cleanName(name); if (nameFilter.isEmpty(modified)) // Empty name return ErrorMessage.NAME_DECLINED_EMPTY; if (nameFilter.containsBadCharacters(modified)) // Has non-alphabetic characters return ErrorMessage.NAME_DECLINED_SYNTAX; if (nameFilter.isProfanity(modified)) // Contains profanity return ErrorMessage.NAME_DECLINED_PROFANE; if (nameFilter.isFictionallyInappropriate(modified)) return ErrorMessage.NAME_DECLINED_SYNTAX; if (nameFilter.isReserved(modified) && !admin) return ErrorMessage.NAME_DECLINED_RESERVED; if (characterExistsForName(modified)) // User already exists. return ErrorMessage.NAME_DECLINED_IN_USE; if (nameFilter.isFictionallyReserved(modified)) return ErrorMessage.NAME_DECLINED_FICTIONALLY_RESERVED; if (!modified.equals(name)) // If we needed to remove double spaces, trim the ends, etc return ErrorMessage.NAME_APPROVED_MODIFIED; return ErrorMessage.NAME_APPROVED; } private boolean characterExistsForName(String name) { synchronized (getCharacter) { ResultSet set = null; try { String nameSplitStr[] = name.split(" "); String charExistsPrepStmtStr = nameSplitStr[0] + "%"; //Only the first name should be unique. getLikeCharacterName.setString(1, charExistsPrepStmtStr); set = getLikeCharacterName.executeQuery(); while (set.next()){ String dbName = set.getString("name"); if(nameSplitStr[0].equalsIgnoreCase(dbName.split(" ")[0])){ return true; } } return false; } catch (SQLException e) { e.printStackTrace(); return false; } finally { try { if (set != null) set.close(); } catch (SQLException e) { e.printStackTrace(); } } } } private long createCharacter(ObjectManager objManager, Player player, ClientCreateCharacter create) { Location start = getStartLocation(create.getStart()); Race race = Race.getRaceByFile(create.getRace()); CreatureObject creatureObj = createCreature(objManager, race.getFilename(), start); PlayerObject playerObj = createPlayer(objManager, "object/player/shared_player.iff"); setCreatureObjectValues(objManager, creatureObj, create); setPlayerObjectValues(playerObj, create); createHair(objManager, creatureObj, create.getHair(), create.getHairCustomization()); createStarterClothing(objManager, creatureObj, create.getRace(), create.getClothes()); creatureObj.setVolume(0x000F4240); creatureObj.setOwner(player); creatureObj.addObject(playerObj); // ghost slot playerObj.setAdminTag(player.getAccessLevel()); playerObj.setOwner(player); player.setCreatureObject(creatureObj); return creatureObj.getObjectId(); } private CreatureObject createCreature(ObjectManager objManager, String template, Location location) { SWGObject obj = objManager.createObject(template, location); if (obj instanceof CreatureObject) return (CreatureObject) obj; return null; } private PlayerObject createPlayer(ObjectManager objManager, String template) { SWGObject obj = objManager.createObject(template); if (obj instanceof PlayerObject) return (PlayerObject) obj; return null; } private TangibleObject createTangible(ObjectManager objManager, String template) { SWGObject obj = objManager.createObject(template); if (obj instanceof TangibleObject) return (TangibleObject) obj; return null; } private void createHair(ObjectManager objManager, CreatureObject creatureObj, String hair, byte [] customization) { if (hair.isEmpty()) return; TangibleObject hairObj = createTangible(objManager, ClientFactory.formatToSharedFile(hair)); hairObj.getContainerPermissions().addDefaultWorldPermissions(); hairObj.setAppearanceData(customization); creatureObj.addObject(hairObj); // slot = hair creatureObj.addEquipment(hairObj); } private void setCreatureObjectValues(ObjectManager objManager, CreatureObject creatureObj, ClientCreateCharacter create) { TangibleObject inventory = createTangible(objManager, "object/tangible/inventory/shared_character_inventory.iff"); TangibleObject datapad = createTangible(objManager, "object/tangible/datapad/shared_character_datapad.iff"); TangibleObject apprncInventory = createTangible(objManager, "object/tangible/inventory/shared_appearance_inventory.iff"); creatureObj.setRace(Race.getRaceByFile(create.getRace())); creatureObj.setAppearanceData(create.getCharCustomization()); creatureObj.setHeight(create.getHeight()); creatureObj.setName(create.getName()); creatureObj.setPvpType(20); creatureObj.getSkills().add("species_" + creatureObj.getRace().getSpecies()); creatureObj.addObject(inventory); // slot = inventory creatureObj.addObject(datapad); // slot = datapad creatureObj.addObject(apprncInventory); // slot = appearance_inventory creatureObj.addEquipment(inventory); creatureObj.addEquipment(datapad); creatureObj.addEquipment(apprncInventory); creatureObj.getContainerPermissions().addDefaultWorldPermissions(); } private void setPlayerObjectValues(PlayerObject playerObj, ClientCreateCharacter create) { playerObj.setProfession(create.getProfession()); Calendar date = Calendar.getInstance(); playerObj.setBornDate(date.get(Calendar.YEAR), date.get(Calendar.MONTH) + 1, date.get(Calendar.DAY_OF_MONTH)); playerObj.getContainerPermissions().addDefaultWorldPermissions(); } private void handleGalaxyLoopTimesRequest(Player player, GalaxyLoopTimesRequest req) { sendPacket(player, new GalaxyLoopTimesResponse(ProjectSWG.getCoreTime()/1000)); } private void createStarterClothing(ObjectManager objManager, CreatureObject player, String race, String profession) { if (player.getSlottedObject("inventory") == null) return; for (String template : profTemplates.get(profession).getItems(ClientFactory.formatToSharedFile(race))) { TangibleObject item = createTangible(objManager, template); // Move the new item to the player's clothing slots and add to equipment list item.moveToContainer(player, player); player.addEquipment(item); } } private void loadProfTemplates() { profTemplates.put("crafting_artisan", (ProfTemplateData) clientFac.getInfoFromFile("creation/profession_defaults_combat_brawler.iff")); profTemplates.put("combat_brawler", (ProfTemplateData) clientFac.getInfoFromFile("creation/profession_defaults_combat_brawler.iff")); profTemplates.put("social_entertainer", (ProfTemplateData) clientFac.getInfoFromFile("creation/profession_defaults_social_entertainer.iff")); profTemplates.put("combat_marksman", (ProfTemplateData) clientFac.getInfoFromFile("creation/profession_defaults_combat_marksman.iff")); profTemplates.put("science_medic", (ProfTemplateData) clientFac.getInfoFromFile("creation/profession_defaults_science_medic.iff")); profTemplates.put("outdoors_scout", (ProfTemplateData) clientFac.getInfoFromFile("creation/profession_defaults_outdoors_scout.iff")); profTemplates.put("jedi", (ProfTemplateData) clientFac.getInfoFromFile("creation/profession_defaults_jedi.iff")); } private boolean lockName(String name, Player player) { String firstName = name.split(" ", 2)[0].toLowerCase(Locale.ENGLISH); if (isLocked(firstName)) return false; synchronized (lockedNames) { unlockName(player); lockedNames.put(firstName, player); Log.i("ZoneService", "Locked name %s for user %s", firstName, player.getUsername()); } return true; } private void unlockName(Player player) { synchronized (lockedNames) { String fName = null; for (Entry e : lockedNames.entrySet()) { Player locked = e.getValue(); if (locked != null && locked.equals(player)) { fName = e.getKey(); break; } } if (fName != null) { if (lockedNames.remove(fName) != null) Log.i("ZoneService", "Unlocked name %s for user %s", fName, player.getUsername()); } } } private boolean isLocked(String firstName) { Player player = null; synchronized (lockedNames) { player = lockedNames.get(firstName); } if (player == null) return false; PlayerState state = player.getPlayerState(); return state != PlayerState.DISCONNECTED && state != PlayerState.LOGGED_OUT; } private Location getStartLocation(String start) { return TerrainZoneInsertion.getInsertionForTerrain(Terrain.TATOOINE); // return TerrainZoneInsertion.getInsertionForArea(Terrain.CORELLIA, -5436, 24, -6211); } }