package ageb.modules.avatar.timelineClasses
{
	import flash.events.Event;
	import flash.events.MouseEvent;
	import spark.events.IndexChangeEvent;
	import age.assets.ObjectInfo;
	import ageb.ageb_internal;
	import ageb.modules.ae.ActionInfoEditable;
	import ageb.modules.ae.FrameInfoEditable;
	import ageb.modules.avatar.op.AddFrameLayer;
	import ageb.modules.avatar.op.ChangeActionFPS;
	import ageb.modules.avatar.op.RemoveFrameLayer;
	import ageb.modules.avatar.op.SelectAction;
	import ageb.modules.document.Document;
	import nt.lib.util.assert;

	/**
	 * 时间轴面板
	 * @author zhanghaocong
	 *
	 */
	public class TimelinePanel extends TimelinePanelTemplate
	{
		/**
		 * 创建一个新的 TimelinePanel
		 *
		 */
		public function TimelinePanel()
		{
			super();
		}

		/**
		 * @inheritDoc
		 *
		 */
		override protected function createChildren():void
		{
			super.createChildren();
			framesGrid.onVerticalScrollPositionChange.add(framesGrid_onVerticalScrollPositionChange);
			actionsField.addEventListener(IndexChangeEvent.CHANGE, actionsField_onChange);
			actionsField.addEventListener(IndexChangeEvent.CHANGING, actionsField_onChanging);
			nextFrameButton.addEventListener(MouseEvent.CLICK, frameButton_onClick);
			playPauseButton.addEventListener(MouseEvent.CLICK, frameButton_onClick);
			prevFrameButton.addEventListener(MouseEvent.CLICK, frameButton_onClick);
			currentFrameField.addEventListener(Event.CHANGE, currentFrameField_onClick);
			directionButtons.addEventListener(IndexChangeEvent.CHANGE, directionButtons_onChange);
			layersField.addEventListener(MouseEvent.CLICK, layersField_onClick);
			layersField.onMouseWheel.add(layersField_onMouseWheel);
			fpsField.addEventListener(Event.CHANGE, fpsField_onChange);
			addLayerButton.addEventListener(MouseEvent.CLICK, addLayerButton_onClick);
			removeLayerButton.addEventListener(MouseEvent.CLICK, removeLayerButton_onClick);
		}

		private function layersField_onMouseWheel():void
		{
			framesGrid.grid.verticalScrollPosition = layersField.scroller.viewport.verticalScrollPosition;
		}

		/**
		 * @private
		 * @param event
		 *
		 */
		protected function fpsField_onChange(event:Event):void
		{
			new ChangeActionFPS(doc, fpsField.value).execute();
		}

		/**
		 * 点击列表任意项等于选择中该图层所有帧
		 * @param event
		 *
		 */
		protected function layersField_onClick(event:MouseEvent):void
		{
			var frames:Vector.<FrameInfoEditable> = new Vector.<FrameInfoEditable>;

			if (layersField.selectedIndex != -1)
			{
				for each (var rowIndex:int in layersField.selectedIndices)
				{
					frames = frames.concat(Vector.<FrameInfoEditable>(actionInfo.layers[rowIndex].frames));
				}
			}
			actionInfo.ageb_internal::setSelectedFrames(frames, this);
		}

		/**
		 * 更换方向
		 * @param event
		 *
		 */
		protected function directionButtons_onChange(event:IndexChangeEvent):void
		{
			objectInfo.direction = directionButtons.selectedItem.value;
		}

		/**
		 * 切换到指定帧
		 * @param event
		 *
		 */
		protected function currentFrameField_onClick(event:Event):void
		{
			objectInfo.gotoAndStop(currentFrameField.value);
		}

		/**
		 * 回放控制
		 * @param event
		 *
		 */
		protected function frameButton_onClick(event:MouseEvent):void
		{
			switch (event.currentTarget)
			{
				case playPauseButton:
				{
					if (objectInfo.isPlaying)
					{
						objectInfo.pause();
					}
					else
					{
						objectInfo.play();
					}
					break;
				}
				case nextFrameButton:
				{
					objectInfo.nextFrame();
					break;
				}
				case prevFrameButton:
				{
					objectInfo.prevFrame();
					break;
				}
				default:
				{
					throw new Error("不该进这里");
					break;
				}
			}
		}

		/**
		 * @private
		 * @param event
		 *
		 */
		protected function actionsField_onChanging(event:IndexChangeEvent):void
		{
			// 拦截不存在的动作
			if (!(actionsField.selectedItem is ActionInfoEditable))
			{
				event.preventDefault();
			}
		}

		/**
		 * 更换渲染的动作
		 * @param event
		 *
		 */
		protected function actionsField_onChange(event:IndexChangeEvent):void
		{
			new SelectAction(doc, ActionInfoEditable(actionsField.selectedItem).name).execute();
		}

		/**
		 * 添加图层
		 * @param event
		 *
		 */
		protected function addLayerButton_onClick(event:MouseEvent):void
		{
			new AddFrameLayer(doc).execute();
		}

		/**
		 * 删除图层
		 * @param event
		 *
		 */
		protected function removeLayerButton_onClick(event:MouseEvent):void
		{
			new RemoveFrameLayer(doc, layersField.selectedIndices).execute();
		}

		/**
		 * 返回 objectInfo.currentFrame
		 * @return
		 *
		 */
		[Inline]
		final protected function get currentFrame():int
		{
			return objectInfo.currentFrame;
		}

		/**
		 * @inheritDoc
		 * @param value
		 *
		 */
		override public function set doc(value:Document):void
		{
			if (avatarDoc)
			{
				objectInfo.onCurrentFrameChange.remove(onCurrentFrameChange);
				// 动作列表
				actionsField.dataProvider = null;
				// 帧网格
				framesGrid.avatarDoc = null;
				// 当前动作
				actionInfo = null;
			}
			super.doc = value;

			if (avatarDoc)
			{
				assert(avatarDoc.avatar == objectInfo.avatarInfo);
				actionsField.dataProvider = avatarDoc.avatar.actionsArrayList;
				framesGrid.avatarDoc = avatarDoc;
				objectInfo.onCurrentFrameChange.add(onCurrentFrameChange);
				onCurrentFrameChange(objectInfo);
			}
		}

		/**
		 * @inheritDoc
		 * @param value
		 *
		 */
		override public function set actionInfo(value:ActionInfoEditable):void
		{
			// 重设
			if (actionInfo)
			{
				// 更新选中项
				actionsField.selectedItem = null;
				// 当前帧
				currentFrameField.maximum = 0;
				// FPS
				fpsField.value = 0;
				// 图层列表
				layersField.dataProvider = null;
				// 一些事件
				actionInfo.onSelectedFramesChange.remove(onSelectedFramesChange);
				actionInfo.onFPSChange.remove(onFPSChange);
			}
			super.actionInfo = value;

			if (actionInfo)
			{
				// 更新选中项
				actionsField.selectedItem = actionInfo;
				// 当前帧
				currentFrameField.maximum = actionInfo.numFrames - 1; // currentFrame 从 0  开始
				// 图层列表
				layersField.dataProvider = actionInfo.layersVectorList;
				// 一些事件
				actionInfo.onSelectedFramesChange.add(onSelectedFramesChange);
				onSelectedFramesChange(null);
				actionInfo.onFPSChange.add(onFPSChange);
				onFPSChange();
			}
		}

		/**
		 * @private
		 *
		 */
		private function onFPSChange():void
		{
			fpsField.value = actionInfo.fps;
		}

		/**
		 * @private
		 *
		 */
		private function onSelectedFramesChange(trigger:Object):void
		{
			if (trigger == this)
			{
				return;
			}
			const frames:Vector.<FrameInfoEditable> = actionInfo.selectedFrames;
			var selectedIndices:Vector.<int> = new Vector.<int>;

			for (var i:int = 0, n:int = frames.length; i < n; i++)
			{
				const layerIndex:int = frames[i].layerIndex;

				if (selectedIndices.indexOf(layerIndex) == -1)
				{
					selectedIndices.push(layerIndex);
				}
			}
			layersField.selectedIndices = selectedIndices;
		}

		/**
		 * 此为同步滚动条位置
		 * @param value
		 *
		 */
		private function framesGrid_onVerticalScrollPositionChange(value:Number):void
		{
			layersField.dataGroup.verticalScrollPosition = value;
		}

		/**
		 * 更新当前播放的帧
		 * @param target
		 *
		 */
		private function onCurrentFrameChange(target:ObjectInfo):void
		{
			currentFrameField.value = currentFrame;
			currentTimeField.text = (currentFrame * actionInfo.defautFrameDuration).toFixed(3) + "s";
		}
	}
}

