Package marauroa.server.game.rp

Source Code of marauroa.server.game.rp.RPServerManager

/* $Id: RPServerManager.java,v 1.74 2010/12/13 20:30:11 kymara Exp $ */
/***************************************************************************
*                      (C) Copyright 2003 - Marauroa                      *
***************************************************************************
***************************************************************************
*                                                                         *
*   This program is free software; you can redistribute it and/or modify  *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************/
package marauroa.server.game.rp;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import marauroa.common.Configuration;
import marauroa.common.Log4J;
import marauroa.common.game.AccountResult;
import marauroa.common.game.CharacterResult;
import marauroa.common.game.IRPZone;
import marauroa.common.game.Perception;
import marauroa.common.game.RPAction;
import marauroa.common.game.RPObject;
import marauroa.common.game.RPObjectInvalidException;
import marauroa.common.game.RPObjectNotFoundException;
import marauroa.common.game.Result;
import marauroa.common.net.message.MessageS2CPerception;
import marauroa.common.net.message.MessageS2CTransferREQ;
import marauroa.common.net.message.TransferContent;
import marauroa.server.db.TransactionPool;
import marauroa.server.game.ActionInvalidException;
import marauroa.server.game.Statistics;
import marauroa.server.game.container.ClientState;
import marauroa.server.game.container.PlayerEntry;
import marauroa.server.game.container.PlayerEntryContainer;
import marauroa.server.game.db.AccountDAO;
import marauroa.server.game.db.CharacterDAO;
import marauroa.server.game.db.DAORegister;
import marauroa.server.net.INetworkServerManager;
import marauroa.server.net.validator.ConnectionValidator;

/**
* This class is responsible for adding actions to scheduler, and to build and
* sent perceptions.
* <p>
* The goal of the RP Manager is to handle the RP of the game. This means:
* <ul>
* <li>run RPActions from clients
* <li>manage RPWorld
* <li>control triggers for events
* <li>control AI
* </ul>
* <p>
* This is a HUGE task that is very complex.<br>
* Hence we split that behaviour in different class:
* <ul>
* <li>IRuleProcessor will handle al the RP logic and run actions from client.
* This class will also implement AI, triggers, events, rules, etc...
* <li>RPWorld will handle all the world storage and management logic.
* </ul>
*
* @author miguel
*/
public class RPServerManager extends Thread {

  /** the logger instance. */
  private static final marauroa.common.Logger logger = Log4J.getLogger(RPServerManager.class);

  /** The thread will be running while keepRunning is true */
  private volatile boolean keepRunning;

  /** isFinished is true when the thread has really exited. */
  private volatile boolean isfinished;

  /** The time elapsed between 2 turns. */
  private long turnDuration;

  /** The number of the turn that we are executing now */
  private int turn;

  /** The scheduler needed to organize actions */
  private RPScheduler scheduler;

  /** The ruleProcessor that the scheduler will use to execute the actions */
  private IRPRuleProcessor ruleProcessor;

  /** The place where the objects are stored */
  private RPWorld world;

  private Statistics stats;

  /** The networkServerManager so that we can send perceptions */
  private INetworkServerManager netMan;

  /** The PlayerEntryContainer so that we know where to send perceptions */
  private PlayerEntryContainer playerContainer;

  private List<PlayerEntry> playersToRemove;

  private Map<RPObject, List<TransferContent>> contentsToTransfer;

