<commit_before>'use strict';

module.exports = Location;


var _ = require('lodash');
var assert = require('assert');
var config = require('config');
var GameObject = require('model/GameObject');
var Geo = require('model/Geo');
var Bag = require('model/Bag');
var Item = require('model/Item');
var IdObjRefMap = require('model/IdObjRefMap');
var OrderedHash = require('model/OrderedHash');
var pers = require('data/pers');
var RC = require('data/RequestContext');
var RQ = require('data/RequestQueue');
var rpc = require('data/rpc');
var util = require('util');
var utils = require('utils');


util.inherits(Location, GameObject);
Location.prototype.TSID_INITIAL = GameObject.prototype.TSID_INITIAL_LOCATION;


/**
 * Generic constructor for both instantiating an existing location
 * (from JSON data), and creating a new location.
 *
 * @param {object} [data] initialization values (properties are
 *        shallow-copied into the location object)
 * @param {Geo} [geo] geometry data (optional, for testing); if
 *        `undefined`, the respective `Geo` object is loaded from
 *        persistence
 * @constructor
 * @augments GameObject
 * @mixes LocationApi
 */
function Location(data, geo) {
	data = data || {};
	if (!data.tsid) {
		if (geo) {
			data.tsid = geo.getLocTsid();
		}
		else {
			data.tsid = rpc.makeLocalTsid(this.TSID_INITIAL_LOCATION);
		}
	}
	Location.super_.call(this, data);
	// initialize items and players, convert to IdObjRefMap
	this.players = new IdObjRefMap(this.players);
	this.items = new IdObjRefMap(this.items);
	// convert neighbor list to OrderedHash
	if (this.neighbors) {
		this.neighbors = new OrderedHash(this.neighbors);
	}
	// initialize geometry
	utils.addNonEnumerable(this, 'geometry');
	utils.addNonEnumerable(this, 'clientGeometry');
	utils.addNonEnumerable(this, 'geo');
	var geoData = geo || pers.get(this.getGeoTsid(), true);
	assert(_.isObject(geoData), 'no geometry data for ' + this);
	if (rpc.isLocal(this)) {
		this.updateGeo(geoData);
	}
}

utils.copyProps(require('model/LocationApi').prototype, Location.prototype);


// dummy property just so we can more easily "inherit" some functions from Bag
Object.defineProperty(Location.prototype, 'hiddenItems', {
	get: function get() {
		return {};
	},
});


// define activePlayers property as read-only alias for players
Object.defineProperty(Location.prototype, 'activePlayers', {
	get: function get() {
		return this.players;
	},
});


Location.prototype.gsOnLoad = function gsOnLoad() {
	Location.super_.prototype.gsOnLoad.call(this);
	// initialize request queue (not strictly necessary b/c it would be created
	// on demand, but this makes the logs easier to grok)
	this.getRQ();
	this.startUnloadInterval();
	// remove broken item refs in locations item list
	pers.clearStaleRefs(this, 'items');
	// clean up stale instance group references
	var instances = _.get(this, 'instances.instances', {});
	for (var k in instances) {
		pers.clearStaleRefs(instances, k);
		if (!instances[k].length) delete instances[k];
	}
};


Location.prototype.gsOnCreate = function gsOnCreate() {
	Location.super_.prototype.gsOnCreate.call(this);
	if (this.class_tsid !== 'home') {  // POLs are unloaded after creation anyway
		this.startUnloadInterval();
	}
};


Location.prototype.startUnloadInterval = function startUnloadInterval() {
	// periodically check whether location can be released from memory
	var unloadInt = config.get('pers:locUnloadInt', null);
	if (unloadInt) {
		this.setGsTimer({fname: 'checkUnload', delay: unloadInt, interval: true,
			internal: true});
	}
};


/**
 * Creates a new `Location` instance and adds it to persistence.
 *
 * @param {Geo} geo geometry data (location TSID will be derived from
 *        `geo.tsid`)
 * @param {object} [data] additional properties
 * @returns {object} a `Location` object
 */