package ageb.modules.avatar.timelineClasses
{
	import flash.events.Event;
	import flash.events.MouseEvent;
	import spark.events.IndexChangeEvent;
	import age.assets.ObjectInfo;
	import ageb.ageb_internal;
	import ageb.modules.ae.ActionInfoEditable;
	import ageb.modules.ae.FrameInfoEditable;
	import ageb.modules.avatar.op.AddFrameLayer;
	import ageb.modules.avatar.op.ChangeActionFPS;
	import ageb.modules.avatar.op.RemoveFrameLayer;
	import ageb.modules.avatar.op.SelectAction;
	import ageb.modules.document.Document;
	import nt.lib.util.assert;

	/**
	 * 时间轴面板
	 * @author zhanghaocong
	 *
	 */
	public class TimelinePanel extends TimelinePanelTemplate
	{
		/**
		 * 创建一个新的 TimelinePanel
		 *
		 */
		public function TimelinePanel()
		{
			super();
		}

		/**
		 * @inheritDoc
		 *
		 */
		override protected function createChildren():void
		{
			super.createChildren();
			framesGrid.onVerticalScrollPositionChange.add(framesGrid_onVerticalScrollPositionChange);
			actionsField.addEventListener(IndexChangeEvent.CHANGE, actionsField_onChange);
			actionsField.addEventListener(IndexChangeEvent.CHANGING, actionsField_onChanging);
			nextFrameButton.addEventListener(MouseEvent.CLICK, frameButton_onClick);
			playPauseButton.addEventListener(MouseEvent.CLICK, frameButton_onClick);
			prevFrameButton.addEventListener(MouseEvent.CLICK, frameButton_onClick);
			currentFrameField.addEventListener(Event.CHANGE, currentFrameField_onClick);
			directionButtons.addEventListener(IndexChangeEvent.CHANGE, directionButtons_onChange);
			layersField.addEventListener(MouseEvent.CLICK, layersField_onClick);
			layersField.onMouseWheel.add(layersField_onMouseWheel);
			fpsField.addEventListener(Event.CHANGE, fpsField_onChange);
			addLayerButton.addEventListener(MouseEvent.CLICK, addLayerButton_onClick);
			removeLayerButton.addEventListener(MouseEvent.CLICK, removeLayerButton_onClick);
		}

		private function layersField_onMouseWheel():void
		{
			framesGrid.grid.verticalScrollPosition = layersField.scroller.viewport.verticalScrollPosition;
		}

		/**
		 * @private
		 * @param event
		 *
		 */
		protected function fpsField_onChange(event:Event):void
		{
			new ChangeActionFPS(doc, fpsField.value).execute();
		}

		/**
		 * 点击列表任意项等于选择中该图层所有帧
		 * @param event
		 *
		 */
		protected function layersField_onClick(event:MouseEvent):void
		{
			var frames:Vector.<FrameInfoEditable> = new Vector.<FrameInfoEditable>;

			if (layersField.selectedIndex != -1)
			{
				for each (var rowIndex:int in layersField.selectedIndices)
				{
					frames = frames.concat(Vector.<FrameInfoEditable>(actionInfo.layers[rowIndex].frames));
				}
			}
			actionInfo.ageb_internal::setSelectedFrames(frames, this);
		}

		/**
		 * 更换方向
		 * @param event
		 *
		 */
		protected function directionButtons_onChange(event:IndexChangeEvent):void
		{
			objectInfo.direction = directionButtons.selectedItem.value;
		}

		/**
		 * 切换到指定帧
		 * @param event
		 *
		 */
		protected function currentFrameField_onClick(event:Event):void
		{
			objectInfo.gotoAndStop(currentFrameField.value);
		}

		/**
		 * 回放控制
		 * @param event
		 *
		 */
		protected function frameButton_onClick(event:MouseEvent):void
		{
			switch (event.currentTarget)
			{
				case playPauseButton:
				{
					if (objectInfo.isPlaying)
					{
						objectInfo.pause();
					}
					else
					{
						objectInfo.play();
					}
					break;
				}
				case nextFrameButton:
				{
					objectInfo.nextFrame();
					break;
				}
				case prevFrameButton:
				{
					objectInfo.prevFrame();
					break;
				}
				default:
				{
					throw new Error("不该进这里");
					break;
				}
			}
		}

		/**
		 * @private
		 * @param event
		 *
		 */
		protected function actionsField_onChanging(event:IndexChangeEvent):void
		{
			// 拦截不存在的动作
			if (!(actionsField.selectedItem is ActionInfoEditable))
			{
				event.preventDefault();
			}
		}

		/**
		 * 更换渲染的动作
		 * @param event
		 *
		 */
		protected function actionsField_onChange(event:IndexChangeEvent):void
		{
			callLater(new SelectAction(doc, ActionInfoEditable(actionsField.selectedItem).name).execute);
		}

		/**
		 * 添加图层
		 * @param event
		 *
		 */
		protected function addLayerButton_onClick(event:MouseEvent):void
		{
			new AddFrameLayer(doc).execute();
		}

		/**
		 * 删除图层
		 * @param event
		 *
		 */
		protected function removeLayerButton_onClick(event:MouseEvent):void
		{
			new RemoveFrameLayer(doc, layersField.selectedIndices).execute();
		}

		/**
		 * 返回 objectInfo.currentFrame
		 * @return
		 *
		 */
		[Inline]
		final protected function get currentFrame():int
		{
			return objectInfo.currentFrame;
		}

		/**
		 * @inheritDoc
		 * @param value
		 *
		 */
		override public function set doc(value:Document):void
		{
			if (avatarDoc)
			{
				objectInfo.onCurrentFrameChange.remove(onCurrentFrameChange);
				// 动作列表
				actionsField.dataProvider = null;
				// 帧网格
				framesGrid.avatarDoc = null;
				// 当前动作
				actionInfo = null;
			}
			super.doc = value;

			if (avatarDoc)
			{
				assert(avatarDoc.avatar == objectInfo.avatarInfo);
				actionsField.dataProvider = avatarDoc.avatar.actionsArrayList;
				framesGrid.avatarDoc = avatarDoc;
				objectInfo.onCurrentFrameChange.add(onCurrentFrameChange);
				onCurrentFrameChange(objectInfo);
			}
		}

		/**
		 * @inheritDoc
		 * @param value
		 *
		 */
		override public function set actionInfo(value:ActionInfoEditable):void
		{
			// 重设
			if (actionInfo)
			{
				// 更新选中项
				actionsField.selectedItem = null;
				// 当前帧
				currentFrameField.maximum = 0;
				// FPS
				fpsField.value = 0;
				// 图层列表
				layersField.dataProvider = null;
				// 一些事件
				actionInfo.onSelectedFramesChange.remove(onSelectedFramesChange);
				actionInfo.onFPSChange.remove(onFPSChange);
			}
			super.actionInfo = value;

			if (actionInfo)
			{
				// 更新选中项
				actionsField.selectedItem = actionInfo;
				// 当前帧
				currentFrameField.maximum = actionInfo.numFrames - 1; // currentFrame 从 0  开始
				// 图层列表
				layersField.dataProvider = actionInfo.layersVectorList;
				// 一些事件
				actionInfo.onSelectedFramesChange.add(onSelectedFramesChange);
				onSelectedFramesChange(null);
				actionInfo.onFPSChange.add(onFPSChange);
				onFPSChange();
			}
		}

		/**
		 * @private
		 *
		 */
		private function onFPSChange():void
		{
			fpsField.value = actionInfo.fps;
		}

		/**
		 * @private
		 *
		 */
		private function onSelectedFramesChange(trigger:Object):void
		{
			if (trigger == this)
			{
				return;
			}
			const frames:Vector.<FrameInfoEditable> = actionInfo.selectedFrames;
			var selectedIndices:Vector.<int> = new Vector.<int>;

			for (var i:int = 0, n:int = frames.length; i < n; i++)
			{
				const layerIndex:int = frames[i].layerIndex;

				if (selectedIndices.indexOf(layerIndex) == -1)
				{
					selectedIndices.push(layerIndex);
				}
			}
			layersField.selectedIndices = selectedIndices;
		}

		/**
		 * 此为同步滚动条位置
		 * @param value
		 *
		 */
		private function framesGrid_onVerticalScrollPositionChange(value:Number):void
		{
			layersField.dataGroup.verticalScrollPosition = value;
		}

		/**
		 * 更新当前播放的帧
		 * @param target
		 *
		 */
		private function onCurrentFrameChange(target:ObjectInfo):void
		{
			currentFrameField.value = currentFrame;
			currentTimeField.text = (currentFrame * actionInfo.defautFrameDuration).toFixed(3) + "s";
		}
	}
}

