// ACCleanupAndSpawnBot.js // // Created by Thijs Wenker on 9/26/2016 // // This Assignment Client script cleans up a the defined // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // Times when the cleanup script runs (on a daily basis 00:00 -> 23:59) var RUN_WHEN = [ '0:00', '2:00', '4:00', '6:00', '8:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00' ]; // Cleanup filters // // Allows the following entry-types: // Name (string) eg: 'This is a cube' // Name Regular Expression (RexExp) eg: /Cube[0-9]+/ // Filter Object (JS Object eg: // { // name: 'Cube', // locked: false, // visible: true // } var CLEANUP_FILTER = [ { locked: 0, clientOnly: 0 } ]; // The script will only run in the domain with the following placename: // (The PLACENAME_LOCK is only effective when the script is ran in Interface, not for Assignment Client scripts) var PLACENAME_LOCK = 'staging-welcome'; var DEFAULT_LIFETIME = 1800; var JSON_PATH = 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/'; // IMPORT_ENTITIES supports JSON exported entities using importEntitiesJSON(URL, parentProperties); e.g: // importEntitiesJSON('https://www.acme.com/example/cubes.json', {position: MyAvatar.position}) var IMPORT_ENTITIES = [ importEntitiesJSON(JSON_PATH + 'seesaw.json', { position: {x: 55.613121032714844, y: 0.29320013523101807, z: -94.694454193115234} }), importEntitiesJSON(JSON_PATH + 'domino.json', { position: {x: 33.79003143310547, y: 1.714470656013, z: -153.43145751953125} }), importEntitiesJSON(JSON_PATH + 'castle7.json', { position: {x: -60.238300323486328, y: -0.2793, z: -145.61320114135742}, rotation: Quat.fromPitchYawRollDegrees(0.0, 65.0, 0.0) }), importEntitiesJSON(JSON_PATH + 'eventCalendar.json', { position: {x: -6.2534, y: 1.3659, z: -61.6625} }), importEntitiesJSON(JSON_PATH + 'soccerCones.json', { position: {x: 117.21154022216797 - 3.8746109008789062, y: -0.5399885773658752, z: -162.28115844726562} }), importEntitiesJSON(JSON_PATH + '3SetBBallFeb13.json', { position: {x: 122.96149298095703, y: 0.216986283659935, z: -138.96031694335937} }, { lifetime: DEFAULT_LIFETIME }), importEntitiesJSON(JSON_PATH + '3SetRBallJan6.json', { position: {x: 104.59093475341797, y: 0.26469510793685913, z: -131.70911407470703} }, { lifetime: DEFAULT_LIFETIME }), importEntitiesJSON(JSON_PATH + '3SetSBall.json', { position: {x: 111.51567840576172, y: 0.2618212401866913, z: -161.02066802978516} }, { lifetime: DEFAULT_LIFETIME }), importEntitiesJSON(JSON_PATH + '4SetFrisbee.json', { position: {x: 111.51, y: 0.21, z: -143.679} }, { lifetime: DEFAULT_LIFETIME }), importEntitiesJSON(JSON_PATH + '3SetHorseshoesFeb13.json', { position: {x: 99.16116333007812, y: -0.14801521599292755, z: -149.36492919921875} }, { lifetime: DEFAULT_LIFETIME }), importEntitiesJSON(JSON_PATH + 'maracas.json', { position: {x: -11.144107818603516, y: 0.23886066675186157, z: -168.52725219726562} }), importEntitiesJSON(JSON_PATH + 'xylophoneRev2.json', { position: {x: -11.054655075073242, y: -0.872685432434082, z: -167.0999298095703} }), importEntitiesJSON(JSON_PATH + 'cushions.json', { position: {x: -32.708541870117188, y: -0.37946939468383789, z: -253.29977416992188} }), importEntitiesJSON(JSON_PATH + 'airplaneApr28_1.json', { position: {x: 0.0, y: 0.0, z: -113.47845458984375} }), importEntitiesJSON('http://mpassets.highfidelity.com/a250c5bc-1ee1-4c34-a1d0-7a6acbcaa989-v1/ping_pong_table_Apr28.json', { position: {x: 95.0783, y: -0.1715, z: -134.0455} }), importEntitiesJSON(JSON_PATH + 'coffeeCups.json', { position: {x: 0.0, y: 0.0, z: -63.223968505859375} }), ]; // Mainly for importing creation scripts (that contain hold/sprint or any other actions) // Make sure that the scripts that are put here do not Script.stop() or leave in any timers/event handlers // If you really want to listen to events or have a timer, then you could make the scripts events/timer stop after a specific item that was created in the script gets cleaned up var IMPORT_SCRIPTS = [ // Removed the following script until Physics Actions can be created by AC scripts // importScript('https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/createTetherball.js', { // polePosition: {x: 119.29138946533203, y: 1.8446629047393799, z: -101.87809753417969} // }), importScript('https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/resetShortbow.js', { rootPosition: {"x":-40.68001556396484,"y":5.327760219573975,"z":-99.38505554199219} }), importScript('https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/boppo/createElBoppo.js', { rootPosition: {"x":84.25203990478515,"y":1.187756896018982,"z":-148.57151611328126} }) ]; var CLEANUP_AUDIO_TRACKS_PREFIX = 'http://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Sounds/'; var CLEANUP_AUDIO_BROADCAST_POSITIONS = [ {x: 8.0, y: 5.0, z: -58.0}, // hangar X {x: 110.3, y: 5.0, z: -144.0}, // hangar Y {x: -3.7, y: 5.0, z: -183.5} // hangar Z ]; // No properties set at the moment. var CLEANUP_AUDIO_BROADCAST_PROPERTIES = { }; var CLEANUP_AUDIO_TRACKS = [ { name: 'good_morning', url: CLEANUP_AUDIO_TRACKS_PREFIX + 'VOGGoodMorning_OneMin.wav', startTime: '0:00', endTime: '11:59', cleanupOffsetSeconds: -60, broadcastPositions: CLEANUP_AUDIO_BROADCAST_POSITIONS, properties: CLEANUP_AUDIO_BROADCAST_PROPERTIES }, { name: 'good_afternoon', url: CLEANUP_AUDIO_TRACKS_PREFIX + 'VOGGoodAfternoon_OneMin.wav', startTime: '12:00', endTime: '16:59', cleanupOffsetSeconds: -60, broadcastPositions: CLEANUP_AUDIO_BROADCAST_POSITIONS, properties: CLEANUP_AUDIO_BROADCAST_PROPERTIES }, { name: 'good_evening', url: CLEANUP_AUDIO_TRACKS_PREFIX + 'VOGGoodEvening_OneMin.wav', startTime: '17:00', endTime: '23:59', cleanupOffsetSeconds: -60, broadcastPositions: CLEANUP_AUDIO_BROADCAST_POSITIONS, properties: CLEANUP_AUDIO_BROADCAST_PROPERTIES }, { name: 'countdown', url: CLEANUP_AUDIO_TRACKS_PREFIX + 'VOGCountdown.wav', startTime: '0:00', endTime: '23:59', cleanupOffsetSeconds: -8.7, broadcastPositions: CLEANUP_AUDIO_BROADCAST_POSITIONS, properties: CLEANUP_AUDIO_BROADCAST_PROPERTIES }, { name: 'five_minutes_warning', url: CLEANUP_AUDIO_TRACKS_PREFIX + 'VOGFiveMin.wav', startTime: '0:00', endTime: '23:59', cleanupOffsetSeconds: -300, broadcastPositions: CLEANUP_AUDIO_BROADCAST_POSITIONS, properties: CLEANUP_AUDIO_BROADCAST_PROPERTIES } ]; // You can use DRY RUN mode to test the cleanup filter and see which entities will be // deleted without modifying anything var DRY_RUN = false; var SEARCH_CENTER = {x: 19.2, y: 0.1, z: -135.6}; var SEARCH_AREA = 60000; // search area (sphere) in meters radius // Allows you to manually trigger the cleanups through the messages system var REMOTE_TRIGGER_ENABLED = true; var TRIGGER_CHANNEL = 'cleanUpAndSpawnBot'; var CLEAN_MESSAGE = 'cleanUp'; var SPAWN_MESSAGE = 'spawn'; var CLEAN_AND_SPAWN_MESSAGE = 'cleanUpAndSpawn'; var DEBUG_SCRIPT_OVER_CHANNEL = ''; // Constants var HOURS_IN_DAY = 24; var MINUTES_IN_HOUR = 60; var SECONDS_PER_MINUTE = 60; var MILLISECONDS_PER_SECOND = 1000; var MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR; var TIMER_TICKS_PER_MINUTE = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND; var ZERO_UUID = '{00000000-0000-0000-0000-000000000000}'; var IS_AC_SCRIPT = (this.Agent !== undefined); var SANETIZE_PROPERTIES = ['childEntities', 'parentID', 'id']; function preloadCleanupAudioTracks() { CLEANUP_AUDIO_TRACKS.forEach(function(cleanupAudioTrack) { cleanupAudioTrack.sound = SoundCache.getSound(cleanupAudioTrack.url); }); } function isCorrectPlace() { return location !== undefined && location.hostname === PLACENAME_LOCK; } var debug = function(message) { print(message); if (DEBUG_SCRIPT_OVER_CHANNEL !== '') { Messages.sendMessage(DEBUG_SCRIPT_OVER_CHANNEL, message); } }; function entityListToTree(entitiesList) { function entityListToTreeRecursive(properties) { properties.childEntities = []; entitiesList.forEach(function(entityProperties) { if (properties.id === entityProperties.parentID) { properties.childEntities.push(entityListToTreeRecursive(entityProperties)); } }); return properties; } var entityTree = []; entitiesList.forEach(function(entityProperties) { if (entityProperties.parentID === undefined || entityProperties.parentID === ZERO_UUID) { entityTree.push(entityListToTreeRecursive(entityProperties)); } }); return entityTree; } function importEntitiesJSON(importLink, parentProperties, overrideProperties) { if (parentProperties === undefined) { parentProperties = {}; } if (overrideProperties !== undefined) { parentProperties.overrideProperties = overrideProperties; } try { parentProperties.childEntities = entityListToTree(Script.require(importLink).Entities); return parentProperties; } catch (e) { debug('Failed importing entities JSON because: ' + JSON.stringify(e)); } return null; } function importScript(importScript, properties) { var request = new XMLHttpRequest(); request.open('GET', importScript, false); request.send(); return {script: request.responseText, properties: properties}; } // Creates an entity and returns a mixed object of the creation properties and the assigned entityID var createEntity = function(entityProperties, parent, overrideProperties) { // JSON.stringify -> JSON.parse trick to create a fresh copy of JSON data var newEntityProperties = JSON.parse(JSON.stringify(entityProperties)); if (overrideProperties !== undefined) { Object.keys(overrideProperties).forEach(function(key) { newEntityProperties[key] = overrideProperties[key]; }); } if (parent.rotation !== undefined) { if (newEntityProperties.rotation !== undefined) { newEntityProperties.rotation = Quat.multiply(parent.rotation, newEntityProperties.rotation); } else { newEntityProperties.rotation = parent.rotation; } } if (parent.position !== undefined) { var localPosition = (parent.rotation !== undefined) ? Vec3.multiplyQbyV(parent.rotation, newEntityProperties.position) : newEntityProperties.position; newEntityProperties.position = Vec3.sum(localPosition, parent.position); } if (parent.id !== undefined) { newEntityProperties.parentID = parent.id; } newEntityProperties.id = Entities.addEntity(newEntityProperties); return newEntityProperties; }; var createEntitiesFromTree = function(entityTree, parent, overrideProperties) { if (parent === undefined) { parent = {}; } if (parent.overrideProperties !== undefined) { overrideProperties = parent.overrideProperties; } var createdTree = []; entityTree.forEach(function(entityProperties) { var sanetizedProperties = {}; Object.keys(entityProperties).forEach(function(propertyKey) { if (!entityProperties.hasOwnProperty(propertyKey) || SANETIZE_PROPERTIES.indexOf(propertyKey) !== -1) { return true; } sanetizedProperties[propertyKey] = entityProperties[propertyKey]; }); // Allow for non-entity parent objects, this allows us to offset groups of entities to a specific position/rotation var parentProperties = sanetizedProperties; if (entityProperties.type !== undefined) { parentProperties = createEntity(sanetizedProperties, parent, overrideProperties); } if (entityProperties.childEntities !== undefined) { parentProperties.childEntities = createEntitiesFromTree(entityProperties.childEntities, parentProperties, overrideProperties); } createdTree.push(parentProperties); }); return createdTree; }; function playCleanupAudioTrack(audioTrack) { debug('Playing audio track: ' + audioTrack.name); audioTrack.broadcastPositions.forEach(function(broadcastPosition) { var properties = audioTrack.properties; properties.position = broadcastPosition; Audio.playSound(audioTrack.sound, properties); }); } function timeStringToMinutesInDay(timeText) { try { var timeArray = timeText.split(':'); var hour = parseInt(timeArray[0]); var minute = parseInt(timeArray[1]); return (MINUTES_IN_HOUR * hour) + minute; } catch (e) { debug('Had some trouble while parsing the following time: ' + timeText); } return -1; } function getCurrentMinuteInDay() { var now = new Date(); var currentMinuteInDay = (MINUTES_IN_HOUR * now.getUTCHours()) + now.getUTCMinutes(); return currentMinuteInDay; } function getTimeUntilNextRun(times) { var parsedTimes = []; var currentMinuteInDay = getCurrentMinuteInDay(); times.forEach(function(timeText) { var minuteInDay = timeStringToMinutesInDay(timeText); var minutesToNextRun = minuteInDay - currentMinuteInDay + (minuteInDay <= currentMinuteInDay ? MINUTES_IN_DAY : 0); parsedTimes.push({ timeText: timeText, timerTicksUntilNextRun: TIMER_TICKS_PER_MINUTE * minutesToNextRun, minuteInDay: minuteInDay, minutesToNextRun: minutesToNextRun }); }); var lowestParsedTime = null; parsedTimes.forEach(function(parsedTime) { if (lowestParsedTime === null || parsedTime.minutesToNextRun < lowestParsedTime.minutesToNextRun) { lowestParsedTime = parsedTime; } }); if (lowestParsedTime === null) { debug('Something went wrong while retrieving the next run time.'); return null; } return lowestParsedTime; } function triggerEventOn(times, callback) { var parsedNextRunTime = getTimeUntilNextRun(times); var timeout = parsedNextRunTime.timerTicksUntilNextRun; debug('Set timeout to ' + timeout + ' ms.'); Script.setTimeout(callback, timeout); return parsedNextRunTime; } function planAudioForCleanupEvent(nextCleanupParsedTime) { CLEANUP_AUDIO_TRACKS.forEach(function(audioTrack) { var startTimeMinutes = timeStringToMinutesInDay(audioTrack.startTime); var endTimeMinutes = timeStringToMinutesInDay(audioTrack.endTime); debug(startTimeMinutes + ' ' + endTimeMinutes); if (startTimeMinutes <= nextCleanupParsedTime.minuteInDay && endTimeMinutes >= nextCleanupParsedTime.minuteInDay) { debug('Planned to play ' + audioTrack.name + ' for the next scheduled cleanup'); var timerTicksUntilNextRun = nextCleanupParsedTime.timerTicksUntilNextRun + (audioTrack.cleanupOffsetSeconds * MILLISECONDS_PER_SECOND); Script.setTimeout(function() { playCleanupAudioTrack(audioTrack); }, timerTicksUntilNextRun); } }); } function setupNextCleanupAndAudioEvent() { var nextCleanupParsedTime = triggerEventOn(RUN_WHEN, doCleanup); planAudioForCleanupEvent(nextCleanupParsedTime); } var doCleanup = function() { debug('Doing Cleanup and Spawn.'); purgeEntities(); importEntities(); importScripts(); setupNextCleanupAndAudioEvent(); }; var testFilter = function(filter, properties) { var testFilter = typeof(filter) === 'object' ? filter : {name: filter}; for (var key in testFilter) { if (!testFilter.hasOwnProperty(key)) { continue; } if (testFilter[key] !== properties[key]) { return false; } } return true; }; var purgeEntities = function() { if (!IS_AC_SCRIPT && !isCorrectPlace()) { debug('The PLACENAME_LOCK does not match the current placename, aborting purgeEntities.'); return; } var propertiesToRequest = ['name']; CLEANUP_FILTER.forEach(function(filter) { if (typeof(filter) === 'object') { Object.keys(filter).forEach(function(property) { if (propertiesToRequest.indexOf(property) === -1) { propertiesToRequest.push(property); } }); } }); Entities.findEntities(SEARCH_CENTER, SEARCH_AREA).forEach(function(entityID) { var entityProperties = Entities.getEntityProperties(entityID, propertiesToRequest); var deleteEntity = false; CLEANUP_FILTER.forEach(function(filter) { if (testFilter(filter, entityProperties)) { deleteEntity = true; return true; } }); if (deleteEntity) { debug(DRY_RUN ? '[DRY_RUN] Would have removed: ' : 'removing: ' + entityProperties.id + ' ' + entityProperties.name); if (!DRY_RUN) { Entities.deleteEntity(entityProperties.id); } } }); }; var importScripts = function() { if (DRY_RUN) { debug('Skipping importScripts for the DRY_RUN.'); return {}; } if (!IS_AC_SCRIPT && !isCorrectPlace()) { debug('The PLACENAME_LOCK does not match the current placename, aborting importEntities.'); return {}; } IMPORT_SCRIPTS.forEach(function(importScript) { // eslint-disable-next-line no-unused-vars var SCRIPT_IMPORT_PROPERTIES = importScript.properties; this.isCleanupAndSpawnScript = true; eval(importScript.script); }); }; var importEntities = function() { if (DRY_RUN) { debug('Skipping importEntities for the DRY_RUN.'); return {}; } if (!IS_AC_SCRIPT && !isCorrectPlace()) { debug('The PLACENAME_LOCK does not match the current placename, aborting importEntities.'); return {}; } return createEntitiesFromTree(IMPORT_ENTITIES); }; preloadCleanupAudioTracks(); setupNextCleanupAndAudioEvent(); if (REMOTE_TRIGGER_ENABLED) { Messages.messageReceived.connect(function(channel, message, senderUUID, localOnly) { if (channel !== TRIGGER_CHANNEL) { return; } if (message === CLEAN_MESSAGE || message === CLEAN_AND_SPAWN_MESSAGE) { purgeEntities(); } if (message === SPAWN_MESSAGE || message === CLEAN_AND_SPAWN_MESSAGE) { importEntities(); importScripts(); } }); Messages.subscribe(TRIGGER_CHANNEL); } // Assignment Client related code: if (IS_AC_SCRIPT) { Agent.isAvatar = true; Avatar.skeletonModelURL = 'http://hifi-content.s3.amazonaws.com/ozan/dev/avatars/invisible_avatar/invisible_avatar.fst'; var initialized = false; var update = function(deltaTime) { if (!initialized) { if (Entities.serversExist() && Entities.canRez()) { Entities.setPacketsPerSecond(60000); EntityViewer.setPosition(SEARCH_CENTER); EntityViewer.setCenterRadius(SEARCH_AREA); // This should allow us to see nano-scale entities from great distances EntityViewer.setVoxelSizeScale(Number.MAX_VALUE); Script.setInterval(function() { EntityViewer.queryOctree(); }, 1000); initialized = true; Script.update.disconnect(update); } return; } }; Script.update.connect(update); }