Location.create = function create(geo, data) {
	data = data || {};
	data.tsid = geo.getLocTsid();
	data.class_tsid = data.class_tsid || 'town';
	return pers.create(Location, data);
};


/**
 * Creates a copy of a template location.
 *
 * @param {Location} src the location to copy
 * @param {object} options settings for the copied location
 * @param {string} options.label label for new location
 * @param {string} options.moteId mote ID for new location
 * @param {string} options.hubId hub ID for new location
 * @param {boolean} options.isInstance is the copied location an instance
 * @param {string} [options.classTsid] alternate class of new location (source
 *        location class by default)
 * @returns {Location|null} the copied location, or `null` if an instance copy
 *          was requested, but the given source is not an instance template
 */
Location.copy = function copy(src, options) {
	if (options.isInstance && !src.instance_me) {
		// temporarily fail gracefully/return null here because of the door to
		// GG in the placeholder location, otherwise instancing fails when quest
		// locations are instanced with `preserve_links` === false
		log.warn('not copying %s (not an instance template)', src);
		return null;
	}
	var isPol = options.classTsid === 'home';
	var geo = Geo.copy(src.geometry, options.label, isPol);
	var ret = Location.create(geo, {
		class_tsid: options.classTsid || src.class_tsid,
		label: options.label,
		moteid: options.moteId,
		hubid: options.hubId,
		is_instance: options.isInstance,
	});
	if (options.isInstance) {
		ret.instance_of = src.tsid;
	}
	ret.copyProps(src, ['class_tsid', 'label', 'moteid', 'hubid', 'instance_me',
		'is_instance', 'instances', 'players', 'items']);
	for (var k in src.items) {
		Item.copy(src.items[k], ret);
	}
	if (!isPol) ret.onCreateAsCopyOf(src);
	else {
		// ensure geo data is initialized (constructor may have skipped it)
		ret.updateGeo(geo);
		// make sure loc (and geo/items) don't stay in persistence cache, as
		// this GS (that created it) may not be the one that is managing it
		RC.getContext().setUnload(ret);
	}
	return ret;
};


/**
 * Retrieves the request queue for this location.
 *
 * @returns {RequestQueue} the request queue for this location
 */
Location.prototype.getRQ = function getRQ() {
	return RQ.get(this);
};


/**
 * Schedules this location, its geometry object and all items in it for
 * deletion after the current request.
 */
Location.prototype.del = function del() {
	assert(Object.keys(this.players).length === 0, 'there are people here!');
	for (var k in this.items) {
		this.items[k].del();
	}
	this.geometry.del();
	Location.super_.prototype.del.call(this);
};


/**
 * Adds a player to the list of players in this location
 *
 * @param {Player} player the player to add
 */
Location.prototype.addPlayer = function addPlayer(player) {
	this.players[player.tsid] = player;
};


/**
 * Calls GSJS event handlers for a player entering a location.
 *
 * @param {Player} player the player that is entering the location
 */
Location.prototype.gsOnPlayerEnter = function gsOnPlayerEnter(player) {
	if (this.onPlayerEnter) {
		this.rqPush(this.onPlayerEnter, player);
	}
	for (var k in this.items) {
		var it = this.items[k];
		if (it && it.onPlayerEnter) {
			try {
				it.rqPush(it.onPlayerEnter, player);
			}
			catch (e) {
				log.error(e, 'error in %s.onPlayerEnter handler', it);
			}
		}
	}
};


/**
 * Removes a player from the list of players in this location and
 * calls various GSJS "onExit" event handlers.
 *
 * @param {Player} player the player to remove
 * @param {Location} [newLoc] the location the player is moving to
 *        (`undefined` during logout)
 */
Location.prototype.removePlayer = function removePlayer(player, newLoc) {
	delete this.players[player.tsid];
	if (this.onPlayerExit) {
		this.onPlayerExit(player, newLoc);
	}
	for (var k in this.items) {
		var it = this.items[k];
		if (it && it.onPlayerExit) {
			try {
				it.rqPush(it.onPlayerExit, player);
			}
			catch (e) {
				log.error(e, 'error in %s.onPlayerExit handler', it);
			}
		}
	}
};