修复了在动作列表 Dropdown 按 ↑↓ 键翻阅动作时，列表内容切换不正确的 BUG


//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/vilya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.stage.client {

import flash.display.DisplayObject;

import com.threerings.miso.client.MisoScenePanel;
import com.threerings.miso.util.MisoContext;
import com.threerings.miso.util.MisoSceneMetrics;
import com.threerings.miso.client.SceneBlock;
import com.threerings.miso.data.MisoSceneModel;
import com.threerings.util.Iterator;
import com.threerings.util.MathUtil;
import com.threerings.whirled.spot.data.Portal;
import com.threerings.whirled.spot.data.SpotScene;
import com.threerings.whirled.spot.data.SpotSceneModel;
import com.threerings.stage.data.StageLocation;

import as3isolib.display.IsoSprite;
import as3isolib.display.IsoView;
import as3isolib.display.scene.IsoScene;

public class StageSceneBlock extends SceneBlock
{
    public function StageSceneBlock (key :int, objScene :IsoScene, portalScene :IsoScene,
        isoView :IsoView, metrics :MisoSceneMetrics, spotScene :SpotScene)
    {
        super(key, objScene, isoView, metrics);
        _spotScene = spotScene;
        _portalScene = portalScene;
    }

    override public function resolve (ctx :MisoContext, model :MisoSceneModel, panel :MisoScenePanel,
        completeCallback :Function) :void
    {
        _tryingPortals = true;
        super.resolve(ctx, model, panel, completeCallback);

        var bx :int = getBlockX(_key);
        var by :int = getBlockY(_key);
        var iter :Iterator = _spotScene.getPortals();
        while (iter.hasNext()) {
            var portal :Portal = Portal(iter.next());
            var x :int = fullToTile(StageLocation(portal.loc).x);
            var y :int = fullToTile(StageLocation(portal.loc).y);
            if (x < bx || x > bx + BLOCK_SIZE || y < by || y > by + BLOCK_SIZE) {
                continue;
            }

            trace("MST loc: " + StageLocation(portal.loc).x + "," + StageLocation(portal.loc).y);

            var fineX :int = StageLocation(portal.loc).x - FULL_TILE_FACTOR * x;
            var fineY :int = StageLocation(portal.loc).y - FULL_TILE_FACTOR * y;

            trace("MST part: " + fineX + "," + fineY);

            // Grab the portal image, and center it.
            var portalSprite :IsoSprite = new IsoSprite();
            var img :DisplayObject =
                StageScenePanel(panel).getPortalImage(StageLocation(portal.loc).orient);
            img.x = -img.width/2 + int(_metrics.finehwid * (fineX - fineY));
            img.y = -img.height/2 + int(_metrics.finehhei * (fineX + fineY));

            portalSprite.sprites = [img];
            portalSprite.setSize(1, 1, 1);
            trace("MST Adding portal: " + x + "," + y);
            portalSprite.moveTo(x, y, 0);

            if (_portSprites == null) {
                _portSprites = [];
            }
            _portSprites.push(portalSprite);
        }

        _tryingPortals = false;
        maybeLoaded();
    }

    override public function render () :void
    {
        super.render();
        for each (var sprite :IsoSprite in _portSprites) {
            _portalScene.addChild(sprite);
        }
    }

    override public function release () :void
    {
        super.release();
        for each (var sprite :IsoSprite in _portSprites) {
            _portalScene.removeChild(sprite);
        }
    }

    override protected function maybeLoaded () :void
    {
        // If we're still working on getting our portals setup, skip it...
        if (!_tryingPortals) {
            super.maybeLoaded();
        }
    }

    protected function fullToTile (fullCoord :int) :int
    {
        return MathUtil.floorDiv(fullCoord, FULL_TILE_FACTOR);
    }

    protected static const FULL_TILE_FACTOR :int = 100;

    protected var _spotScene :SpotScene;

    protected var _tryingPortals :Boolean;

    protected var _portalScene :IsoScene;

    protected var _portSprites :Array;
}
}

//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/vilya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.stage.client {

import flash.display.DisplayObject;

import com.threerings.miso.client.MisoScenePanel;
import com.threerings.miso.util.MisoContext;
import com.threerings.miso.util.MisoSceneMetrics;
import com.threerings.miso.client.SceneBlock;
import com.threerings.miso.data.MisoSceneModel;
import com.threerings.util.Iterator;
import com.threerings.util.MathUtil;
import com.threerings.whirled.spot.data.Portal;
import com.threerings.whirled.spot.data.SpotScene;
import com.threerings.whirled.spot.data.SpotSceneModel;
import com.threerings.stage.data.StageLocation;

import as3isolib.display.IsoSprite;
import as3isolib.display.IsoView;
import as3isolib.display.scene.IsoScene;

public class StageSceneBlock extends SceneBlock
{
    public function StageSceneBlock (key :int, objScene :IsoScene, portalScene :IsoScene,
        isoView :IsoView, metrics :MisoSceneMetrics, spotScene :SpotScene)
    {
        super(key, objScene, isoView, metrics);
        _spotScene = spotScene;
        _portalScene = portalScene;
    }

    override public function resolve (ctx :MisoContext, model :MisoSceneModel, panel :MisoScenePanel,
        completeCallback :Function) :void
    {
        _tryingPortals = true;
        super.resolve(ctx, model, panel, completeCallback);

        var bx :int = getBlockX(_key);
        var by :int = getBlockY(_key);
        var iter :Iterator = _spotScene.getPortals();
        while (iter.hasNext()) {
            var portal :Portal = Portal(iter.next());
            var x :int = fullToTile(StageLocation(portal.loc).x);
            var y :int = fullToTile(StageLocation(portal.loc).y);
            if (x < bx || x > bx + BLOCK_SIZE || y < by || y > by + BLOCK_SIZE) {
                continue;
            }

            var fineX :int = StageLocation(portal.loc).x - FULL_TILE_FACTOR * x;
            var fineY :int = StageLocation(portal.loc).y - FULL_TILE_FACTOR * y;

            // Grab the portal image, and center it.
            var portalSprite :IsoSprite = new IsoSprite();
            var img :DisplayObject =
                StageScenePanel(panel).getPortalImage(StageLocation(portal.loc).orient);
            img.x = -img.width/2 + int(_metrics.finehwid * (fineX - fineY));
            img.y = -img.height/2 + int(_metrics.finehhei * (fineX + fineY));

            portalSprite.sprites = [img];
            portalSprite.setSize(1, 1, 1);
            portalSprite.moveTo(x, y, 0);

            if (_portSprites == null) {
                _portSprites = [];
            }
            _portSprites.push(portalSprite);
        }

        _tryingPortals = false;
        maybeLoaded();
    }

    override public function render () :void
    {
        super.render();
        for each (var sprite :IsoSprite in _portSprites) {
            _portalScene.addChild(sprite);
        }
    }

    override public function release () :void
    {
        super.release();
        for each (var sprite :IsoSprite in _portSprites) {
            _portalScene.removeChild(sprite);
        }
    }

    override protected function maybeLoaded () :void
    {
        // If we're still working on getting our portals setup, skip it...
        if (!_tryingPortals) {
            super.maybeLoaded();
        }
    }

    protected function fullToTile (fullCoord :int) :int
    {
        return MathUtil.floorDiv(fullCoord, FULL_TILE_FACTOR);
    }

    protected static const FULL_TILE_FACTOR :int = 100;

    protected var _spotScene :SpotScene;

    protected var _tryingPortals :Boolean;

    protected var _portalScene :IsoScene;

    protected var _portSprites :Array;
}
}

Remove some debug printouts.

Remove some debug printouts.

git-svn-id: a3e1eb16dde062992de22c830ed8045c8013209a@941 c613c5cb-e716-0410-b11b-feb51c14d237


//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.presents.client {

import flash.events.EventDispatcher;
import flash.events.TimerEvent;

import flash.utils.Timer;

import com.threerings.util.Log;
import com.threerings.util.MethodQueue;
import com.threerings.util.Throttle;

import com.threerings.presents.client.InvocationService_ConfirmListener;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationCodes;
import com.threerings.presents.data.TimeBaseMarshaller;

import com.threerings.presents.dobj.DObjectManager;

import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.BootstrapData;
import com.threerings.presents.net.Credentials;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse;
import com.threerings.presents.net.ThrottleUpdatedMessage;

public class Client extends EventDispatcher
{
    /** The default port on which the server listens for client connections. */
    public static const DEFAULT_SERVER_PORTS :Array = [ 47624 ];

    /** Our default maximum outgoing message rate in messages per second. */
    public static const DEFAULT_MSGS_PER_SECOND :int = 10;

    private static const log :Log = Log.getLog(Client);

    // statically reference classes we require
    TimeBaseMarshaller;

