コード例 #1
0
ファイル: JSEvents.cs プロジェクト: MaikoTan/cactbot
 public EntityChangedEvent(FFXIVProcess.EntityData e)
 {
     if (e != null)
     {
         id        = e.id;
         level     = e.level;
         name      = e.name;
         job       = e.job.ToString();
         currentHP = e.hp;
         maxHP     = e.max_hp;
         currentMP = e.mp;
         maxMP     = e.max_mp;
         pos       = new Point3F(e.pos_x, e.pos_y, e.pos_z);
         distance  = e.distance;
     }
 }
コード例 #2
0
ファイル: JSEvents.cs プロジェクト: daggeron/cactbot
 public PlayerChangedEvent(FFXIVProcess.EntityData e)
 {
     id        = e.id;
     level     = e.level;
     name      = e.name;
     job       = e.job.ToString();
     currentHP = e.hp;
     maxHP     = e.max_hp;
     currentMP = e.mp;
     maxMP     = e.max_mp;
     maxTP     = 1000;
     currentGP = e.gp;
     maxGP     = e.max_gp;
     currentCP = e.cp;
     maxCP     = e.max_cp;
     pos       = new Point3F(e.pos_x, e.pos_y, e.pos_z);
     jobDetail = null;
     bait      = e.bait;
     debugJob  = e.debug_job;
 }