/**
 * Checks whether it is possible to unload the location, and does so
 * if it is (called by an interval set up in the constructor).
 * @private
 */
Location.prototype.checkUnload = function checkUnload() {
	// don't unload if there are people around
	for (var tsid in this.players) {
		if (this.players[tsid].isConnected()) return;
	}
	// don't unload if anything is busy growing (e.g. jellisacs, barnacles)
	for (var k in this.items) {
		var it = this.items[k];
		if (it.gsTimers.onGrow) {
			log.debug('not unloading %s, %s is still growing here', this, it);
			return;
		}
		if (it.is_running) {
			log.debug('not unloading %s, %s is still making here', this, it);
			return;
		}
	}
	// still here? go ahead, then
	var self = this;
	this.unload(function cb(err) {
		if (err) log.error(err, 'failed to unload %s', self);
	});
};


/**
 * Schedules this location and its associated geometry object to be released
 * from the live object cache after all pending requests for it have been
 * handled. When this is called, the location's request queue will not accept
 * any new requests.
 *
 * @param {function} [callback] for optional error handling
 */
Location.prototype.unload = function unload(callback) {
	var self = this;
	this.getRQ().push('unload', function unloadReq() {
		for (var tsid in self.items) {
			// unload all items first to stop timers etc
			self.items[tsid].unload();
		}
		Location.super_.prototype.unload.call(self);
		self.geometry.unload();
	}, callback, {close: true, obj: this});
};


/**
 * Gets the TSID of the {@link Geo} object for this location.
 *
 * @returns {string} TSID of the corresponding {@link Geo} object
 */<commit_msg>Do not persist players on location unload<commit_after>'use strict';

module.exports = Location;


var _ = require('lodash');
var assert = require('assert');
var config = require('config');
var GameObject = require('model/GameObject');
var Geo = require('model/Geo');
var Bag = require('model/Bag');
var Item = require('model/Item');
var IdObjRefMap = require('model/IdObjRefMap');
var OrderedHash = require('model/OrderedHash');
var pers = require('data/pers');
var RC = require('data/RequestContext');
var RQ = require('data/RequestQueue');
var rpc = require('data/rpc');
var util = require('util');
var utils = require('utils');


util.inherits(Location, GameObject);
Location.prototype.TSID_INITIAL = GameObject.prototype.TSID_INITIAL_LOCATION;


/**
 * Generic constructor for both instantiating an existing location
 * (from JSON data), and creating a new location.
 *
 * @param {object} [data] initialization values (properties are
 *        shallow-copied into the location object)
 * @param {Geo} [geo] geometry data (optional, for testing); if
 *        `undefined`, the respective `Geo` object is loaded from
 *        persistence
 * @constructor
 * @augments GameObject
 * @mixes LocationApi
 */
function Location(data, geo) {
	data = data || {};
	if (!data.tsid) {
		if (geo) {
			data.tsid = geo.getLocTsid();
		}
		else {
			data.tsid = rpc.makeLocalTsid(this.TSID_INITIAL_LOCATION);
		}
	}
	Location.super_.call(this, data);
	// initialize items and players, convert to IdObjRefMap
	this.players = new IdObjRefMap(this.players);
	this.items = new IdObjRefMap(this.items);
	// convert neighbor list to OrderedHash
	if (this.neighbors) {
		this.neighbors = new OrderedHash(this.neighbors);
	}
	// initialize geometry
	utils.addNonEnumerable(this, 'geometry');
	utils.addNonEnumerable(this, 'clientGeometry');
	utils.addNonEnumerable(this, 'geo');
	var geoData = geo || pers.get(this.getGeoTsid(), true);
	assert(_.isObject(geoData), 'no geometry data for ' + this);
	if (rpc.isLocal(this)) {
		this.updateGeo(geoData);
	}
}

