import elements.BaseGuiElement; import elements.GuiDraggable; import elements.GuiPanel; import elements.GuiTextbox; import elements.GuiText; import elements.GuiButton; import elements.GuiBackgroundPanel; import elements.GuiMarkupText; import elements.GuiResizeHandle; import elements.MarkupTooltip; import dialogs.InputDialog; import version; #include "dialogs/include/UniqueDialogs.as" #section gui from tabs.WikiTab import showWikiPage, openGeneralLink; void openWiki(const string& link, int button) { if(button == 0) showWikiPage(link, false); else if(button == 2) showWikiPage(link, true); } #section menu void openWiki(const string& link, int button) { if(button == 0) openBrowser(format("http://wiki.starruler2.com/$1", link.replaced(" ", "_"))); } void openGeneralLink(const string& link, int button = 0) { if(link.findFirst(URI_HINT) != -1) { if(button == 0) openBrowser(link); } else { openWiki(link, button); } } #section all const uint SENDER_WIDTH = 110; const string URI_HINT = "://"; const string LEFT_BRACKET = "["; const double FLASH_TIME = 1.0; const array NICK_COLORS = { Color(0x759ca6ff), Color(0x8675a6ff), Color(0xa87590ff), Color(0xa67577ff), Color(0x7da675ff), Color(0x75a685ff) }; enum Dialogs { D_ChangeNick, }; class ChangeNick : InputDialogCallback { void inputCallback(InputDialog@ dialog, bool accepted) { if(accepted) { string name = dialog.getTextInput(0); IRC.nickname = name; settings::sNickname = name; saveSettings(); } } }; class LinkableMarkupText : GuiMarkupText { LinkableMarkupText(IGuiElement@ parent, const recti& pos) { super(parent, pos); @tooltipObject = MarkupTooltip(400, 0.f, true, true); } void onLinkClicked(const string& link, int button) override { openGeneralLink(link, button); } }; bool isNickBold(const string& nick) { if(nick.length == 0) return false; return nick == "GGLucas" || nick == "ThyReaper" || nick == "Firgof" || nick == "*" || nick[0] == '@'; } Color getNickColor(const string& nick) { uint8 amt = 0; for(uint i = 0, cnt = nick.length; i < cnt; ++i) amt += uint8(nick[i]); return NICK_COLORS[amt % NICK_COLORS.length]; } uint missedMessages = 0; void updateHighlights(IRCChannel@ chan, const string& myNick) { Lock lock(chan.mutex); uint msgCount = chan.messageCount; bool isPM = chan.isPM; for(uint i = 0; i < msgCount; ++i) { IRCMessageType type = chan.message_types[i]; if(type == IMT_Action || type == IMT_Message) { uint id = chan.message_ids[i]; if(id > chan.handledId) { bool highlight = false; if(isPM) { highlight = true; } else { string msg = chan.messages[i]; highlight = msg.contains_nocase(myNick); } if(highlight) { sound::notify.play(priority=true); if(window is null || !window.visible) ircButton.flash(); if(window is null || window.activeChannel !is chan) chan.highlight = true; } if(window is null || !window.visible) ++missedMessages; chan.handledId = id; } } } } class ChannelDisplay : BaseGuiElement { array senders; array messages; array markups; array nicksBold; array isHighlight; array nickColors; string lastChan; uint prevId = 0; ChannelDisplay(IGuiElement@ parent) { super(parent, recti(0, 0, 20, 20)); updateAbsolutePosition(); } void updateAbsolutePosition() override { int w = parent.updatePosition.width; if(w >= 720) w -= 120; size = vec2i(w, max(20, size.height)); BaseGuiElement::updateAbsolutePosition(); } void update(IRCChannel@ chan) { chan.highlight = false; uint msgCount = chan.messageCount; if(chan.messageId != prevId || msgCount != messages.length || chan.name != lastChan) { lastChan = chan.name; messages.length = msgCount; senders.length = msgCount; nicksBold.length = msgCount; nickColors.length = msgCount; isHighlight.length = msgCount; prevId = chan.messageId; string myNick = IRC.nickname; uint prevMarkups = markups.length; for(uint i = msgCount; i < prevMarkups; ++i) { if(markups[i] !is null) { markups[i].remove(); @markups[i] = null; } } markups.length = msgCount; { Lock lock(chan.mutex); for(uint i = 0; i < msgCount; ++i) { IRCMessageType type = chan.message_types[i]; string sender = chan.message_senders[i]; uint8 sender_type = chan.message_sender_types[i]; uint id = chan.message_ids[i]; nickColors[i] = getNickColor(sender); string msg = chan.messages[i]; if(type == IMT_Action || type == IMT_Message) isHighlight[i] = msg.contains_nocase(myNick); else isHighlight[i] = false; bool needMarkup = msg.findFirst(LEFT_BRACKET) != -1; if(sender_type != '@' && sender_type != '+') { if(needMarkup) msg = bbescape(msg, allowWikiLinks=true); } switch(type) { case IMT_Join: senders[i] = "*"; msg = format(locale::IRC_JOIN, sender, chan.name, toString(nickColors[i])); needMarkup = true; break; case IMT_Part: senders[i] = "*"; msg = format(locale::IRC_PART, sender, chan.name, toString(nickColors[i])); needMarkup = true; break; case IMT_Quit: senders[i] = "*"; msg = format(locale::IRC_QUIT, sender, msg, toString(nickColors[i])); needMarkup = true; break; case IMT_Action: senders[i] = "*"; msg = format(locale::IRC_ACTION, sender, msg, toString(nickColors[i])); needMarkup = true; break; case IMT_Nick: senders[i] = "*"; msg = format(locale::IRC_NICK, sender, msg, toString(nickColors[i]), toString(getNickColor(msg))); needMarkup = true; break; case IMT_Kick: senders[i] = "*"; msg = format(locale::IRC_KICK, sender, msg, toString(nickColors[i]), toString(getNickColor(msg))); needMarkup = true; break; case IMT_Topic: senders[i] = "*"; msg = format(locale::IRC_TOPIC, sender, msg, toString(nickColors[i])); needMarkup = true; break; case IMT_Mode: senders[i] = "*"; msg = format(locale::IRC_MODE, sender, msg, toString(nickColors[i])); needMarkup = true; break; case IMT_UMode: senders[i] = "*"; msg = format(locale::IRC_UMODE, sender, msg, toString(nickColors[i])); needMarkup = true; break; case IMT_TopicIs: senders[i] = "*"; msg = format(locale::IRC_TOPIC_IS, chan.name, msg, toString(nickColors[i])); break; case IMT_Disconnect: senders[i] = "*"; msg = locale::IRC_DISCONNECT; needMarkup = true; break; default: { if(sender_type != ' ') { senders[i] = " "+sender; senders[i][0] = sender_type; } else { senders[i] = sender; } } break; } bool hasLinks = msg.findFirst(URI_HINT) != -1; if(hasLinks) { msg = makebbLinks(msg); needMarkup = true; } messages[i] = msg; nicksBold[i] = isNickBold(senders[i]); auto@ elem = markups[i]; if(needMarkup) { if(elem is null) { @elem = LinkableMarkupText(this, recti_area(0, 0, size.width-SENDER_WIDTH-27, 20)); @markups[i] = elem; } elem.text = msg; elem.updateAbsolutePosition(); } else { if(elem !is null) { elem.remove(); @markups[i] = null; } } } } } } void draw() override { const Font@ ft = skin.getFont(FT_Normal); const Font@ bold = skin.getFont(FT_Bold); recti leftPos = recti_area(AbsolutePosition.topLeft+vec2i(4, 6), vec2i(SENDER_WIDTH, 20)); recti rightPos = recti_area(AbsolutePosition.topLeft+vec2i(SENDER_WIDTH+13, 7), vec2i(size.width-SENDER_WIDTH-27, 20)); uint msgCount = messages.length; vec2i npos; for(uint i = 0; i < msgCount; ++i) { if(isHighlight[i]) drawRectangle(leftPos, Color(0xff888820)); if(nicksBold[i]) bold.draw(leftPos, senders[i], locale::ELLIPSIS, nickColors[i], 1.0, 0.5); else ft.draw(leftPos, senders[i], locale::ELLIPSIS, nickColors[i], 1.0, 0.5); auto@ elem = markups[i]; if(elem is null) { npos = ft.draw(rightPos, vec2i(), 20, messages[i], Color(0xffffffff), true); } else { vec2i markupSize = elem.size; elem.size = vec2i(size.width-SENDER_WIDTH-27, markupSize.y); elem.position = rightPos.topLeft - AbsolutePosition.topLeft - vec2i(0, 1); npos = vec2i(rightPos.topLeft.x, rightPos.topLeft.y+markupSize.y); } int ydiff = npos.y - rightPos.topLeft.y; if(npos.x > rightPos.topLeft.x) ydiff += 20; leftPos += vec2i(0, ydiff); rightPos += vec2i(0, ydiff); } int needHeight = max(rightPos.topLeft.y - AbsolutePosition.topLeft.y, 20); if(size.height != needHeight) { size = vec2i(parent.updatePosition.width, needHeight); //Hax GuiPanel@ par = cast(parent); bool wasBottom = par.vert.pos >= (par.vert.end - par.vert.page); par.updateAbsolutePosition(); if(wasBottom) { par.vert.pos = max(0.0, par.vert.end - par.vert.page); par.updateAbsolutePosition(); } } BaseGuiElement::draw(); } }; class ChanButton : GuiButton { IRCChannel@ channel; ChanButton(IGuiElement@ parent) { super(parent, recti()); } }; class IRCInput : GuiTextbox { IRCWindow@ window; IRCInput(IRCWindow@ win, Alignment@ align) { @window = win; super(win, align); } bool onKeyEvent(const KeyboardEvent& event, IGuiElement@ source) { if(event.type == KET_Key_Down) { if(event.key == KEY_TAB) { return true; } } else if(event.type == KET_Key_Up) { if(event.key == KEY_TAB) { tabComplete(); return true; } } return GuiTextbox::onKeyEvent(event, source); } void tabComplete() { if(Text.length == 0) return; //Get word before cursor string before; IRCChannel@ chan = window.activeChannel; int pos = curs-1; for(; pos > 0; --pos) { if(Text[pos] == ' ') { ++pos; break; } } if(pos >= 0 && pos < curs) before = Text.substr(pos, curs-pos); else if(pos > 0) return; //Find nickname in opposite direction of use. string found; for(int i = window.disp.senders.length - 1; i >= 0; --i) { string compare = window.disp.senders[i].substr(1); if((before.length == 0 || compare.startswith_nocase(before)) && compare.length > 1) { found = compare; break; } } //No nickname found, try user list if(found.length == 0) { for(uint i = 0, cnt = window.activeUsers.length; i < cnt; ++i) { string compare = window.activeUsers[i].substr(1); if(compare.startswith_nocase(before)) { found = compare; break; } } } //Append to textbox if(found.length != 0) { if(pos > 0) Text = Text.substr(0, pos); else Text = ""; Text += found; if(pos <= 0) Text += ": "; else Text += " "; curs = Text.length; } } void remove() { @window = null; GuiTextbox::remove(); } } class IRCWindow : GuiDraggable { GuiBackgroundPanel@ bg; GuiPanel@ panel; ChannelDisplay@ disp; GuiTextbox@ messageBox; GuiButton@ nickButton; GuiButton@ closeButton; GuiResizeHandle@ handle; BaseGuiElement@ chanList; array chanButtons; IRCChannel@ activeChannel; array activeUsers; array userMarks; array usersCut; array userColors; array usersBold; GuiSkinElement@ userBG; GuiPanel@ userPanel; BaseGuiElement@ userList; string prevUsers; IRCWindow() { super(null, IRC.display); visible = IRC.display.width != 0; @bg = GuiBackgroundPanel(this, Alignment_Fill()); bg.titleColor = Color(0xb3fe00ff); @handle = GuiResizeHandle(this, Alignment(Right-12, Bottom-12, Right, Bottom)); handle.minSize = vec2i(450, 200); @nickButton = GuiButton(bg, Alignment(Right-185, Top+3, Right-33, Top+28)); nickButton.visible = false; @closeButton = GuiButton(bg, Alignment(Right-31, Top+3, Right-5, Top+28), "X"); @panel = GuiPanel(this, Alignment(Left+10, Top+32, Right-10, Bottom-44)); @chanList = BaseGuiElement(this, Alignment(Left+10, Bottom-70, Right-10, Bottom-44)); chanList.visible = false; @disp = ChannelDisplay(panel); @messageBox = IRCInput(this, Alignment(Left+10, Bottom-40, Right-10, Bottom-10)); @userBG = GuiSkinElement(this, Alignment(Right-150, Top+34, Right-10, Bottom-44), SS_LightPanel); @userPanel = GuiPanel(userBG, Alignment().fill()); userPanel.horizType = ST_Never; @userList = BaseGuiElement(userPanel, recti(0, 0, 140, 20)); userBG.visible = false; updateAbsolutePosition(); } void remove() override { GuiDraggable::remove(); } void updateAbsolutePosition() { if(panel !is null) { bool showUsers = size.width >= 750 && (activeChannel !is null && !activeChannel.isPM); if(showUsers) { panel.alignment.right.pixels = 160; userBG.visible = true; } else { panel.alignment.right.pixels = 10; userBG.visible = false; } } GuiDraggable::updateAbsolutePosition(); } void updateUsers() { if(activeChannel is null) return; activeChannel.getUsers(activeUsers); activeUsers.sortDesc(); uint userCnt = activeUsers.length; usersCut.length = userCnt; userMarks.length = userCnt; usersBold.length = userCnt; userColors.length = userCnt; for(uint i = 0; i < userCnt; ++i) { usersCut[i] = activeUsers[i].substr(1); userMarks[i] = activeUsers[i].substr(0, 1); usersBold[i] = isNickBold(activeUsers[i]); userColors[i] = getNickColor(usersCut[i]); } int h = userCnt * 24; if(h != userList.size.height) { userList.size = vec2i(140, h); userPanel.updateAbsolutePosition(); } } void draw() { GuiDraggable::draw(); if(userBG.visible) { const Font@ font = skin.getFont(FT_Normal); vec2i pos = userList.AbsolutePosition.topLeft + vec2i(6, 6); int start = userBG.absolutePosition.topLeft.y; int end = userBG.absolutePosition.botRight.y; setClip(userBG.absoluteClipRect); int w = 130; if(userPanel.vert.visible) w -= 20; for(uint i = 0, cnt = activeUsers.length; i < cnt; ++i) { if(pos.y < start - 20 || pos.y > end) { pos.y += 20; continue; } Color col = userColors[i]; const Font@ ft = font; if(usersBold[i] && ft.bold !is null) @ft = ft.bold; ft.draw(pos=recti_area(pos, vec2i(14, 20)), text=userMarks[i], color=col, ellipsis=locale::SHORT_ELLIPSIS, horizAlign=1.0); ft.draw(pos=recti_area(pos+vec2i(15,0), vec2i(w-15, 20)), text=usersCut[i], ellipsis=locale::ELLIPSIS, color=col); pos.y += 20; } } } void update(double time) { if(visible) { IRC.display = rect; prevSize = rect; missedMessages = 0; } else { IRC.display = recti(); } if(!IRC.connected) return; if(IRC.channelCount == 0) { IRC.join("#starruler"); return; } if(activeChannel is null || activeChannel.closed) setActiveChannel(IRC.channels[0]); Lock lock(IRC.mutex); if(visible) { //Update window elements if(activeChannel !is null) bg.title = activeChannel.topic; nickButton.text = format(locale::IRC_NICKNAME, IRC.nickname); nickButton.visible = true; //Update the active channel if(activeChannel.messageId != disp.prevId || activeChannel.getUserCount() != activeUsers.length) updateUsers(); disp.update(activeChannel); //Update the channel list buttons uint chanCnt = IRC.channelCount; if(chanCnt > 1) { chanList.visible = true; panel.alignment.bottom.pixels = 74; uint oldCnt = chanButtons.length; uint newCnt = chanCnt; for(uint i = newCnt; i < oldCnt; ++i) chanButtons[i].remove(); chanButtons.length = newCnt; for(uint i = oldCnt; i < newCnt; ++i) { @chanButtons[i] = ChanButton(chanList); chanButtons[i].toggleButton = true; } int x = 0; int h = chanList.size.height; int w = chanList.size.width; if(userBG.visible) w -= 150; w = min(160, w / chanCnt); for(uint i = 0; i < chanCnt; ++i) { IRCChannel@ chan = IRC.channels[i]; @chanButtons[i].channel = chan; chanButtons[i].text = chan.name; chanButtons[i].pressed = chan is activeChannel; if(chan.highlight) chanButtons[i].color = Color(0xff8000ff); else chanButtons[i].color = Color(0xffffffff); chanButtons[i].size = vec2i(w, h); chanButtons[i].position = vec2i(x, 0); x += w; } } else { chanList.visible = false; panel.alignment.bottom.pixels = 44; uint oldCnt = chanButtons.length; for(uint i = 0; i < oldCnt; ++i) chanButtons[i].remove(); chanButtons.length = 0; } } //Update highlights { string myNick = IRC.nickname; uint chanCnt = IRC.channelCount; for(uint i = 0; i < chanCnt; ++i) updateHighlights(IRC.channels[i], myNick); } } void promptNickChange() { if(focusDialog(D_ChangeNick)) return; InputDialog@ dialog = InputDialog(ChangeNick(), this); dialog.accept.text = locale::CHANGE_NICK; dialog.addTextInput(locale::NICKNAME, IRC.nickname); addDialog(D_ChangeNick, dialog); dialog.focusInput(); } bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) { switch(event.type) { case MET_Button_Up: if(handle.dragging) handle.dragging = false; setGuiFocus(messageBox); break; } return GuiDraggable::onMouseEvent(event, source); } void setActiveChannel(IRCChannel@ chan) { @activeChannel = chan; bool showUsers = size.width >= 750 && (chan !is null && !chan.isPM); if(showUsers != userBG.visible) updateAbsolutePosition(); } bool onGuiEvent(const GuiEvent& evt) override { if(evt.caller is messageBox) { if(evt.type == GUI_Confirmed) { if(activeChannel is null) return true; IRC.send(activeChannel, messageBox.text); messageBox.clear(); return true; } } else if(evt.caller is closeButton) { if(evt.type == GUI_Clicked) { visible = false; return true; } } else if(evt.caller is nickButton) { if(evt.type == GUI_Clicked) { promptNickChange(); return true; } } else if(evt.type == GUI_Clicked) { ChanButton@ but = cast(evt.caller); if(but !is null) setActiveChannel(but.channel); } return GuiDraggable::onGuiEvent(evt); } }; IRCWindow@ window; IRCButton@ ircButton; recti prevSize; void openIRC() { IRC.display = recti_centered(recti(vec2i(0, 0), screenSize), vec2i(750, 500)); if(window is null) @window = IRCWindow(); } void closeIRC() { if(window !is null) window.visible = false; IRC.display = recti(); } void showIRC() { if(!IRC.running) { IRC.nickname = settings::sNickname; IRC.connect(); openIRC(); } else { if(window is null) { openIRC(); } else { window.visible = !window.visible; if(!window.visible) { IRC.display = recti(); } else if(IRC.display.width == 0) { if(prevSize.width != 0) IRC.display = prevSize; else IRC.display = recti_centered(recti(vec2i(0, 0), screenSize), vec2i(750, 500)); window.rect = IRC.display; } } } } class IRCButtonEvent : onButtonClick { bool onClick(GuiButton@ btn) { ircButton.flashes = 0; IRC.highlight = false; if(window is null) { openIRC(); } else { window.visible = !window.visible; if(!window.visible) { IRC.display = recti(); } else if(IRC.display.width == 0) { if(prevSize.width != 0) IRC.display = prevSize; else IRC.display = recti_centered(recti(vec2i(0, 0), screenSize), vec2i(750, 500)); window.rect = IRC.display; } } return true; } }; class ChangeNickCommand : ConsoleCommand { void execute(const string& args) { string name = args; IRC.nickname = name; settings::sNickname = name; saveSettings(); } }; class IRCButton : GuiButton { int flashes = 0; double flashTime = 0; GuiText@ missed; IRCButton(Alignment@ align) { super(null, align); spriteStyle = Sprite(material::IRCButton); visible = false; @missed = GuiText(this, Alignment().padded(4)); missed.horizAlign = 1.0; missed.vertAlign = 0.6; missed.stroke = colors::Black; missed.color = Color(0xff8000ff); missed.font = FT_Bold; } void flash() { flashes += 3; IRC.highlight = true; } void draw() { //Show flashes if(flashTime > 0.0) { float pct = 0.f; if(flashTime < FLASH_TIME * 0.5) pct = 1.f - (FLASH_TIME * 0.5 - flashTime) / (FLASH_TIME * 0.5); else pct = 1.f - (flashTime - FLASH_TIME * 0.5) / (FLASH_TIME * 0.5); color = Color(0xffaaaaff).interpolate(Color(0xff8000ff), pct); flashTime -= frameLength; } else { if(flashes > 0) { --flashes; flashTime = FLASH_TIME; } if(IRC.highlight) color = Color(0xffaaaaff); else color = colors::White; } if(missedMessages != 0) { missed.text = toString(missedMessages); missed.visible = true; } else { missed.visible = false; } GuiButton::draw(); } }; void init() { #section game @ircButton = IRCButton(Alignment(Right-32, Top+20, Width=32, Height=64)); #section menu @ircButton = IRCButton(Alignment(Right-32, Top+100, Width=32, Height=64)); setNickname(settings::sNickname); addConsoleCommand("nick", ChangeNickCommand()); string ver = SCRIPT_VERSION; int pos = ver.findLast(" "); if(pos != -1) ver = ver.substr(pos+1); IRC_HOSTNAME = "SR2"+ArchName+ArchBits+ver; if(cloud::isActive) IRC_HOSTNAME += "S"; if(!IS_STEAM_BUILD) IRC_HOSTNAME += "G"; if(hasDLC("Heralds")) IRC_HOSTNAME += "H"; #section all @ircButton.onClick = IRCButtonEvent(); } void onGameStateChange() { if(window !is null) { if(IRC.display.width == 0) { window.visible = false; } else { window.rect = IRC.display; window.visible = true; } window.update(1.0); } } void tick(double time) { #section menu if(game_state != GS_Menu) return; #section gui if(game_state != GS_Game) return; #section all if(IRC.running) { if(!ircButton.visible) { ircButton.visible = true; if(window is null) @window = IRCWindow(); } ircButton.bringToFront(); } else { if(ircButton.visible) ircButton.visible = false; if(window !is null && window.visible) window.visible = false; } if(window !is null) window.update(time); }