コード例 #3
0
        // Events that we want to update as soon as possible.  Return next time this should be called.
        private int SendFastRateEvents()
        {
            if (reset_notify_state_)
            {
                notify_state_ = new NotifyState();
            }
            reset_notify_state_ = false;

            // Loading dance:
            // * OverlayPlugin loads addons and initializes event sources.
            // * OverlayPlugin loads its configuration.
            // * Event sources are told to load their configuration and start (LoadConfig and Start are called).
            // * Overlays are initialised and the browser instances are started. At this points the overlays start loading.
            // * At some point the overlay's JavaScript is executed and OverlayPluginApi is injected. This order isn't
            //   deterministic and depends on what the ACT process is doing at that point in time. During startup the
            //   OverlayPluginApi is usually injected after the overlay is done loading while an overlay that's reloaded or
            //   loaded later on will see the OverlayPluginApi before the page has loaded.
            // * The overlay JavaScript sets up the initial event handlers and calls the cactbotLoadUser handler through
            //   getUserConfigLocation. These actions are queued by the JS implementation in common.js until OverlayPluginApi
            //   (or the WebSocket) is available. Once it is, the event subscriptions and handler calls are transmitted.
            // * OverlayPlugin stores the event subscriptions and executes the C# handlers which in this case means
            //   FetchUserFiles is called. That method loads the user files and returns them. The result is now transmitted
            //   back to the overlay that called the handler and the Promise in JS is resolved with the result.
            // * getUserConfigLocation processes the received information and calls the passed callback. This constructs the
            //   overlay specific objects and registers additional event handlers. Finally, the cactbotRequestState handler
            //   is called.
            // * OverlayPlugin processes the new event subscriptions and executes the cactbotRequestState handler.
            // * The next time SendFastRateEvents is called, it resets notify_state_ (since the previous handler set
            //   reset_notify_state_ to true) which causes it to dispatch all state events again. These events are now
            //   dispatched to all subscribed overlays. However, this means that overlays can receive state events multiple
            //   times during startup. If the user has three Cactbot overlays, all of them will call cactbotRequestState and
            //   thus cause this to happen one to three times depending on their timing. This shouldn't cause any issues but
            //   it's a waste of CPU cycles.
            // * Since this only happens during startup, it's probably not worth fixing though. Not sure.
            // * Some overlays behave slightly different from the above explanation. Raidboss for example loads data files
            //   in addition to the listed steps. I think it's even loading them twice since raidboss.js loads the data files
            //   for gTimelineController and popup-text.js requests them again for its own purposes.

            bool game_exists = ffxiv_.FindProcess();

            if (game_exists != notify_state_.game_exists)
            {
                notify_state_.game_exists = game_exists;
                OnGameExists(new JSEvents.GameExistsEvent(game_exists));
            }

            bool game_active = game_active = ffxiv_.IsActive();

            if (game_active != notify_state_.game_active)
            {
                notify_state_.game_active = game_active;
                OnGameActiveChanged(new JSEvents.GameActiveChangedEvent(game_active));
            }

            // Silently stop sending other messages if the ffxiv process isn't around.
            if (!game_exists)
            {
                return(kUberSlowTimerMilli);
            }

            // onInCombatChangedEvent: Fires when entering or leaving combat.
            bool in_act_combat  = Advanced_Combat_Tracker.ActGlobals.oFormActMain.InCombat;
            bool in_game_combat = ffxiv_.GetInGameCombat();

            if (!notify_state_.in_act_combat.HasValue || in_act_combat != notify_state_.in_act_combat ||
                !notify_state_.in_game_combat.HasValue || in_game_combat != notify_state_.in_game_combat)
            {
                notify_state_.in_act_combat  = in_act_combat;
                notify_state_.in_game_combat = in_game_combat;
                OnInCombatChanged(new JSEvents.InCombatChangedEvent(in_act_combat, in_game_combat));
            }

            // onZoneChangedEvent: Fires when the player changes their current zone.
            string zone_name = Advanced_Combat_Tracker.ActGlobals.oFormActMain.CurrentZone;

            if (notify_state_.zone_name == null || !zone_name.Equals(notify_state_.zone_name))
            {
                notify_state_.zone_name = zone_name;
                OnZoneChanged(new JSEvents.ZoneChangedEvent(zone_name));
                ClearFateWatcherDictionaries();
            }

            DateTime now = DateTime.Now;

            // The |player| can be null, such as during a zone change.
            FFXIVProcess.EntityData player = ffxiv_.GetSelfData();

            // onPlayerDiedEvent: Fires when the player dies. All buffs/debuffs are
            // lost.
            if (player != null)
            {
                bool dead = player.hp == 0;
                if (dead != notify_state_.dead)
                {
                    notify_state_.dead = dead;
                    if (dead)
                    {
                        OnPlayerDied(new JSEvents.PlayerDiedEvent());
                    }
                }
            }

            // onPlayerChangedEvent: Fires when current player data changes.
            if (player != null)
            {
                bool send = false;
                if (!player.Equals(notify_state_.player))
                {
                    // Clear the FateWatcher dictionaries if we switched characters
                    if (notify_state_.player != null && !player.name.Equals(notify_state_.player.name))
                    {
                        ClearFateWatcherDictionaries();
                    }
                    notify_state_.player = player;
                    send = true;
                }
                var job = ffxiv_.GetJobSpecificData(player.job);
                if (job != null)
                {
                    if (send || !JObject.DeepEquals(job, notify_state_.job_data))
                    {
                        notify_state_.job_data = job;
                        var e = new JSEvents.PlayerChangedEvent(player);
                        e.jobDetail = job;
                        OnPlayerChanged(e);
                    }
                }
                else if (send)
                {
                    // No job-specific data.
                    OnPlayerChanged(new JSEvents.PlayerChangedEvent(player));
                }
            }

            // onLogEvent: Fires when new combat log events from FFXIV are available. This fires after any
            // more specific events, some of which may involve parsing the logs as well.
            List <string> logs;
            List <string> import_logs;

            log_lines_semaphore_.Wait();
            logs              = log_lines_;
            log_lines_        = last_log_lines_;
            import_logs       = import_log_lines_;
            import_log_lines_ = last_import_log_lines_;
            log_lines_semaphore_.Release();

            if (logs.Count > 0)
            {
                OnLogsChanged(new JSEvents.LogEvent(logs));
                logs.Clear();
            }
            if (import_logs.Count > 0)
            {
                OnImportLogsChanged(new JSEvents.ImportLogEvent(import_logs));
                import_logs.Clear();
            }

            last_log_lines_        = logs;
            last_import_log_lines_ = import_logs;

            return(game_active ? kFastTimerMilli : kSlowTimerMilli);
        }
コード例 #4
0
ファイル: JSEvents.cs プロジェクト: mattddonovan/cactbot
 public FocusChangedEvent(FFXIVProcess.EntityData e) : base(e)
 {
 }
コード例 #5
0
ファイル: JSEvents.cs プロジェクト: mattddonovan/cactbot
 public TargetChangedEvent(FFXIVProcess.EntityData e) : base(e)
 {
 }