  /**
   * Constructor
   *
   * @param netMan
   *            the NetworkServerManager so that we can send message
   * @throws Exception in case of an unexpected error
   */
  public RPServerManager(INetworkServerManager netMan) throws Exception {
    super("RPServerManager");

    try {
      stats = Statistics.getStatistics();
      keepRunning = true;
      isfinished = false;

      scheduler = new RPScheduler();
      contentsToTransfer = new HashMap<RPObject, List<TransferContent>>();
      playerContainer = PlayerEntryContainer.getContainer();

      playersToRemove = new LinkedList<PlayerEntry>();
      this.netMan = netMan;

      Configuration conf = Configuration.getConfiguration();
      /*
       * This method loads the extensions that implement the game server
       * code.
       */
      initializeExtensions(conf);

      String duration = conf.get("turn_length");
      turnDuration = Long.parseLong(duration);
      turn = 0;
    } catch (Exception e) {
      logger.warn("ABORT: Unable to create RPZone, RPRuleProcessor or RPAIManager instances",
              e);
      throw e;
    }
  }

  /**
   * This method loads the extensions: IRPRuleProcessor and IRPWorld that are
   * going to be used to implement your game. This method loads these class
   * from the class names passed as arguments in Configuration
   *
   * @param conf
   *            the Configuration class
   * @throws ClassNotFoundException
   * @throws NoSuchMethodException
   * @throws InvocationTargetException
   * @throws IllegalAccessException
   * @throws SecurityException
   * @throws IllegalArgumentException
   */
  protected void initializeExtensions(Configuration conf) throws ClassNotFoundException,
          IllegalArgumentException, SecurityException, IllegalAccessException,
          InvocationTargetException, NoSuchMethodException {
    Class<?> worldClass = Class.forName(conf.get("world"));
    // call the get() method without parameters to retrieve the singleton
    // instance
    world = (RPWorld) worldClass.getDeclaredMethod("get", new Class[0]).invoke(null, (Object[]) null);
    RPWorld.set(world);
    world.onInit();

    Class<?> ruleProcessorClass = Class.forName(conf.get("ruleprocessor"));
    // call the get() method without parameters to retrieve the singleton
    // instance
    ruleProcessor = (IRPRuleProcessor) ruleProcessorClass.getDeclaredMethod("get", new Class[0]).invoke(null, (Object[]) null);
    ruleProcessor.setContext(this);
  }


  /**
   * This method returns the actual turn number.
   *
   * @return actual turn number
   */
  public int getTurn() {
    return turn;
  }

  /**
   * gets the duration of a turn
   *
   * @return duration
   */
  public long getTurnDuration() {
    return turnDuration;
  }

  /**
   * This method finishes the thread that runs the RPServerManager. It calls the
   * RPWorld.onFinish() method.
   */
  public void finish() {
    keepRunning = false;

    while (isfinished == false) {
      Thread.yield();
    }

    try {
      world.onFinish();
    } catch (Exception e) {
      logger.error("error while finishing RPServerManager", e);
    }
  }

  /**
   * Adds an action for the next turn
   *
   * @param object
   *            the object that casted the action
   * @param action
   *            the action itself
   * @throws ActionInvalidException
   */
  public void addRPAction(RPObject object, RPAction action) throws ActionInvalidException {
    if (logger.isDebugEnabled()) {
      logger.debug("Added action: " + action);
    }

    scheduler.addRPAction(object, action, ruleProcessor);
  }

  /**
   * This method decide if an client runs a compatible version of the game
   *
   * @param game
   *            the game name
   * @param version
   *            the game version as a string
   * @return true if it is compatible.
   */
  public boolean checkGameVersion(String game, String version) {
    return ruleProcessor.checkGameVersion(game, version);
  }

  /**
   * Creates an account for a player in the game.
   *
   * @param username
   *            player's username
   * @param password
   *            player's password
   * @param email
   *            player's email
   * @param address
   *            ip address of client
   * @return a Result indicating if account creation was done successfully or not.
   */
  public AccountResult createAccount(String username, String password, String email, String address) {
    try {
      if (!Boolean.parseBoolean(Configuration.getConfiguration().get("allow_account_creation", "true"))) {
        return new AccountResult(Result.FAILED_CREATE_ON_MAIN_INSTEAD, username);
      }
    } catch (IOException e) {
      logger.error(e, e);
    }

    // check account creation limits
    try {
      if (DAORegister.get().get(AccountDAO.class).isAccountCreationLimitReached(address)) {
        return new AccountResult(Result.FAILED_TOO_MANY, username);
      }
    } catch (SQLException e) {
      logger.error(e, e);
      return new AccountResult(Result.FAILED_EXCEPTION, username);
    } catch (IOException e) {
      logger.error(e, e);
      return new AccountResult(Result.FAILED_EXCEPTION, username);
    }

    // forward the creation request to the game
    return ruleProcessor.createAccount(username, password, email);
  }

