Clean Notifications

Overview

Clean Notifications stops the notification window from showing AP/SP when all abilities/skills are already maxed. The FIFO text telling you that you've reached the cap is also prevented. Some additional options are made available for fine tuning the specifics.

The current version is 1.10.1. However, the text below was written against 1.4, so there are a few slight differences.

 

Setup

Clean Notifications is a modification to two of the standard UI elements, GUI/HUD/AnimaWheelLink and GUI/Fifo/Fifo.

To get started, first create a working folder called CleanNotifications under "The Secret World\Data\Gui\Customized\Flash". Then copy the following four files to the new folder:

  • The Secret World\Data\Gui\Customized\Flash\Sources\GUI\HUD\AnimaWheelLink.as
  • The Secret World\Data\Gui\Customized\Flash\Sources\GUI\HUD\AnimaWheelLink.fla
  • The Secret World\Data\Gui\Customized\Flash\Sources\GUI\Fifo\Fifo.as
  • The Secret World\Data\Gui\Customized\Flash\Sources\GUI\Fifo\Fifo.fla

cnsetup1

 

Modifications

AnimaWheelLink.as

The first order of business will be to determine when AP and SP are actually maxed. We want to do this in a way that doesn't require updating with every patch, so we'll need to ask the game to tell us about the player's training. The FeatInterface's FeatList has the data we need. There's no good way to know if a given "Feat" is trained using SP or AP, so we need to use the ClusterIndex; at this time, the range of 1000 to 2000 uses SP and the rest use AP.

First, we'll add a couple of new functions and some supporting variables and initialization:

var m_SkillsMaxed:Boolean;
var m_AnimaMaxed:Boolean;
function onLoad():Void
{ 
    m_SkillsMaxed = false;
    m_AnimaMaxed = false;
function SkillsMaxed():Boolean
{
    if (m_SkillsMaxed) return true;
    for (var featID in FeatInterface.m_FeatList)
    {
        var featData:FeatData = FeatInterface.m_FeatList[featID];
        if (featData != undefined && !featData.m_AutoTrain && !featData.m_Trained &&
            featData.m_ClusterIndex > 1000 && featData.m_ClusterIndex < 2000)
            return false;
    }
    m_SkillsMaxed = true;
    return true;
}
function AnimaMaxed():Boolean
{
    if (m_AnimaMaxed) return true;
    for (var featID in FeatInterface.m_FeatList)
    {
        var featData:FeatData = FeatInterface.m_FeatList[featID];
        if (featData != undefined && !featData.m_AutoTrain && !featData.m_Trained &&
            (featData.m_ClusterIndex < 1000 || featData.m_ClusterIndex > 2000))
            return false;
    }
    m_AnimaMaxed = true;
    return true;
}

Now we want to use these new functions to skip showing the notifications when they are unwarranted. We'll modify the highlighted lines in the following function:

//Slot Token Amount Changed
function SlotTokenAmountChanged(id:Number, newValue:Number, oldValue:Number):Void
{
  var targetIcon:MovieClip;
  var totalPoints:Number;
  var headline:String;
  var bodyText:String;
  var maxed:Boolean;
  
  if (id == 1) //Anima Points
  {
    targetIcon = m_AnimaPointsIcon;
    totalPoints = GetAnimaPoints();
    headline = LDBFormat.LDBGetText("GenericGUI", "AnimaWheelLink_AnimaPointsHeader");
    bodyText = LDBFormat.Printf(LDBFormat.LDBGetText("GenericGUI", "AnimaWheelLink_AnimaPointsBody"), totalPoints);
    maxed = AnimaMaxed();
  }
  
  if (id == 2) //Skill Points
  {
    targetIcon = m_SkillPointsIcon;
    totalPoints = GetSkillPoints();
    headline = LDBFormat.LDBGetText("GenericGUI", "AnimaWheelLink_SkillPointsHeader");
    bodyText = LDBFormat.Printf(LDBFormat.LDBGetText("GenericGUI", "AnimaWheelLink_SkillPointsBody"), totalPoints);  
    maxed = SkillsMaxed();
  }
  
  CreateRealTooltip(targetIcon, headline, bodyText);
  
  SetVisible(targetIcon, !maxed && totalPoints > 0 && oldValue < newValue);
  targetIcon.m_IsClicked = false;
  targetIcon.m_Badge.SetCharge(totalPoints);
  targetIcon.onPress = RealPresshandler;
}

That's actually all it takes to hide the useless notifications. However, we'd like to add some extra features as well, such as the ability to stop the blinking and to hide some or all notifications explicitly. For that, we'll need to add some DistributedValues,

var m_BlinkNotificationsMonitor:DistributedValue;
var m_HideNotificationsMonitor:DistributedValue;

initialize them onLoad,

function onLoad():Void
{
  m_BlinkNotificationsMonitor = DistributedValue.Create("BlinkNotifications");
  m_BlinkNotificationsMonitor.SignalChanged.Connect(SlotBlinkNotifications, this);
  SlotBlinkNotifications();
  
  m_HideNotificationsMonitor = DistributedValue.Create("HideNotifications");
  m_HideNotificationsMonitor.SignalChanged.Connect(SlotHideNotifications, this);
  SlotHideNotifications();
}

and then create some listener functions:

function SlotBlinkNotifications():Void
{
  if (m_NotificationThrottleIntervalId > -1)
    clearInterval( m_NotificationThrottleIntervalId );
  
  if (m_BlinkNotificationsMonitor.GetValue())
    m_NotificationThrottleIntervalId = setInterval(Delegate.create(this, AnimateUnclickedNotifications), m_NotificationThrottleInterval );
}
function SlotHideNotifications():Void
{
  for (var i:Number = 0; i < m_VisibleNotificationsArray.length; i++)
  {
    SetVisible(m_VisibleNotificationsArray[i], true);
  }
}

The DistributedValue instances attach to options that can be set via the game's XML files, or with the /option command in game. The Signals cause the Slot methods to be called if the value of the option changes while the game is running, ensuring that the change takes place immediately. The first listener allows us to set the value of BlinkNotifications to true or false to control the strobing effect. The second merely forces a refresh when HideNotifications is changed.

SlotHideNotifications simply calls SetVisible, so our next step is to modify this function to be smarter:

//Set Visible
function SetVisible(targetIcon:MovieClip, visible:Boolean):Void
{
  var hideWhich:String = m_HideNotificationsMonitor.GetValue().toLowerCase();
  if (hideWhich == "all")
    targetIcon._visible = false;
  else if (hideWhich == "apsp" && (targetIcon == m_AnimaPointsIcon || targetIcon == m_SkillPointsIcon))
    targetIcon._visible = false;
  else
    targetIcon._visible = visible;

  if (visible)
  {
    var found:Boolean = false;
    for (var i:Number = 0; i < m_VisibleNotificationsArray.length; i++)
    {
      if (m_VisibleNotificationsArray[i] == targetIcon)
        found = true;
    }
    if (!found)
      m_VisibleNotificationsArray.push(targetIcon);
  }
  else
  {
    for (var i:Number = 0; i < m_VisibleNotificationsArray.length; i++)
    {
      if (m_VisibleNotificationsArray[i] == targetIcon)
      {
        m_VisibleNotificationsArray.splice(i, 1);
      }
    }
  }
  var y:Number = 0;
  for (var i:Number = 0; i < m_VisibleNotificationsArray.length; i++)
  {
    if (hideWhich != "apsp" || (m_VisibleNotificationsArray[i] != m_AnimaPointsIcon && m_VisibleNotificationsArray[i] != m_SkillPointsIcon))
      m_VisibleNotificationsArray[i]._y = 0 - ICON_DISPLACEMENT * y++;
  }
}

These improvements now allow us to set the HideNotifications option value to "apsp" to always hide the AP/SP notifications, to "all" to hide all notifications, or to "none" for the game's normal behavior.

 

Fifo.as

Hiding the notifications is great, but we still get an annoying text popup in the FIFO area that we'd also like to hide. For this, we'll just add some code to the FIFO notifier itself to ignore messages that match a pattern.

First, the imports we'll need:

import com.Utils.LDBFormat;
import com.GameInterface.FeatData;
import com.GameInterface.FeatInterface;

Next, we'll be needing the two methods AnimaMaxed and SkillMaxed that we wrote previously and the supporting variables and initialization. Go ahead and copy/paste those.

var m_SkillsMaxed:Boolean;
var m_AnimaMaxed:Boolean;
function onLoad()
{
  m_SkillsMaxed = false;
  m_AnimaMaxed = false;
function SkillsMaxed():Boolean
{
    if (m_SkillsMaxed) return true;
    for (var featID in FeatInterface.m_FeatList)
    {
        var featData:FeatData = FeatInterface.m_FeatList[featID];
        if (featData != undefined && !featData.m_AutoTrain && !featData.m_Trained &&
            featData.m_ClusterIndex > 1000 && featData.m_ClusterIndex < 2000)
            return false;
    }
    m_SkillsMaxed = true;
    return true;
}
function AnimaMaxed():Boolean
{
    if (m_AnimaMaxed) return true;
    for (var featID in FeatInterface.m_FeatList)
    {
        var featData:FeatData = FeatInterface.m_FeatList[featID];
        if (featData != undefined && !featData.m_AutoTrain && !featData.m_Trained &&
            (featData.m_ClusterIndex < 1000 || featData.m_ClusterIndex > 2000))
            return false;
    }
    m_AnimaMaxed = true;
    return true;
}

Then we'll add some simple code to the beginning of SlotShowFIFOMessage to ignore AP/SP messages:

function SlotShowFIFOMessage( text:String, mode:Number )
{
  // Don't add the line if it's the AP/SP capped notification and points are maxed.
  if ((text == m_APText && AnimaMaxed()) || (text == m_SPText && SkillsMaxed()))
    return;

And finally, we need to initialize m_APText and m_SPText in onLoad. These values need to be dynamic to work with the different interface languages:

var m_APText:String;
var m_SPText:String;
function onLoad()
{
  var APSPText:String = LDBFormat.LDBGetText(100, 24702512);
  // The first time the above statement runs, it successfully returns "Blah %s blah %d.".
  // Absurdly, if it is called after receiving the message in game (say, a /reloadui after
  // turning in some quests), it returns "Blah Skill Point blah 40." instead. Therefore
  // the following attempts to detect and undo this mess.
  var APText:String = LDBFormat.LDBGetText(10028, 1);
  var SPText:String = LDBFormat.LDBGetText(10028, 2);
  var APCap:String = "" + com.GameInterface.Utils.GetGameTweak("LevelTokensCap");
  var SPCap:String = "" + com.GameInterface.Utils.GetGameTweak("SkillTokensCap");
  
  if (APSPText.indexOf("%") > 0) {
    m_APText = LDBFormat.Printf(APSPText, APText, APCap);
    m_SPText = LDBFormat.Printf(APSPText, SPText, SPCap);
  } else if (APSPText.indexOf(APText) > 0) {
    m_APText = APSPText;
    m_SPText = APSPText.split(APText).join(SPText).split(APCap).join(SPCap);
  } else if (APSPText.indexOf(SPText) > 0) {
    m_SPText = APSPText;
    m_APText = APSPText.split(SPText).join(APText).split(SPCap).join(APCap);
  }
}

As you can see from the comments, there's a bit of strangeness that has to be worked around when reloading the UI. Nothing terribly difficult, but definitely unexpected. Also, I know that LDBGetText can take string arguments, but I don't know how to figure out what the right strings to use are. I do know how to get the numeric values, though I'm afraid I can't share that knowledge as it involves delving into the game files in a way that Funcom frowns upon.

 

LoginPrefs.xml

We created some DistributedValue options earlier, but right now the values would be reset every time the player exits the game. In order to save the values and provide defaults, we need to create an XML file in the CleanNotifications folder called LoginPrefs.xml. The contents will be:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Root>
  <Value name="BlinkNotifications" value="true" />
  <Value name="HideNotifications" value="none" />
</Root>

The name of the file determines at what level the values are saved. In this case, we choose LoginPrefs so that the same values will apply to all characters within the same account.

 

Building

AnimaWheelLink.fla

Upon opening this file, you should see that the Futura-HeavyFix and FuturaStd-BookFix fonts are missing and need to be mapped. If you've followed my environment set up guide, you'll map these to Futura Heavy/Heavy and Futura Md/Medium, respectively.

Next, configure font embedding to be more restrictive to help reduce the file size:

  1. Go to Text, Font Embedding
  2. Select (Fonts/_Headline)
  3. In the Character ranges box, uncheck All
  4. In the same box, check only the following groups:
    1. Punctuation
    2. Basic Latin
    3. Latin I
    4. Latin Extended A
    5. Latin Extended Add'l
  5. Select (Fonts/_StandardFont) and make the same changes as above
  6. Click OK

To create the .swf file:

  1. Go to File, Publish Settings
  2. Change the Output file to "..\AnimaWheelLink.swf"
  3. Uncheck Include XMP metadata
  4. Click Publish

 

Fifo.fla

This file has the FuturaStd-BookFix font, which you should have already mapped. Follow the same instructions as above for the font embedding.

To create the .swf file:

  1. Go to File, Publish Settings
  2. Change the Output file to "..\Fifo.swf"
  3. Uncheck Include XMP metadata
  4. Click Publish

 

Downloads/Links

Official Distributors SecretUI Curse
Official Forum The Secret World Forums
Git Repository git://git.curseforge.net/tsw/cleannotifications/mainline.git
Source Archive Download