コード例 #6
0
        // Events that we want to update as soon as possible.  Return next time this should be called.
        private int SendFastRateEvents()
        {
            // Handle startup and shutdown. And do not fire any events until the page has loaded and had a chance to
            // register its event handlers.
            if (Overlay == null || Overlay.Renderer == null || Overlay.Renderer.Browser == null || Overlay.Renderer.Browser.IsLoading)
            {
                return(kSlowTimerMilli);
            }

            if (reset_notify_state_)
            {
                notify_state_ = new NotifyState();
            }
            reset_notify_state_ = false;

            // Loading dance:
            // * wait for !CefBrowser::IsLoading.
            // * Execute JS in all overlays to wait for DOMContentReady and send back an event.
            // * When this OverlayMessage comes in, send back an onInitializeOverlay message.
            // * Overlays should load options from the provider user data dir in that message.
            // * As DOMContentReady happened, overlays should also construct themselves and attach to the DOM.
            // * Now, messages can be sent freely and everything is initialized.
            if (!notify_state_.added_dom_content_listener)
            {
                // Send this from C# so that overlays that are using non-cactbot html
                // (e.g. dps overlays) don't have to be modified.
                const string waitForDOMContentReady = @"
          (function() {
            var sendDOMContentLoaded = function() {
              if (!window.OverlayPluginApi) {
                window.setTimeout(sendDOMContentLoaded, 100);
              } else {
                window.OverlayPluginApi.overlayMessage(OverlayPluginApi.overlayName, JSON.stringify({'onDOMContentLoaded': true}));
              }
            };
            if (document.readyState == 'loaded' || document.readyState == 'complete') {
              sendDOMContentLoaded();
            } else {
              document.addEventListener('DOMContentLoaded', sendDOMContentLoaded);
            }
          })();
        ";
                this.Overlay.Renderer.ExecuteScript(waitForDOMContentReady);
                notify_state_.added_dom_content_listener = true;
            }

            // This flag set as a result of onDOMContentLoaded overlay message.
            if (!notify_state_.dom_content_loaded)
            {
                return(kSlowTimerMilli);
            }

            if (!notify_state_.sent_data_dir && Config.Url.Length > 0)
            {
                notify_state_.sent_data_dir = true;

                var url = Config.Url;
                // If file is a remote pointer, load that file explicitly so that the manifest
                // is relative to the pointed to url and not the local file.
                if (url.StartsWith("file:///"))
                {
                    var html  = File.ReadAllText(new Uri(url).LocalPath);
                    var match = System.Text.RegularExpressions.Regex.Match(html, @"<meta http-equiv=""refresh"" content=""0; url=(.*)?""\/?>");
                    if (match.Groups.Count > 1)
                    {
                        url = match.Groups[1].Value;
                    }
                }

                var web = new System.Net.WebClient();
                web.Encoding = System.Text.Encoding.UTF8;
                System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Ssl3 | System.Net.SecurityProtocolType.Tls | System.Net.SecurityProtocolType.Tls11 | System.Net.SecurityProtocolType.Tls12;

                var data_file_paths = new List <string>();
                try {
                    var data_dir_manifest = new Uri(new Uri(url), "data/manifest.txt");
                    var manifest_reader   = new StringReader(web.DownloadString(data_dir_manifest));
                    for (var line = manifest_reader.ReadLine(); line != null; line = manifest_reader.ReadLine())
                    {
                        line = line.Trim();
                        if (line.Length > 0)
                        {
                            data_file_paths.Add(line);
                        }
                    }
                } catch (System.Net.WebException e) {
                    if (e.Status == System.Net.WebExceptionStatus.ProtocolError &&
                        e.Response is System.Net.HttpWebResponse &&
                        ((System.Net.HttpWebResponse)e.Response).StatusCode == System.Net.HttpStatusCode.NotFound)
                    {
                        // Ignore file not found.
                    }
                    else if (e.InnerException != null &&
                             (e.InnerException is FileNotFoundException || e.InnerException is DirectoryNotFoundException))
                    {
                        // Ignore file not found.
                    }
                    else if (e.InnerException != null && e.InnerException.InnerException != null &&
                             (e.InnerException.InnerException is FileNotFoundException || e.InnerException.InnerException is DirectoryNotFoundException))
                    {
                        // Ignore file not found.
                    }
                    else
                    {
                        LogError("Unable to read manifest file: " + e.Message);
                    }
                } catch (Exception e) {
                    LogError("Unable to read manifest file: " + e.Message);
                }

                if (data_file_paths.Count > 0)
                {
                    var file_data = new Dictionary <string, string>();
                    foreach (string data_filename in data_file_paths)
                    {
                        try {
                            var file_path = new Uri(new Uri(url), "data/" + data_filename);
                            file_data[data_filename] = web.DownloadString(file_path);
                        } catch (Exception e) {
                            LogError("Unable to read data file: " + e.Message);
                        }
                    }
                    OnDataFilesRead(new JSEvents.DataFilesRead(file_data));
                }
            }

            bool game_exists = ffxiv_.FindProcess();

            if (game_exists != notify_state_.game_exists)
            {
                notify_state_.game_exists = game_exists;
                OnGameExists(new JSEvents.GameExistsEvent(game_exists));
            }

            bool game_active = game_active = ffxiv_.IsActive();

            if (game_active != notify_state_.game_active)
            {
                notify_state_.game_active = game_active;
                OnGameActiveChanged(new JSEvents.GameActiveChangedEvent(game_active));
            }

            // Silently stop sending other messages if the ffxiv process isn't around.
            if (!game_exists)
            {
                return(kUberSlowTimerMilli);
            }

            // onInCombatChangedEvent: Fires when entering or leaving combat.
            bool in_act_combat  = Advanced_Combat_Tracker.ActGlobals.oFormActMain.InCombat;
            bool in_game_combat = ffxiv_.GetInGameCombat();

            if (!notify_state_.in_act_combat.HasValue || in_act_combat != notify_state_.in_act_combat ||
                !notify_state_.in_game_combat.HasValue || in_game_combat != notify_state_.in_game_combat)
            {
                notify_state_.in_act_combat  = in_act_combat;
                notify_state_.in_game_combat = in_game_combat;
                OnInCombatChanged(new JSEvents.InCombatChangedEvent(in_act_combat, in_game_combat));
            }

            // onZoneChangedEvent: Fires when the player changes their current zone.
            string zone_name = Advanced_Combat_Tracker.ActGlobals.oFormActMain.CurrentZone;

            if (notify_state_.zone_name == null || !zone_name.Equals(notify_state_.zone_name))
            {
                notify_state_.zone_name = zone_name;
                OnZoneChanged(new JSEvents.ZoneChangedEvent(zone_name));
            }

            DateTime now = DateTime.Now;

            // The |player| can be null, such as during a zone change.
            FFXIVProcess.EntityData player = ffxiv_.GetSelfData();
            // The |target| can be null when no target is selected.
            FFXIVProcess.EntityData target = ffxiv_.GetTargetData();
            // The |focus| can be null when no focus target is selected.
            FFXIVProcess.EntityData focus = ffxiv_.GetFocusData();

            // onPlayerDiedEvent: Fires when the player dies. All buffs/debuffs are
            // lost.
            if (player != null)
            {
                bool dead = player.hp == 0;
                if (dead != notify_state_.dead)
                {
                    notify_state_.dead = dead;
                    if (dead)
                    {
                        OnPlayerDied(new JSEvents.PlayerDiedEvent());
                    }
                }
            }

            // onPlayerChangedEvent: Fires when current player data changes.
            if (player != null)
            {
                bool send = false;
                if (player != notify_state_.player)
                {
                    notify_state_.player = player;
                    send = true;
                }

                if (player.job == FFXIVProcess.EntityJob.RDM)
                {
                    var job = ffxiv_.GetRedMage();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.rdm))
                        {
                            notify_state_.rdm = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.RedMageDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.WAR)
                {
                    var job = ffxiv_.GetWarrior();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.war))
                        {
                            notify_state_.war = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.WarriorDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.DRK)
                {
                    var job = ffxiv_.GetDarkKnight();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.drk))
                        {
                            notify_state_.drk = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.DarkKnightDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.PLD)
                {
                    var job = ffxiv_.GetPaladin();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.pld))
                        {
                            notify_state_.pld = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.PaladinDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.BRD)
                {
                    var job = ffxiv_.GetBard();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.brd))
                        {
                            notify_state_.brd = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.BardDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.NIN)
                {
                    var job = ffxiv_.GetNinja();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.nin))
                        {
                            notify_state_.nin = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.NinjaDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.DRG)
                {
                    var job = ffxiv_.GetDragoon();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.drg))
                        {
                            notify_state_.drg = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.DragoonDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.BLM || player.job == FFXIVProcess.EntityJob.THM)
                {
                    var job = ffxiv_.GetBlackMage();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.blm))
                        {
                            notify_state_.blm = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.BlackMageDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.WHM)
                {
                    var job = ffxiv_.GetWhiteMage();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.whm))
                        {
                            notify_state_.whm = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.WhiteMageDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.SMN || player.job == FFXIVProcess.EntityJob.SCH || player.job == FFXIVProcess.EntityJob.ACN)
                {
                    var job = ffxiv_.GetSummonerAndScholar();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.smn_sch))
                        {
                            notify_state_.smn_sch = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.SummonerAndScholarDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.MNK || player.job == FFXIVProcess.EntityJob.PGL)
                {
                    var job = ffxiv_.GetMonk();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.mnk))
                        {
                            notify_state_.mnk = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.MonkDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.MCH)
                {
                    var job = ffxiv_.GetMachinist();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.mch))
                        {
                            notify_state_.mch = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.MachinistDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.AST)
                {
                    var job = ffxiv_.GetAstrologian();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.ast))
                        {
                            notify_state_.ast = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.AstrologianDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (player.job == FFXIVProcess.EntityJob.SAM)
                {
                    var job = ffxiv_.GetSamurai();
                    if (job != null)
                    {
                        if (send || !job.Equals(notify_state_.sam))
                        {
                            notify_state_.sam = job;
                            var e = new JSEvents.PlayerChangedEvent(player);
                            e.jobDetail = new JSEvents.PlayerChangedEvent.SamuraiDetail(job);
                            OnPlayerChanged(e);
                        }
                    }
                }
                else if (send)
                {
                    // No job-specific data.
                    OnPlayerChanged(new JSEvents.PlayerChangedEvent(player));
                }
            }

            // onTargetChangedEvent: Fires when current target or their state changes.
            if (target != notify_state_.target)
            {
                notify_state_.target = target;
                if (target != null)
                {
                    OnTargetChanged(new JSEvents.TargetChangedEvent(target));
                }
                else
                {
                    OnTargetChanged(new JSEvents.TargetChangedEvent(null));
                }
            }

            // onFocusChangedEvent: Fires when current focus target or their state changes.
            if (focus != notify_state_.focus)
            {
                notify_state_.focus = focus;
                if (target != null)
                {
                    OnFocusChanged(new JSEvents.FocusChangedEvent(focus));
                }
                else
                {
                    OnFocusChanged(new JSEvents.FocusChangedEvent(null));
                }
            }

            // onLogEvent: Fires when new combat log events from FFXIV are available. This fires after any
            // more specific events, some of which may involve parsing the logs as well.
            List <string> logs;
            List <string> import_logs;

            log_lines_semaphore_.Wait();
            logs              = log_lines_;
            log_lines_        = last_log_lines_;
            import_logs       = import_log_lines_;
            import_log_lines_ = last_import_log_lines_;
            log_lines_semaphore_.Release();

            if (logs.Count > 0)
            {
                OnLogsChanged(new JSEvents.LogEvent(logs));
                logs.Clear();
            }
            if (import_logs.Count > 0)
            {
                OnImportLogsChanged(new JSEvents.ImportLogEvent(import_logs));
                import_logs.Clear();
            }

            last_log_lines_        = logs;
            last_import_log_lines_ = import_logs;
            fight_tracker_.Tick(DateTime.Now);

            return(game_active ? kFastTimerMilli : kSlowTimerMilli);
        }
コード例 #7
0
 public StatusEffectsUpdateEvent(FFXIVProcess.EntityData player, FFXIVProcess.EntityData target, FFXIVProcess.EntityData focus)
 {
     playerStatusEffects = player?.statusEffects;
     targetStatusEffects = target?.statusEffects;
     focusStatusEffects  = focus?.statusEffects;
 }