  /**
   * Creates a character for a account of a player
   *
   * @param username
   *            player's username
   * @param character
   * @param template
   *            the template we are going to use to create the object.
   * @param address
   *            ip address of client
   * @return a Result indicating if account creation was done successfully or
   *         if it is not the cause.
   */
  public CharacterResult createCharacter(String username, String character, RPObject template, String address) {
    try {
      if (!Boolean.parseBoolean(Configuration.getConfiguration().get("allow_account_creation", "true"))) {
        return new CharacterResult(Result.FAILED_CREATE_ON_MAIN_INSTEAD, character, template);
      }
    } catch (IOException e) {
      logger.error(e, e);
    }

    // check account creation limits
    try {
      if (DAORegister.get().get(CharacterDAO.class).isCharacterCreationLimitReached(username, address)) {
        return new CharacterResult(Result.FAILED_TOO_MANY, character, template);
      }
    } catch (SQLException e) {
      logger.error(e, e);
      return new CharacterResult(Result.FAILED_EXCEPTION, character, template);
    } catch (IOException e) {
      logger.error(e, e);
      return new CharacterResult(Result.FAILED_EXCEPTION, character, template);
    }
    return ruleProcessor.createCharacter(username, character, template);
  }

  private Perception getPlayerPerception(PlayerEntry entry) {
    Perception perception = null;

    IRPZone.ID id = new IRPZone.ID(entry.object.get("zoneid"));
    IRPZone zone = world.getRPZone(id);

    if (entry.requestedSync == false) {
      perception = zone.getPerception(entry.object, Perception.DELTA);
    } else {
      entry.requestedSync = false;
      perception = zone.getPerception(entry.object, Perception.SYNC);
    }

    return perception;
  }

  private void sendPlayerPerception(PlayerEntry entry, Perception perception, RPObject playerObject) {
    if (perception == null) {
      /** Until player enters game perception is null */
      return;
    }

    MessageS2CPerception messages2cPerception = new MessageS2CPerception(entry.channel,
            perception);

    stats.add("Perceptions " + (perception.type == 0 ? "DELTA" : "SYNC"), 1);

    /*
     * The perception is build of two parts: the general information and the
     * private information about our object. This private information
     * consists only of attributes that are not visible to every player but
     * the owner, because visible attributes are already stored in the
     * perception.
     */

    if (perception.type == Perception.SYNC) {
      RPObject copy = new RPObject();
      copy.fill(playerObject);
      if (!playerObject.isHidden()) {
        copy.clearVisible(true);
      }
      messages2cPerception.setMyRPObject(copy, null);
    } else {
      RPObject added = new RPObject();
      RPObject deleted = new RPObject();

      try {
        playerObject.getDifferences(added, deleted);
        if (!playerObject.isHidden()) {
          added.clearVisible(false);
          deleted.clearVisible(false);
        }

        if (added.size() == 0) {
          added = null;
        }

        if (deleted.size() == 0) {
          deleted = null;
        }
      } catch (Exception e) {
        logger.error("Error getting object differences", e);
        logger.error(playerObject);
        added = null;
        deleted = null;
      }
      messages2cPerception.setMyRPObject(added, deleted);
    }

    messages2cPerception.setClientID(entry.clientid);
    messages2cPerception.setPerceptionTimestamp(entry.getPerceptionTimestamp());
    messages2cPerception.setProtocolVersion(entry.getProtocolVersion());

    netMan.sendMessage(messages2cPerception);
  }