    public function Client (creds :Credentials)
    {
        _creds = creds;
    }

    /**
     * Registers the supplied observer with this client. While registered the observer will receive
     * notifications of state changes within the client. The function will refuse to register an
     * already registered observer.
     *
     * @see ClientObserver
     * @see SessionObserver
     */
    public function addClientObserver (observer :SessionObserver) :void
    {
        addEventListener(ClientEvent.CLIENT_WILL_LOGON, observer.clientWillLogon);
        addEventListener(ClientEvent.CLIENT_DID_LOGON, observer.clientDidLogon);
        addEventListener(ClientEvent.CLIENT_OBJECT_CHANGED, observer.clientObjectDidChange);
        addEventListener(ClientEvent.CLIENT_DID_LOGOFF, observer.clientDidLogoff);
        if (observer is ClientObserver) {
            var cliObs :ClientObserver = (observer as ClientObserver);
            addEventListener(ClientEvent.CLIENT_FAILED_TO_LOGON, cliObs.clientFailedToLogon);
            addEventListener(ClientEvent.CLIENT_CONNECTION_FAILED, cliObs.clientConnectionFailed);
            addEventListener(ClientEvent.CLIENT_WILL_LOGOFF, cliObs.clientWillLogoff);
            addEventListener(ClientEvent.CLIENT_DID_CLEAR, cliObs.clientDidClear);
        }
    }

    /**
     * Unregisters the supplied observer. Upon return of this function, the observer will no longer
     * receive notifications of state changes within the client.
     */
    public function removeClientObserver (observer :SessionObserver) :void
    {
        removeEventListener(ClientEvent.CLIENT_WILL_LOGON, observer.clientWillLogon);
        removeEventListener(ClientEvent.CLIENT_DID_LOGON, observer.clientDidLogon);
        removeEventListener(ClientEvent.CLIENT_OBJECT_CHANGED, observer.clientObjectDidChange);
        removeEventListener(ClientEvent.CLIENT_DID_LOGOFF, observer.clientDidLogoff);
        if (observer is ClientObserver) {
            var cliObs :ClientObserver = (observer as ClientObserver);
            removeEventListener(ClientEvent.CLIENT_FAILED_TO_LOGON, cliObs.clientFailedToLogon);
            removeEventListener(
                ClientEvent.CLIENT_CONNECTION_FAILED, cliObs.clientConnectionFailed);
            removeEventListener(ClientEvent.CLIENT_WILL_LOGOFF, cliObs.clientWillLogoff);
            removeEventListener(ClientEvent.CLIENT_DID_CLEAR, cliObs.clientDidClear);
        }
    }

    public function setServer (hostname :String, ports :Array) :void
    {
        _hostname = hostname;
        _ports = ports;
    }

    public function callLater (fn :Function, args :Array = null) :void
    {
        MethodQueue.callLater(fn, args);
    }

    public function getHostname () :String
    {
        return _hostname;
    }

    /**
     * Returns the ports on which this client is configured to connect.
     */
    public function getPorts () :Array
    {
        return _ports;
    }

    public function getCredentials () :Credentials
    {
        return _creds;
    }

    public function setCredentials (creds :Credentials) :void
    {
        _creds = creds;
    }

    public function getVersion () :String
    {
        return _version;
    }

    public function setVersion (version :String) :void
    {
        _version = version;
    }

    public function getAuthResponseData () :AuthResponseData
    {
        return _authData;
    }

    public function setAuthResponseData (data :AuthResponseData) :void
    {
        _authData = data;
    }

    public function getDObjectManager () :DObjectManager
    {
        return _omgr;
    }

    public function getClientOid () :int
    {
        return _cloid;
    }

    public function getClientObject () :ClientObject
    {
        return _clobj;
    }

    public function getInvocationDirector () :InvocationDirector
    {
        return _invdir;
    }

    /**
     * Returns the set of bootstrap service groups needed by this client.
     */
    public function getBootGroups () :Array
    {
        return _bootGroups;
    }

    /**
     * Marks this client as interested in the specified bootstrap services group. Any services
     * registered as bootstrap services with the supplied group name will be included in this
     * clients bootstrap services set. This must be called before {@link #logon}.
     */
    public function addServiceGroup (group :String) :void
    {
        if (isLoggedOn()) {
            throw new Error("Services must be registered prior to logon().");
        }
        if (_bootGroups.indexOf(group) == -1) {
            _bootGroups.push(group);
        }
    }

    public function getService (clazz :Class) :InvocationService
    {
        if (_bstrap != null) {
            for each (var isvc :InvocationService in _bstrap.services) {
                if (isvc is clazz) {
                    return isvc;
                }
            }
        }

        return null;
    }

    public function requireService (clazz :Class) :InvocationService
    {
        var isvc :InvocationService = getService(clazz);
        if (isvc == null) {
            throw new Error(clazz + " isn't available. I can't bear to go on.");
        }
        return isvc;
    }

    public function getBootstrapData () :BootstrapData
    {
        return _bstrap;
    }

    public function isLoggedOn () :Boolean
    {
        return (_clobj != null);
    }

    /**
     * Requests that this client connect and logon to the server with which it was previously
     * configured.
     *
     * @return false if we're already logged on.
     */
    public function logon () :Boolean
    {
        // if we have a communicator, we're already logged on
        if (_comm != null) {
            return false;
        }

        // let our observers know that we're logging on (this will give directors a chance to
        // register invocation service groups)
        notifyObservers(ClientEvent.CLIENT_WILL_LOGON);

        // we need to wait for the CLIENT_WILL_LOGON to have been dispatched before we actually
        // tell the communicator to logon, so we run this through the callLater pipeline
        _comm = new Communicator(this);
        callLater(_comm.logon);

        return true;
    }

    /**
     * Transitions a logged on client from its current server to the specified new server.
     * Currently this simply logs the client off of its current server (if it is logged on) and
     * logs it onto the new server, but in the future we may aim to do something fancier.
     *
     *
 If we fail to connect to the new server, the client will not be automatically
     * reconnected to the old server. It will be in a logged off state. However, it will be
     * reconfigured with the hostname and ports of the old server so that the caller can notify the
     * user of the failure and then simply call {@link #logon} to attempt to reconnect to the old
     * server.
     *
     * @param observer an observer that will be notified when we have successfully logged onto the
     * other server, or if the move failed.
     */
    public function moveToServer (hostname :String, ports :Array,
                                  obs :InvocationService_ConfirmListener) :void
    {
        // the server switcher will take care of everything for us
        _switcher = new ServerSwitcher(this, hostname, ports, obs);
        _switcher.switchServers();
    }

    /**
     * Requests that the client log off of the server to which it is connected.
     *
     * @param abortable if true, the client will call clientWillDisconnect on allthe client
     * observers and abort the logoff process if any of them return false. If false,
     * clientWillDisconnect will not be called.
     *
     * @return true if the logoff succeeded, false if it failed due to a disagreeable observer.
     */
    public function logoff (abortable :Boolean) :Boolean
    {
        if (_comm == null) {
            log.warning("Ignoring request to log off: not logged on.");
            return true;
        }

        // if this is an externally initiated logoff request, clear our active session status
        if (_switcher == null) {
            _activeSession = false;
        }

        // if the request is abortable, let's run it past the observers.  if any of them call
        // preventDefault() then the logoff will be cancelled
        if (abortable && !notifyObservers(ClientEvent.CLIENT_WILL_LOGOFF)) {
            _activeSession = true; // restore our active session status
            return false;
        }

        if (_tickInterval != null) {
            _tickInterval.stop();
            _tickInterval = null;
        }

        _comm.logoff();
        return true;
    }

    public function gotBootstrap (data :BootstrapData, omgr :DObjectManager) :void
    {
        // log.debug("Got bootstrap " + data + ".");

        _bstrap = data;
        _omgr = omgr;
        _cloid = data.clientOid;

        _invdir.init(omgr, _cloid, this);

        // log.debug("TimeBaseService: " + requireService(TimeBaseService));
    }