utils.copyProps(require('model/LocationApi').prototype, Location.prototype);


// dummy property just so we can more easily "inherit" some functions from Bag
Object.defineProperty(Location.prototype, 'hiddenItems', {
	get: function get() {
		return {};
	},
});


// define activePlayers property as read-only alias for players
Object.defineProperty(Location.prototype, 'activePlayers', {
	get: function get() {
		return this.players;
	},
});


Location.prototype.gsOnLoad = function gsOnLoad() {
	Location.super_.prototype.gsOnLoad.call(this);
	// initialize request queue (not strictly necessary b/c it would be created
	// on demand, but this makes the logs easier to grok)
	this.getRQ();
	this.startUnloadInterval();
	// remove broken item refs in locations item list
	pers.clearStaleRefs(this, 'items');
	// clean up stale instance group references
	var instances = _.get(this, 'instances.instances', {});
	for (var k in instances) {
		pers.clearStaleRefs(instances, k);
		if (!instances[k].length) delete instances[k];
	}
};


Location.prototype.gsOnCreate = function gsOnCreate() {
	Location.super_.prototype.gsOnCreate.call(this);
	if (this.class_tsid !== 'home') {  // POLs are unloaded after creation anyway
		this.startUnloadInterval();
	}
};


Location.prototype.startUnloadInterval = function startUnloadInterval() {
	// periodically check whether location can be released from memory
	var unloadInt = config.get('pers:locUnloadInt', null);
	if (unloadInt) {
		this.setGsTimer({fname: 'checkUnload', delay: unloadInt, interval: true,
			internal: true});
	}
};


/**
 * Creates a new `Location` instance and adds it to persistence.
 *
 * @param {Geo} geo geometry data (location TSID will be derived from
 *        `geo.tsid`)
 * @param {object} [data] additional properties
 * @returns {object} a `Location` object
 */
Location.create = function create(geo, data) {
	data = data || {};
	data.tsid = geo.getLocTsid();
	data.class_tsid = data.class_tsid || 'town';
	return pers.create(Location, data);
};


/**
 * Creates a copy of a template location.
 *
 * @param {Location} src the location to copy
 * @param {object} options settings for the copied location
 * @param {string} options.label label for new location
 * @param {string} options.moteId mote ID for new location
 * @param {string} options.hubId hub ID for new location
 * @param {boolean} options.isInstance is the copied location an instance
 * @param {string} [options.classTsid] alternate class of new location (source
 *        location class by default)
 * @returns {Location|null} the copied location, or `null` if an instance copy
 *          was requested, but the given source is not an instance template
 */
Location.copy = function copy(src, options) {
	if (options.isInstance && !src.instance_me) {
		// temporarily fail gracefully/return null here because of the door to
		// GG in the placeholder location, otherwise instancing fails when quest
		// locations are instanced with `preserve_links` === false
		log.warn('not copying %s (not an instance template)', src);
		return null;
	}
	var isPol = options.classTsid === 'home';
	var geo = Geo.copy(src.geometry, options.label, isPol);
	var ret = Location.create(geo, {
		class_tsid: options.classTsid || src.class_tsid,
		label: options.label,
		moteid: options.moteId,
		hubid: options.hubId,
		is_instance: options.isInstance,
	});
	if (options.isInstance) {
		ret.instance_of = src.tsid;
	}
	ret.copyProps(src, ['class_tsid', 'label', 'moteid', 'hubid', 'instance_me',
		'is_instance', 'instances', 'players', 'items']);
	for (var k in src.items) {
		Item.copy(src.items[k], ret);
	}
	if (!isPol) ret.onCreateAsCopyOf(src);
	else {
		// ensure geo data is initialized (constructor may have skipped it)
		ret.updateGeo(geo);
		// make sure loc (and geo/items) don't stay in persistence cache, as
		// this GS (that created it) may not be the one that is managing it
		RC.getContext().setUnload(ret);
	}
	return ret;
};