  private void buildPerceptions() {
    playersToRemove.clear();

    /** We reset the cache at Perceptions */
    MessageS2CPerception.clearPrecomputedPerception();

    for (PlayerEntry entry : playerContainer) {
      try {
        // Before creating the perception we check the player is still there.
        if(entry.isTimeout()) {
          logger.info("Request (TIMEOUT) disconnection of Player "+entry.channel);
          playersToRemove.add(entry);
          continue;
        }

        if (entry.state == ClientState.GAME_BEGIN) {
          Perception perception = getPlayerPerception(entry);
          sendPlayerPerception(entry, perception, entry.object);
        }
      } catch (Exception e) {
        logger.error("Removing player(" + entry.clientid + ") because it caused a Exception while contacting it", e);
        playersToRemove.add(entry);
      }

    }

    for (PlayerEntry entry : playersToRemove) {
      logger.warn("RP Disconnecting entry: " + entry);
      netMan.disconnectClient(entry.channel);
    }
  }

  /** This method is called when a player is added to the game */
  public boolean onInit(RPObject object) throws RPObjectInvalidException {
    return ruleProcessor.onInit(object);
  }

  /** This method is called when a player leaves the game */
  public boolean onExit(RPObject object) throws RPObjectNotFoundException {
    scheduler.clearRPActions(object);
    contentsToTransfer.remove(object);

    return ruleProcessor.onExit(object);
  }

  /** This method is called when connection to client is closed */
  public void onTimeout(RPObject object) throws RPObjectNotFoundException {
    scheduler.clearRPActions(object);
    contentsToTransfer.remove(object);

    ruleProcessor.onTimeout(object);
  }

  private void deliverTransferContent() {
    synchronized (contentsToTransfer) {
      for (Map.Entry<RPObject, List<TransferContent>> val : contentsToTransfer.entrySet()) {
        RPObject target = val.getKey();
        List<TransferContent> content = val.getValue();

        PlayerEntry entry = playerContainer.get(target);
        if(entry==null) {
          logger.warn("Entry for player ("+target+") does not exist: " + playerContainer, new Throwable());
          continue;
        }


        if (content == null) {
          logger.warn("content is null");
        }
        if (!entry.contentToTransfer.isEmpty()) {
          // prevent DoS if the client never confirms the Transfer offer
          if (entry.contentToTransfer.size() > 30) {
            synchronized (entry.contentToTransfer) {
              for (int i = 0; i < 10; i++) {
                entry.contentToTransfer.remove(0);
              }
            }
          }
          logger.warn("Adding to existing contentToTransfer for player " + entry.character + " old: " + entry.contentToTransfer + " added " + content);
        }
        entry.contentToTransfer.addAll(content);

        MessageS2CTransferREQ mes = new MessageS2CTransferREQ(entry.channel, content);
        mes.setClientID(entry.clientid);
        mes.setProtocolVersion(entry.getProtocolVersion());

        netMan.sendMessage(mes);
      }

      contentsToTransfer.clear();
    }
  }

  /** This method is triggered to send content to the clients */
  public void transferContent(RPObject target, List<TransferContent> content) {
    synchronized (contentsToTransfer) {
      contentsToTransfer.put(target, content);
    }
  }

  /** This method is triggered to send content to the clients */
  public void transferContent(RPObject target, TransferContent content) {
    List<TransferContent> list = new LinkedList<TransferContent>();
    list.add(content);

    transferContent(target, list);
  }