    /**
     * Called every five seconds; ensures that we ping the server if we haven't communicated in a
     * long while.
     */
    protected function tick (event :TimerEvent) :void
    {
        if (_comm == null) {
            return;
        }

        var now :uint = flash.utils.getTimer();
        if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) {
            _comm.postMessage(new PingRequest());
        }
    }

    /**
     * Called by the {@link Communicator} if it is experiencing trouble logging on but is still
     * trying fallback strategies.
     */
    internal function reportLogonTribulations (cause :LogonError) :void
    {
        notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, cause);
    }

    /**
     * Called by the invocation director when it successfully subscribes to the client object
     * immediately following logon.
     */
    public function gotClientObject (clobj :ClientObject) :void
    {
        // keep our client object around
        _clobj = clobj;

        // and start up our tick interval (which will send pings when necessary)
        if (_tickInterval == null) {
            _tickInterval = new Timer(5000);
            _tickInterval.addEventListener(TimerEvent.TIMER, tick);
            _tickInterval.start();
        }

        notifyObservers(ClientEvent.CLIENT_DID_LOGON);

        // now that we've logged on at least once, we're in the middle of an active session and
        // will remain so until we receive an externally initiated logoff request
        _activeSession = true;
    }

    /**
     * Called by the invocation director if it fails to subscribe to the client object after logon.
     */
    public function getClientObjectFailed (cause :Error) :void
    {
        notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, cause);
    }

    /**
     * Called by the invocation director when it discovers that the client object has changed.
     */
    protected function clientObjectDidChange (clobj :ClientObject) :void
    {
        _clobj = clobj;
        _cloid = clobj.getOid();

        notifyObservers(ClientEvent.CLIENT_OBJECT_CHANGED);
    }

    /**
     * Convenience method to dispatch a client event to any listeners and return the result of
     * dispatchEvent.
     */
    public function notifyObservers (evtCode :String, cause :Error = null) :Boolean
    {
        return dispatchEvent(new ClientEvent(evtCode, this, _activeSession, cause));
    }

    /**
     * Called by the omgr when we receive a pong packet.
     */
    internal function gotPong (pong :PongResponse) :void
    {
        // TODO: compute time delta?
    }

    internal function setOutgoingMessageThrottle (messagesPerSec :int) :void
    {
        _comm.postMessage(new ThrottleUpdatedMessage(messagesPerSec));
    }

    internal function finalizeOutgoingMessageThrottle (messagesPerSec :int) :void
    {
        // when the throttle update message goes out to the server
        _outThrottle.reinit(messagesPerSec, 1000);
        log.info("Updated outgoing throttle", "messagesPerSec", messagesPerSec);
    }

    internal function getOutgoingMessageThrottle () :Throttle
    {
        return _outThrottle;
    }

    internal function cleanup (logonError :Error) :void
    {
        // clear out our references
        _comm = null;
        _omgr = null;
        _clobj = null;
        _cloid = -1;

        // and let our invocation director know we're logged off
        _invdir.cleanup();

        // if this was due to a logon error, we can notify our listeners now that we're cleaned up:
        // they may want to retry logon on another port, or something
        if (logonError != null) {
            notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, logonError);
        } else {
            notifyObservers(ClientEvent.CLIENT_DID_CLEAR, null);
        }

        // clear out any server switcher reference
        _switcher = null;
    }

    /** The credentials we used to authenticate with the server. */
    protected var _creds :Credentials;

    /** The version string reported to the server at auth time. */
    protected var _version :String = "";

    /** The distributed object manager we're using during this session. */
    protected var _omgr :DObjectManager;

    /** The data associated with our authentication response. */
    protected var _authData :AuthResponseData;

    /** Our client distributed object id. */
    protected var _cloid :int = -1;

    /** Our client distributed object. */
    protected var _clobj :ClientObject;

    /** The game server host. */
    protected var _hostname :String;

    /** The port on which we connect to the game server. */
    protected var _ports :Array; /* of int */

    /** The entity that manages our network communications. */
    protected var _comm :Communicator;

    /** Our outgoing message throttle. */
    protected var _outThrottle :Throttle = new Throttle(DEFAULT_MSGS_PER_SECOND, 1000);

    /** The set of bootstrap service groups this client cares about. */
    protected var _bootGroups :Array = new Array(InvocationCodes.GLOBAL_GROUP);

    /** General startup information provided by the server. */
    protected var _bstrap :BootstrapData;

    /** Manages invocation services. */
    protected var _invdir :InvocationDirector = new InvocationDirector();

    /** Ticks. */
    protected var _tickInterval :Timer;

    /** This flag is used to distinguish our *first* willLogon/didLogon and a caller-initiated
     * willLogoff/didLogoff from similar events generated during server switches. Thus it is true
     * for most of a normal client session. */
    protected var _activeSession :Boolean;

    /** Used to temporarily track our server switcher so that we can tell when we're logging off
     * whether or not we're switching servers or actually ending our session. */
    protected var _switcher :ServerSwitcher;
}
}

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.presents.client {

import flash.events.EventDispatcher;
import flash.events.TimerEvent;

import flash.utils.Timer;

import com.threerings.util.Log;
import com.threerings.util.MethodQueue;
import com.threerings.util.Throttle;

import com.threerings.presents.client.InvocationService_ConfirmListener;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationCodes;
import com.threerings.presents.data.TimeBaseMarshaller;

import com.threerings.presents.dobj.DObjectManager;

import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.BootstrapData;
import com.threerings.presents.net.Credentials;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse;
import com.threerings.presents.net.ThrottleUpdatedMessage;

public class Client extends EventDispatcher
{
    /** The default port on which the server listens for client connections. */
    public static const DEFAULT_SERVER_PORTS :Array = [ 47624 ];

    /** Our default maximum outgoing message rate in messages per second. */
    public static const DEFAULT_MSGS_PER_SECOND :int = 10;

    private static const log :Log = Log.getLog(Client);

    // statically reference classes we require
    TimeBaseMarshaller;

    public function Client (creds :Credentials)
    {
        _creds = creds;
    }

    /**
     * Registers the supplied observer with this client. While registered the observer will receive
     * notifications of state changes within the client. The function will refuse to register an
     * already registered observer.
     *
     * @see ClientObserver
     * @see SessionObserver
     */
    public function addClientObserver (observer :SessionObserver) :void
    {
        addEventListener(ClientEvent.CLIENT_WILL_LOGON, observer.clientWillLogon);
        addEventListener(ClientEvent.CLIENT_DID_LOGON, observer.clientDidLogon);
        addEventListener(ClientEvent.CLIENT_OBJECT_CHANGED, observer.clientObjectDidChange);
        addEventListener(ClientEvent.CLIENT_DID_LOGOFF, observer.clientDidLogoff);
        if (observer is ClientObserver) {
            var cliObs :ClientObserver = (observer as ClientObserver);
            addEventListener(ClientEvent.CLIENT_FAILED_TO_LOGON, cliObs.clientFailedToLogon);
            addEventListener(ClientEvent.CLIENT_CONNECTION_FAILED, cliObs.clientConnectionFailed);
            addEventListener(ClientEvent.CLIENT_WILL_LOGOFF, cliObs.clientWillLogoff);
            addEventListener(ClientEvent.CLIENT_DID_CLEAR, cliObs.clientDidClear);
        }
    }

    /**
     * Unregisters the supplied observer. Upon return of this function, the observer will no longer
     * receive notifications of state changes within the client.
     */
    public function removeClientObserver (observer :SessionObserver) :void
    {
        removeEventListener(ClientEvent.CLIENT_WILL_LOGON, observer.clientWillLogon);
        removeEventListener(ClientEvent.CLIENT_DID_LOGON, observer.clientDidLogon);
        removeEventListener(ClientEvent.CLIENT_OBJECT_CHANGED, observer.clientObjectDidChange);
        removeEventListener(ClientEvent.CLIENT_DID_LOGOFF, observer.clientDidLogoff);
        if (observer is ClientObserver) {
            var cliObs :ClientObserver = (observer as ClientObserver);
            removeEventListener(ClientEvent.CLIENT_FAILED_TO_LOGON, cliObs.clientFailedToLogon);
            removeEventListener(
                ClientEvent.CLIENT_CONNECTION_FAILED, cliObs.clientConnectionFailed);
            removeEventListener(ClientEvent.CLIENT_WILL_LOGOFF, cliObs.clientWillLogoff);
            removeEventListener(ClientEvent.CLIENT_DID_CLEAR, cliObs.clientDidClear);
        }
    }

    public function setServer (hostname :String, ports :Array) :void
    {
        _hostname = hostname;
        _ports = ports;
    }

    public function callLater (fn :Function, args :Array = null) :void
    {
        MethodQueue.callLater(fn, args);
    }

    public function getHostname () :String
    {
        return _hostname;
    }

    /**
     * Returns the ports on which this client is configured to connect.
     */
    public function getPorts () :Array
    {
        return _ports;
    }

    public function getCredentials () :Credentials
    {
        return _creds;
    }

    public function setCredentials (creds :Credentials) :void
    {
        _creds = creds;
    }

    public function getVersion () :String
    {
        return _version;
    }

    public function setVersion (version :String) :void
    {
        _version = version;
    }

    public function getAuthResponseData () :AuthResponseData
    {
        return _authData;
    }

    public function setAuthResponseData (data :AuthResponseData) :void
    {
        _authData = data;
    }

    public function getDObjectManager () :DObjectManager
    {
        return _omgr;
    }

    public function getClientOid () :int
    {
        return _cloid;
    }

    public function getClientObject () :ClientObject
    {
        return _clobj;
    }

    public function getInvocationDirector () :InvocationDirector
    {
        return _invdir;
    }

    /**
     * Returns the set of bootstrap service groups needed by this client.
     */
    public function getBootGroups () :Array
    {
        return _bootGroups;
    }

    /**
     * Marks this client as interested in the specified bootstrap services group. Any services
     * registered as bootstrap services with the supplied group name will be included in this
     * clients bootstrap services set. This must be called before {@link #logon}.
     */
    public function addServiceGroup (group :String) :void
    {
        if (isLoggedOn()) {
            throw new Error("Services must be registered prior to logon().");
        }
        if (_bootGroups.indexOf(group) == -1) {
            _bootGroups.push(group);
        }
    }

    public function getService (clazz :Class) :InvocationService
    {
        if (_bstrap != null) {
            for each (var isvc :InvocationService in _bstrap.services) {
                if (isvc is clazz) {
                    return isvc;
                }
            }
        }

        return null;
    }

    public function requireService (clazz :Class) :InvocationService
    {
        var isvc :InvocationService = getService(clazz);
        if (isvc == null) {
            throw new Error(clazz + " isn't available. I can't bear to go on.");
        }
        return isvc;
    }

    public function getBootstrapData () :BootstrapData
    {
        return _bstrap;
    }

    public function isLoggedOn () :Boolean
    {
        return (_clobj != null);
    }

    /**
     * Requests that this client connect and logon to the server with which it was previously
     * configured.
     *
     * @return false if we're already logged on.
     */
    public function logon () :Boolean
    {
        // if we have a communicator, we're already logged on
        if (_comm != null) {
            return false;
        }

        // let our observers know that we're logging on (this will give directors a chance to
        // register invocation service groups)
        notifyObservers(ClientEvent.CLIENT_WILL_LOGON);

        // we need to wait for the CLIENT_WILL_LOGON to have been dispatched before we actually
        // tell the communicator to logon, so we run this through the callLater pipeline
        _comm = new Communicator(this);
        callLater(_comm.logon);

        return true;
    }

    /**
     * Transitions a logged on client from its current server to the specified new server.
     * Currently this simply logs the client off of its current server (if it is logged on) and
     * logs it onto the new server, but in the future we may aim to do something fancier.
     *
     *
 If we fail to connect to the new server, the client will not be automatically
     * reconnected to the old server. It will be in a logged off state. However, it will be
     * reconfigured with the hostname and ports of the old server so that the caller can notify the
     * user of the failure and then simply call {@link #logon} to attempt to reconnect to the old
     * server.
     *
     * @param observer an observer that will be notified when we have successfully logged onto the
     * other server, or if the move failed.
     */
    public function moveToServer (hostname :String, ports :Array,
                                  obs :InvocationService_ConfirmListener) :void
    {
        // the server switcher will take care of everything for us
        _switcher = new ServerSwitcher(this, hostname, ports, obs);
        _switcher.switchServers();
    }

    /**
     * Requests that the client log off of the server to which it is connected.
     *
     * @param abortable if true, the client will call clientWillDisconnect on allthe client
     * observers and abort the logoff process if any of them return false. If false,
     * clientWillDisconnect will not be called.
     *
     * @return true if the logoff succeeded, false if it failed due to a disagreeable observer.
     */
    public function logoff (abortable :Boolean) :Boolean
    {
        if (_comm == null) {
            log.warning("Ignoring request to log off: not logged on.");
            return true;
        }

        // if this is an externally initiated logoff request, clear our active session status
        if (_switcher == null) {
            _activeSession = false;
        }

        // if the request is abortable, let's run it past the observers.  if any of them call
        // preventDefault() then the logoff will be cancelled
        if (abortable && !notifyObservers(ClientEvent.CLIENT_WILL_LOGOFF)) {
            _activeSession = true; // restore our active session status
            return false;
        }

        if (_tickInterval != null) {
            _tickInterval.stop();
            _tickInterval = null;
        }

        _comm.logoff();
        return true;
    }

    public function gotBootstrap (data :BootstrapData, omgr :DObjectManager) :void
    {
        // log.debug("Got bootstrap " + data + ".");

        _bstrap = data;
        _omgr = omgr;
        _cloid = data.clientOid;

        _invdir.init(omgr, _cloid, this);

        // log.debug("TimeBaseService: " + requireService(TimeBaseService));
    }

    /**
     * Called every five seconds; ensures that we ping the server if we haven't communicated in a
     * long while.
     */
    protected function tick (event :TimerEvent) :void
    {
        if (_comm == null) {
            return;
        }

        var now :uint = flash.utils.getTimer();
        if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) {
            _comm.postMessage(new PingRequest());
        }
    }

    /**
     * Called by the {@link Communicator} if it is experiencing trouble logging on but is still
     * trying fallback strategies.
     */
    internal function reportLogonTribulations (cause :LogonError) :void
    {
        notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, cause);
    }

    /**
     * Called by the invocation director when it successfully subscribes to the client object
     * immediately following logon.
     */
    public function gotClientObject (clobj :ClientObject) :void
    {
        // keep our client object around
        _clobj = clobj;

        // and start up our tick interval (which will send pings when necessary)
        if (_tickInterval == null) {
            _tickInterval = new Timer(5000);
            _tickInterval.addEventListener(TimerEvent.TIMER, tick);
            _tickInterval.start();
        }

        notifyObservers(ClientEvent.CLIENT_DID_LOGON);

        // now that we've logged on at least once, we're in the middle of an active session and
        // will remain so until we receive an externally initiated logoff request
        _activeSession = true;
    }

    /**
     * Called by the invocation director if it fails to subscribe to the client object after logon.
     */
    public function getClientObjectFailed (cause :Error) :void
    {
        notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, cause);
    }

    /**
     * Called by the invocation director when it discovers that the client object has changed.
     */
    protected function clientObjectDidChange (clobj :ClientObject) :void
    {
        _clobj = clobj;
        _cloid = clobj.getOid();

        notifyObservers(ClientEvent.CLIENT_OBJECT_CHANGED);
    }

    /**
     * Convenience method to dispatch a client event to any listeners and return the result of
     * dispatchEvent.
     */
    public function notifyObservers (evtCode :String, cause :Error = null) :Boolean
    {
        return dispatchEvent(new ClientEvent(evtCode, this, _activeSession, cause));
    }

    /**
     * Called by the omgr when we receive a pong packet.
     */
    internal function gotPong (pong :PongResponse) :void
    {
        // TODO: compute time delta?
    }

    internal function setOutgoingMessageThrottle (messagesPerSec :int) :void
    {
        _comm.postMessage(new ThrottleUpdatedMessage(messagesPerSec));
    }

    internal function finalizeOutgoingMessageThrottle (messagesPerSec :int) :void
    {
        // when the throttle update message goes out to the server
        _outThrottle.reinit(messagesPerSec, 1000);
        log.info("Updated outgoing throttle", "messagesPerSec", messagesPerSec);
    }

    internal function getOutgoingMessageThrottle () :Throttle
    {
        return _outThrottle;
    }

    internal function cleanup (logonError :Error) :void
    {
        // clear out our references
        _comm = null;
        _bstrap = null;
        _omgr = null;
        _clobj = null;
        _cloid = -1;

        // and let our invocation director know we're logged off
        _invdir.cleanup();

        // if this was due to a logon error, we can notify our listeners now that we're cleaned up:
        // they may want to retry logon on another port, or something
        if (logonError != null) {
            notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, logonError);
        } else {
            notifyObservers(ClientEvent.CLIENT_DID_CLEAR, null);
        }

        // clear out any server switcher reference
        _switcher = null;
    }

    /** The credentials we used to authenticate with the server. */
    protected var _creds :Credentials;

    /** The version string reported to the server at auth time. */
    protected var _version :String = "";

    /** The distributed object manager we're using during this session. */
    protected var _omgr :DObjectManager;

    /** The data associated with our authentication response. */
    protected var _authData :AuthResponseData;

    /** Our client distributed object id. */
    protected var _cloid :int = -1;

    /** Our client distributed object. */
    protected var _clobj :ClientObject;

    /** The game server host. */
    protected var _hostname :String;

    /** The port on which we connect to the game server. */
    protected var _ports :Array; /* of int */

    /** The entity that manages our network communications. */
    protected var _comm :Communicator;

    /** Our outgoing message throttle. */
    protected var _outThrottle :Throttle = new Throttle(DEFAULT_MSGS_PER_SECOND, 1000);

    /** The set of bootstrap service groups this client cares about. */
    protected var _bootGroups :Array = new Array(InvocationCodes.GLOBAL_GROUP);

    /** General startup information provided by the server. */
    protected var _bstrap :BootstrapData;

    /** Manages invocation services. */
    protected var _invdir :InvocationDirector = new InvocationDirector();

    /** Ticks. */
    protected var _tickInterval :Timer;

    /** This flag is used to distinguish our *first* willLogon/didLogon and a caller-initiated
     * willLogoff/didLogoff from similar events generated during server switches. Thus it is true
     * for most of a normal client session. */
    protected var _activeSession :Boolean;

    /** Used to temporarily track our server switcher so that we can tell when we're logging off
     * whether or not we're switching servers or actually ending our session. */
    protected var _switcher :ServerSwitcher;
}
}