/**
 * Retrieves the request queue for this location.
 *
 * @returns {RequestQueue} the request queue for this location
 */
Location.prototype.getRQ = function getRQ() {
	return RQ.get(this);
};


/**
 * Schedules this location, its geometry object and all items in it for
 * deletion after the current request.
 */
Location.prototype.del = function del() {
	assert(Object.keys(this.players).length === 0, 'there are people here!');
	for (var k in this.items) {
		this.items[k].del();
	}
	this.geometry.del();
	Location.super_.prototype.del.call(this);
};


/**
 * Adds a player to the list of players in this location
 *
 * @param {Player} player the player to add
 */
Location.prototype.addPlayer = function addPlayer(player) {
	this.players[player.tsid] = player;
};


/**
 * Calls GSJS event handlers for a player entering a location.
 *
 * @param {Player} player the player that is entering the location
 */
Location.prototype.gsOnPlayerEnter = function gsOnPlayerEnter(player) {
	if (this.onPlayerEnter) {
		this.rqPush(this.onPlayerEnter, player);
	}
	for (var k in this.items) {
		var it = this.items[k];
		if (it && it.onPlayerEnter) {
			try {
				it.rqPush(it.onPlayerEnter, player);
			}
			catch (e) {
				log.error(e, 'error in %s.onPlayerEnter handler', it);
			}
		}
	}
};


/**
 * Removes a player from the list of players in this location and
 * calls various GSJS "onExit" event handlers.
 *
 * @param {Player} player the player to remove
 * @param {Location} [newLoc] the location the player is moving to
 *        (`undefined` during logout)
 */
Location.prototype.removePlayer = function removePlayer(player, newLoc) {
	delete this.players[player.tsid];
	if (this.onPlayerExit) {
		this.onPlayerExit(player, newLoc);
	}
	for (var k in this.items) {
		var it = this.items[k];
		if (it && it.onPlayerExit) {
			try {
				it.rqPush(it.onPlayerExit, player);
			}
			catch (e) {
				log.error(e, 'error in %s.onPlayerExit handler', it);
			}
		}
	}
};


/**
 * Checks whether it is possible to unload the location, and does so
 * if it is (called by an interval set up in the constructor).
 * @private
 */
Location.prototype.checkUnload = function checkUnload() {
	// don't unload if there are people around
	for (var tsid in this.players) {
		if (this.players[tsid].isConnected()) return;
	}
	// don't unload if anything is busy growing (e.g. jellisacs, barnacles)
	for (var k in this.items) {
		var it = this.items[k];
		if (it.gsTimers.onGrow) {
			log.debug('not unloading %s, %s is still growing here', this, it);
			return;
		}
		if (it.is_running) {
			log.debug('not unloading %s, %s is still making here', this, it);
			return;
		}
	}
	// still here? go ahead, then
	var self = this;
	this.unload(function cb(err) {
		if (err) log.error(err, 'failed to unload %s', self);
	});
};


/**
 * Schedules this location and its associated geometry object to be released
 * from the live object cache after all pending requests for it have been
 * handled. When this is called, the location's request queue will not accept
 * any new requests.
 *
 * @param {function} [callback] for optional error handling
 */
Location.prototype.unload = function unload(callback) {
	var self = this;
	this.getRQ().push('unload', function unloadReq() {
		for (var tsid in self.items) {
			// unload all items first to stop timers etc
			self.items[tsid].unload();
		}
		// empty out player list as we do not want to persist any
		// lingering references
		self.players = [];
		Location.super_.prototype.unload.call(self);
		self.geometry.unload();
	}, callback, {close: true, obj: this});
};


/**
 * Gets the TSID of the {@link Geo} object for this location.
 *
 * @returns {string} TSID of the corresponding {@link Geo} object
 */
{'fraction_non_alphanumeric': 0.13075849232201023, 'fraction_numerical': 0.00010340726953104803, 'mean_word_length': 4.02520135100026, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1319357', 'n_tokens_mistral': 6424, 'n_tokens_neox': 6093, 'n_words': 1941}