  @Override
  public void run() {
    try {
      long start = System.nanoTime();
      long stop;
      long delay;
      long timeStart = 0;
      long[] timeEnds = new long[12];

      while (keepRunning) {
        stop = System.nanoTime();

        logger.debug("Turn time elapsed: " + ((stop - start) / 1000) + " microsecs");
        delay = turnDuration - ((stop - start) / 1000000);
        if (delay < 0) {
          StringBuilder sb = new StringBuilder();
          for (long timeEnd : timeEnds) {
            sb.append(" " + (timeEnd - timeStart));
          }

          logger.warn("Turn duration overflow by " + (-delay) + " ms: "
                  + sb.toString());
        } else if (delay > turnDuration) {
          logger.error("Delay bigger than Turn duration. [delay: " + delay
                  + "] [turnDuration:" + turnDuration + "]");
          delay = 0;
        }

        // only sleep when the turn delay is > 0
        if (delay > 0) {
          try {
            Thread.sleep(delay);
          } catch (InterruptedException e) {
            // ignore
          }
        }

        start = System.nanoTime();
        timeStart = System.currentTimeMillis();

        playerContainer.getLock().requestWriteLock();

        try {
          timeEnds[0] = System.currentTimeMillis();

          /* Get actions that players send */
          scheduler.nextTurn();
          timeEnds[1] = System.currentTimeMillis();

          /* Execute them all */
          scheduler.visit(ruleProcessor);
          timeEnds[2] = System.currentTimeMillis();

          /* Compute game RP rules to move to the next turn */
          ruleProcessor.endTurn();
          timeEnds[3] = System.currentTimeMillis();

          /* Send content that is waiting to players */
          deliverTransferContent();
          timeEnds[4] = System.currentTimeMillis();

          /* Tell player what happened */
          buildPerceptions();
          timeEnds[5] = System.currentTimeMillis();

          /* save players regularly to the db */
          savePlayersPeriodicly();
          timeEnds[6] = System.currentTimeMillis();

          /* Move zone to the next turn */
          world.nextTurn();
          timeEnds[7] = System.currentTimeMillis();

          turn++;

          ruleProcessor.beginTurn();
          timeEnds[8] = System.currentTimeMillis();
        } finally {
          playerContainer.getLock().releaseLock();
          timeEnds[9] = System.currentTimeMillis();
        }
        try {
          stats.set("Objects now", world.size());
        } catch ( ConcurrentModificationException e) {
          //TODO: size is obviously not threadsafe as it asks the underlying zone.objects for its sizes, which are not threadsafe.
        }
        timeEnds[10] = System.currentTimeMillis();
        TransactionPool.get().kickHangingTransactionsOfThisThread();
        timeEnds[11] = System.currentTimeMillis();
      }
    } catch (Throwable e) {
      logger.error("Unhandled exception, server will shut down.", e);
    } finally {
      isfinished = true;
    }
  }

  private void savePlayersPeriodicly() {
    for (PlayerEntry entry : playerContainer) {
      try {
        // do not use = 0 because we need a little time until the
        // player object is fully initialized (e. g. has a charname)
        if (entry.getThisPerceptionTimestamp() % 2000 == 1999) {
          entry.storeRPObject(entry.object);
        }
      } catch(Exception e) {
        String name = "null";
        if (entry != null) {
          name = entry.character;
        }
        logger.error("Error while storing player " + name, e);
      }
    }
  }

  /**
   * This method disconnects a player from the server.
   *
   * @param object
   *            the player object that we want to disconnect from world
   */
  public void disconnectPlayer(RPObject object) {
    PlayerEntry entry = playerContainer.get(object);
    if (entry == null) {
      /*
       * There is no player entry for such channel This is not necesaryly
       * an error, as the connection could be anything else but an arianne
       * client or we are just disconnecting a player that logout
       * correctly.
       */
      logger.warn("There is no PlayerEntry associated to this RPObject.");
      return;
    }

    netMan.disconnectClient(entry.channel);
  }

  /**
   * This method exposes network layer connection validator so game logic can
   * handle it.
   *
   * @return the connection validator
   */
  public ConnectionValidator getValidator() {
    return netMan.getValidator();
  }
}
TOP

Related Classes of marauroa.server.game.rp.RPServerManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.