Clear out our _bstrap reference after we logoff.

git-svn-id: a1a4b28b82a3276cc491891159dd9963a0a72fae@5734 542714f4-19e9-0310-aa3c-eee0fc999fb1


package aerys.minko.scene.node.light
{
	import aerys.minko.ns.minko_scene;
	import aerys.minko.render.material.phong.PhongProperties;
	import aerys.minko.render.resource.texture.TextureResource;
	import aerys.minko.scene.controller.light.DirectionalLightController;
	import aerys.minko.scene.node.AbstractSceneNode;
	import aerys.minko.scene.node.ISceneNode;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.binding.DataBindings;
	import aerys.minko.type.enum.ShadowMappingType;
	import aerys.minko.type.math.Matrix4x4;
	import aerys.minko.type.math.Vector4;

	use namespace minko_scene;

	/**
	 *
	 * @author Romain Gilliotte
	 * @author Jean-Marc Le Roux
	 *
	 */
	public class DirectionalLight extends AbstractLight
	{
		private static const LIGHT_TYPE : uint = 1;

		public function get diffuse() : Number
		{
			return lightData.getLightProperty('diffuse') as Number;
		}
		public function set diffuse(v : Number)	: void
		{
			lightData.setLightProperty('diffuse', v);

			if (lightData.getLightProperty('diffuseEnabled') != (v != 0))
				lightData.setLightProperty('diffuseEnabled', v != 0);
		}

		public function get specular() : Number
		{
			return lightData.getLightProperty('specular') as Number;
		}
		public function set specular(v : Number) : void
		{
			lightData.setLightProperty('specular', v);

			if (lightData.getLightProperty('specularEnabled') != (v != 0))
				lightData.setLightProperty('specularEnabled', v != 0);
		}

		public function get shininess() : Number
		{
			return lightData.getLightProperty('shininess') as Number;
		}
		public function set shininess(value : Number) : void
		{
			lightData.setLightProperty('shininess', value);
		}

		public function get shadowWidth() : Number
		{
			return lightData.getLightProperty('shadowWidth');
		}
		public function set shadowWidth(v : Number) : void
		{
			lightData.setLightProperty('shadowWidth', v);
		}

		public function get shadowZFar() : Number
		{
			return lightData.getLightProperty('shadowZFar');
		}
		public function set shadowZFar(v : Number) : void
		{
			lightData.setLightProperty('shadowZFar', v);
		}

		public function get shadowQuality() : uint
		{
			return lightData.getLightProperty('shadowQuality');
		}
		public function set shadowQuality(v : uint) : void
		{
			lightData.setLightProperty('shadowQuality', v);
		}

		public function get shadowSpread() : uint
		{
			return lightData.getLightProperty('shadowSpread');
		}
		public function set shadowSpread(v : uint) : void
		{
			lightData.setLightProperty('shadowSpread', v);
		}

		public function get shadowMapSize() : uint
		{
			return lightData.getLightProperty('shadowMapSize');
		}
		public function set shadowMapSize(v : uint) : void
		{
			lightData.setLightProperty('shadowMapSize', v);
		}

		public function get shadowCastingType() : uint
		{
			return lightData.getLightProperty('shadowCastingType');
		}
		public function set shadowCastingType(value : uint) : void
		{
			lightData.setLightProperty('shadowCastingType', value);
		}

        public function get shadowBias() : Number
        {
            return lightData.getLightProperty(PhongProperties.SHADOW_BIAS);
        }
        public function set shadowBias(value : Number) : void
        {
            lightData.setLightProperty(PhongProperties.SHADOW_BIAS, value);
        }

		public function DirectionalLight(color				: uint		= 0xFFFFFFFF,
									 	 diffuse			: Number	= .6,
										 specular			: Number	= .8,
										 shininess			: Number	= 64,
										 emissionMask		: uint		= 0x1,
										 shadowCastingType	: uint		= 0,
										 shadowMapSize		: uint		= 512,
										 shadowZFar			: Number	= 1000,
										 shadowWidth		: Number	= 20,
										 shadowQuality		: uint		= 0,
										 shadowSpread		: uint		= 1,
                                         shadowBias         : uint      = .002)
		{
			super(
				new DirectionalLightController(),
				LIGHT_TYPE,
				color,
				emissionMask
			);

			this.diffuse			= diffuse;
			this.specular			= specular;
			this.shininess			= shininess;
			this.shadowCastingType	= shadowCastingType;
			this.shadowZFar			= shadowZFar;
			this.shadowWidth		= shadowWidth;
			this.shadowMapSize		= shadowMapSize;
			this.shadowQuality		= shadowQuality;
			this.shadowSpread		= shadowSpread;
            this.shadowBias     	= shadowBias;

			transform.lookAt(Vector4.ZERO, new Vector4(1, -1, 1));
		}

		override minko_scene function cloneNode() : AbstractSceneNode
		{
			var light : DirectionalLight = new DirectionalLight(
				color,
				diffuse,
				specular,
				shininess,
				emissionMask,
				shadowCastingType,
				shadowMapSize,
				shadowZFar,
				shadowWidth,
				shadowQuality,
				shadowSpread,
                shadowBias
			);

			light.name = this.name;
			light.transform.copyFrom(this.transform);

			return light;
		}
	}
}

package aerys.minko.scene.node.light
{
	import aerys.minko.ns.minko_scene;
	import aerys.minko.render.material.phong.PhongProperties;
	import aerys.minko.render.resource.texture.TextureResource;
	import aerys.minko.scene.controller.light.DirectionalLightController;
	import aerys.minko.scene.node.AbstractSceneNode;
	import aerys.minko.scene.node.ISceneNode;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.binding.DataBindings;
	import aerys.minko.type.enum.ShadowMappingType;
	import aerys.minko.type.math.Matrix4x4;
	import aerys.minko.type.math.Vector4;

	use namespace minko_scene;

	/**
	 *
	 * @author Romain Gilliotte
	 * @author Jean-Marc Le Roux
	 *
	 */
	public class DirectionalLight extends AbstractLight
	{
		private static const LIGHT_TYPE : uint = 1;

		public function get diffuse() : Number
		{
			return lightData.getLightProperty('diffuse') as Number;
		}
		public function set diffuse(v : Number)	: void
		{
			lightData.setLightProperty('diffuse', v);

			if (lightData.getLightProperty('diffuseEnabled') != (v != 0))
				lightData.setLightProperty('diffuseEnabled', v != 0);
		}

		public function get specular() : Number
		{
			return lightData.getLightProperty('specular') as Number;
		}
		public function set specular(v : Number) : void
		{
			lightData.setLightProperty('specular', v);

			if (lightData.getLightProperty('specularEnabled') != (v != 0))
				lightData.setLightProperty('specularEnabled', v != 0);
		}

		public function get shininess() : Number
		{
			return lightData.getLightProperty('shininess') as Number;
		}
		public function set shininess(value : Number) : void
		{
			lightData.setLightProperty('shininess', value);
		}

		public function get shadowWidth() : Number
		{
			return lightData.getLightProperty('shadowWidth');
		}
		public function set shadowWidth(v : Number) : void
		{
			lightData.setLightProperty('shadowWidth', v);
		}

		public function get shadowZFar() : Number
		{
			return lightData.getLightProperty('shadowZFar');
		}
		public function set shadowZFar(v : Number) : void
		{
			lightData.setLightProperty('shadowZFar', v);
		}

		public function get shadowQuality() : uint
		{
			return lightData.getLightProperty('shadowQuality');
		}
		public function set shadowQuality(v : uint) : void
		{
			lightData.setLightProperty('shadowQuality', v);
		}

		public function get shadowSpread() : uint
		{
			return lightData.getLightProperty('shadowSpread');
		}
		public function set shadowSpread(v : uint) : void
		{
			lightData.setLightProperty('shadowSpread', v);
		}

		public function get shadowMapSize() : uint
		{
			return lightData.getLightProperty('shadowMapSize');
		}
		public function set shadowMapSize(v : uint) : void
		{
			lightData.setLightProperty('shadowMapSize', v);
		}

		public function get shadowCastingType() : uint
		{
			return lightData.getLightProperty('shadowCastingType');
		}
		public function set shadowCastingType(value : uint) : void
		{
			lightData.setLightProperty('shadowCastingType', value);
		}

        public function get shadowBias() : Number
        {
            return lightData.getLightProperty(PhongProperties.SHADOW_BIAS);
        }
        public function set shadowBias(value : Number) : void
        {
            lightData.setLightProperty(PhongProperties.SHADOW_BIAS, value);
        }

		public function DirectionalLight(color				: uint		= 0xFFFFFFFF,
									 	 diffuse			: Number	= .6,
										 specular			: Number	= .8,
										 shininess			: Number	= 64,
										 emissionMask		: uint		= 0x1,
										 shadowCastingType	: uint		= 0,
										 shadowMapSize		: uint		= 512,
										 shadowZFar			: Number	= 1000,
										 shadowWidth		: Number	= 20,
										 shadowQuality		: uint		= 0,
										 shadowSpread		: uint		= 1,
                                         shadowBias         : uint      = .002)
		{
			super(
				new DirectionalLightController(),
				LIGHT_TYPE,
				color,
				emissionMask
			);

			this.diffuse			= diffuse;
			this.specular			= specular;
			this.shininess			= shininess;
			this.shadowCastingType	= shadowCastingType;
			this.shadowZFar			= shadowZFar;
			this.shadowWidth		= shadowWidth;
			this.shadowMapSize		= shadowMapSize;
			this.shadowQuality		= shadowQuality;
			this.shadowSpread		= shadowSpread;
            this.shadowBias     	= shadowBias;

			transform.lookAt(Vector4.ZERO, new Vector4(1, 1, 1));
		}

		override minko_scene function cloneNode() : AbstractSceneNode
		{
			var light : DirectionalLight = new DirectionalLight(
				color,
				diffuse,
				specular,
				shininess,
				emissionMask,
				shadowCastingType,
				shadowMapSize,
				shadowZFar,
				shadowWidth,
				shadowQuality,
				shadowSpread,
                shadowBias
			);

			light.name = this.name;
			light.transform.copyFrom(this.transform);

			return light;
		}
	}
}

fix DirectionalLight position init

fix DirectionalLight position init


//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.crowd.data {

import com.threerings.util.ClassUtil;
import com.threerings.util.Cloneable;
import com.threerings.util.Integer;
import com.threerings.util.Name;

import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.SimpleStreamableObject;

import com.threerings.presents.dobj.DSet_Entry;

import com.threerings.crowd.data.BodyObject;

/**
 * The occupant info object contains all of the information about an
 * occupant of a place that should be shared with other occupants of the
 * place. These objects are stored in the place object itself and are
 * updated when bodies enter and exit a place.
 *
 * <p> A system that builds upon the Crowd framework can extend this class to
 * include extra information about their occupants. They will need to provide a
 * derived {@link BodyObject} that creates and configures their occupant info
 * in {@link BodyObject#createOccupantInfo}.
 *
 * <p> Note also that this class implements {@link Cloneable} which means
 * that if derived classes add non-primitive attributes, they are
 * responsible for adding the code to clone those attributes when a clone
 * is requested.
 */
public class OccupantInfo extends SimpleStreamableObject
    implements DSet_Entry, Cloneable
{
    /** Constant value for {@link #status}. */
    public static const ACTIVE :int = 0;

    /** Constant value for {@link #status}. */
    public static const IDLE :int = 1;

    /** Constant value for {@link #status}. */
    public static const DISCONNECTED :int = 2;

    /** Maps status codes to human readable strings. */
    public static const X_STATUS :Array = [ "active", "idle", "discon" ];

    /** The body object id of this occupant (and our entry key). */
    public var bodyOid :int;

    /** The username of this occupant. */
    public var username :Name;

    /** The status of this occupant. */
    public var status :int = ACTIVE;

    /**
     * Constructs an occupant info record, optionally obtaining data from the
     * supplied BodyObject.
     */
    public function OccupantInfo (body :BodyObject = null)
    {
        if (body != null) {
            bodyOid = body.getOid();
            username = body.getVisibleName();
            status = body.status;
        }
    }

    /** Access to the body object id as an int. */
    public function getBodyOid () :int
    {
        return bodyOid;
    }

    /**
     * Generates a cloned copy of this instance.
     */
    public function clone () :Object
    {
        var that :OccupantInfo = ClassUtil.newInstance(this) as OccupantInfo;
        that.bodyOid = this.bodyOid;
        that.username = this.username;
        that.status = this.status;
        return that;
    }

    // documentation inherited from interface DSet_Entry
    public function getKey () :Object
    {
        return bodyOid;
    }

    // from interface Streamable
    override public function readObject (ins :ObjectInputStream) :void
    {
        super.readObject(ins);
        bodyOid = (ins.readField(Integer) as Integer).value;
        username = (ins.readObject() as Name);
        status = ins.readByte();
    }

    // from interface Streamable
    override public function writeObject (out :ObjectOutputStream) :void
    {
        super.writeObject(out);
        out.writeObject(new Integer(bodyOid));
        out.writeObject(username);
        out.writeByte(status);
    }
}
}

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.crowd.data {

import flash.system.ApplicationDomain;

import com.threerings.util.ClassUtil;
import com.threerings.util.Cloneable;
import com.threerings.util.Integer;
import com.threerings.util.Name;

import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.SimpleStreamableObject;

import com.threerings.presents.dobj.DSet_Entry;

import com.threerings.crowd.data.BodyObject;

/**
 * The occupant info object contains all of the information about an
 * occupant of a place that should be shared with other occupants of the
 * place. These objects are stored in the place object itself and are
 * updated when bodies enter and exit a place.
 *
 * <p> A system that builds upon the Crowd framework can extend this class to
 * include extra information about their occupants. They will need to provide a
 * derived {@link BodyObject} that creates and configures their occupant info
 * in {@link BodyObject#createOccupantInfo}.
 *
 * <p> Note also that this class implements {@link Cloneable} which means
 * that if derived classes add non-primitive attributes, they are
 * responsible for adding the code to clone those attributes when a clone
 * is requested.
 */
public class OccupantInfo extends SimpleStreamableObject
    implements DSet_Entry, Cloneable
{
    /** Constant value for {@link #status}. */
    public static const ACTIVE :int = 0;

    /** Constant value for {@link #status}. */
    public static const IDLE :int = 1;

    /** Constant value for {@link #status}. */
    public static const DISCONNECTED :int = 2;

    /** Maps status codes to human readable strings. */
    public static const X_STATUS :Array = [ "active", "idle", "discon" ];

    /** The body object id of this occupant (and our entry key). */
    public var bodyOid :int;

    /** The username of this occupant. */
    public var username :Name;

    /** The status of this occupant. */
    public var status :int = ACTIVE;

    /**
     * Constructs an occupant info record, optionally obtaining data from the
     * supplied BodyObject.
     */
    public function OccupantInfo (body :BodyObject = null)
    {
        if (body != null) {
            bodyOid = body.getOid();
            username = body.getVisibleName();
            status = body.status;
        }
    }

    /** Access to the body object id as an int. */
    public function getBodyOid () :int
    {
        return bodyOid;
    }

    /**
     * Generates a cloned copy of this instance.
     */
    public function clone () :Object
    {
        var that :OccupantInfo =
            ClassUtil.newInstance(this, ApplicationDomain.currentDomain) as OccupantInfo;
        that.bodyOid = this.bodyOid;
        that.username = this.username;
        that.status = this.status;
        return that;
    }

    // documentation inherited from interface DSet_Entry
    public function getKey () :Object
    {
        return bodyOid;
    }

    // from interface Streamable
    override public function readObject (ins :ObjectInputStream) :void
    {
        super.readObject(ins);
        bodyOid = (ins.readField(Integer) as Integer).value;
        username = (ins.readObject() as Name);
        status = ins.readByte();
    }

    // from interface Streamable
    override public function writeObject (out :ObjectOutputStream) :void
    {
        super.writeObject(out);
        out.writeObject(new Integer(bodyOid));
        out.writeObject(username);
        out.writeByte(status);
    }
}
}

Clone fix: pass in our ApplicationDomain.

This is sorta wacked. Grumble. I wish there was a pointer to the class always,
instead of the language letting anonymous functions be a "constructor".
This may not always work if a subclass of OccupantInfo is loaded into a sub-domain.

git-svn-id: a1a4b28b82a3276cc491891159dd9963a0a72fae@5227 542714f4-19e9-0310-aa3c-eee0fc999fb1