mirror of
				https://github.com/ErikasKontenis/SabrehavenServer.git
				synced 2025-10-30 19:56:22 +01:00 
			
		
		
		
	commit client
This commit is contained in:
		
							
								
								
									
										125
									
								
								SabrehavenOTClient/modules/client/client.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								SabrehavenOTClient/modules/client/client.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| local musicFilename = "/sounds/startup" | ||||
| local musicChannel = nil | ||||
|  | ||||
| function setMusic(filename) | ||||
|   musicFilename = filename | ||||
|  | ||||
|   if not g_game.isOnline() and musicChannel ~= nil then | ||||
|     musicChannel:stop() | ||||
|     musicChannel:enqueue(musicFilename, 3) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function reloadScripts() | ||||
|   if g_game.getFeature(GameNoDebug) then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   g_textures.clearCache() | ||||
|   g_modules.reloadModules() | ||||
|  | ||||
|   local script = '/' .. g_app.getCompactName() .. 'rc.lua' | ||||
|   if g_resources.fileExists(script) then | ||||
|     dofile(script) | ||||
|   end | ||||
|  | ||||
|   local message = tr('All modules and scripts were reloaded.') | ||||
|  | ||||
|   modules.game_textmessage.displayGameMessage(message) | ||||
|   print(message) | ||||
| end | ||||
|  | ||||
| function startup() | ||||
|   if g_sounds ~= nil then | ||||
|     musicChannel = g_sounds.getChannel(1) | ||||
|   end | ||||
|    | ||||
|   G.UUID = g_settings.getString('report-uuid') | ||||
|   if not G.UUID or #G.UUID ~= 36 then | ||||
|     G.UUID = g_crypt.genUUID() | ||||
|     g_settings.set('report-uuid', G.UUID) | ||||
|   end | ||||
|    | ||||
|   -- Play startup music (The Silver Tree, by Mattias Westlund) | ||||
|   --musicChannel:enqueue(musicFilename, 3) | ||||
|   connect(g_game, { onGameStart = function() if musicChannel ~= nil then musicChannel:stop(3) end end }) | ||||
|   connect(g_game, { onGameEnd = function() | ||||
|       if g_sounds ~= nil then | ||||
|         g_sounds.stopAll() | ||||
|         --musicChannel:enqueue(musicFilename, 3) | ||||
|       end | ||||
|   end }) | ||||
| end | ||||
|  | ||||
| function init() | ||||
|   connect(g_app, { onRun = startup, | ||||
|                    onExit = exit }) | ||||
|   connect(g_game, { onGameStart = onGameStart, | ||||
|                     onGameEnd = onGameEnd }) | ||||
|  | ||||
|   if g_sounds ~= nil then | ||||
|     --g_sounds.preload(musicFilename) | ||||
|   end | ||||
|  | ||||
|   if not Updater then | ||||
|     if g_resources.getLayout() == "mobile" then | ||||
|       g_window.setMinimumSize({ width = 640, height = 360 }) | ||||
|     else | ||||
|       g_window.setMinimumSize({ width = 800, height = 640 })   | ||||
|     end | ||||
|  | ||||
|     -- window size | ||||
|     local size = { width = 1024, height = 600 } | ||||
|     size = g_settings.getSize('window-size', size) | ||||
|     g_window.resize(size) | ||||
|  | ||||
|     -- window position, default is the screen center | ||||
|     local displaySize = g_window.getDisplaySize() | ||||
|     local defaultPos = { x = (displaySize.width - size.width)/2, | ||||
|                          y = (displaySize.height - size.height)/2 } | ||||
|     local pos = g_settings.getPoint('window-pos', defaultPos) | ||||
|     pos.x = math.max(pos.x, 0) | ||||
|     pos.y = math.max(pos.y, 0) | ||||
|     g_window.move(pos) | ||||
|  | ||||
|     -- window maximized? | ||||
|     local maximized = g_settings.getBoolean('window-maximized', false) | ||||
|     if maximized then g_window.maximize() end | ||||
|   end | ||||
|  | ||||
|   g_window.setTitle(g_app.getName()) | ||||
|   g_window.setIcon('/images/clienticon') | ||||
|  | ||||
|   g_keyboard.bindKeyDown('Ctrl+Shift+R', reloadScripts) | ||||
|  | ||||
|   -- generate machine uuid, this is a security measure for storing passwords | ||||
|   if not g_crypt.setMachineUUID(g_settings.get('uuid')) then | ||||
|     g_settings.set('uuid', g_crypt.getMachineUUID()) | ||||
|     g_settings.save() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   disconnect(g_app, { onRun = startup, | ||||
|                       onExit = exit }) | ||||
|   disconnect(g_game, { onGameStart = onGameStart, | ||||
|                        onGameEnd = onGameEnd }) | ||||
|   -- save window configs | ||||
|   g_settings.set('window-size', g_window.getUnmaximizedSize()) | ||||
|   g_settings.set('window-pos', g_window.getUnmaximizedPos()) | ||||
|   g_settings.set('window-maximized', g_window.isMaximized()) | ||||
| end | ||||
|  | ||||
| function exit() | ||||
|   g_logger.info("Exiting application..") | ||||
| end | ||||
|  | ||||
| function onGameStart() | ||||
|   local player = g_game.getLocalPlayer() | ||||
|   if not player then return end | ||||
|   g_window.setTitle(g_app.getName() .. " - " .. player:getName())   | ||||
| end | ||||
|  | ||||
| function onGameEnd() | ||||
|   g_window.setTitle(g_app.getName()) | ||||
| end | ||||
							
								
								
									
										23
									
								
								SabrehavenOTClient/modules/client/client.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								SabrehavenOTClient/modules/client/client.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| Module | ||||
|   name: client | ||||
|   description: Initialize the client and setups its main window | ||||
|   author: edubart | ||||
|   website: https://github.com/edubart/otclient | ||||
|   reloadable: false | ||||
|   sandboxed: true | ||||
|   scripts: [ client ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
|  | ||||
|   load-later: | ||||
|     - client_styles | ||||
|     - client_locales | ||||
|     - client_topmenu | ||||
|     - client_background | ||||
|     - client_textedit | ||||
|     - client_options | ||||
|     - client_entergame | ||||
|     - client_terminal | ||||
|     - client_stats | ||||
|     - client_feedback | ||||
|     - client_mobile | ||||
							
								
								
									
										49
									
								
								SabrehavenOTClient/modules/client_background/background.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								SabrehavenOTClient/modules/client_background/background.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| -- private variables | ||||
| local background | ||||
| local clientVersionLabel | ||||
|  | ||||
| -- public functions | ||||
| function init() | ||||
|   background = g_ui.displayUI('background') | ||||
|   background:lower() | ||||
|  | ||||
|   clientVersionLabel = background:getChildById('clientVersionLabel') | ||||
|   clientVersionLabel:setText('OTClientV8 ' .. g_app.getVersion() .. '\nrev ' .. g_app.getBuildRevision() .. '\nMade by:\n' .. g_app.getAuthor() .. "") | ||||
|    | ||||
|   if not g_game.isOnline() then | ||||
|     addEvent(function() g_effects.fadeIn(clientVersionLabel, 1500) end) | ||||
|   end | ||||
|  | ||||
|   connect(g_game, { onGameStart = hide }) | ||||
|   connect(g_game, { onGameEnd = show }) | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   disconnect(g_game, { onGameStart = hide }) | ||||
|   disconnect(g_game, { onGameEnd = show }) | ||||
|  | ||||
|   g_effects.cancelFade(background:getChildById('clientVersionLabel')) | ||||
|   background:destroy() | ||||
|  | ||||
|   Background = nil | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   background:hide() | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   background:show() | ||||
| end | ||||
|  | ||||
| function hideVersionLabel() | ||||
|   background:getChildById('clientVersionLabel'):hide() | ||||
| end | ||||
|  | ||||
| function setVersionText(text) | ||||
|   clientVersionLabel:setText(text) | ||||
| end | ||||
|  | ||||
| function getBackground() | ||||
|   return background | ||||
| end | ||||
| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: client_background | ||||
|   description: Handles the background of the login screen | ||||
|   author: edubart | ||||
|   website: https://github.com/edubart/otclient | ||||
|   sandboxed: true | ||||
|   scripts: [ background ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										21
									
								
								SabrehavenOTClient/modules/client_background/background.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SabrehavenOTClient/modules/client_background/background.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| UIWidget | ||||
|   id: background | ||||
|   anchors.fill: parent | ||||
|   focusable: false | ||||
|   image-source: /images/background | ||||
|   image-smooth: true | ||||
|   image-fixed-ratio: true | ||||
|   margin-top: 1 | ||||
|  | ||||
|   UILabel | ||||
|     id: clientVersionLabel | ||||
|     background-color: #00000099 | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: parent.bottom | ||||
|     text-align: center | ||||
|     text-auto-resize: false | ||||
|     width: 220 | ||||
|     height: 90 | ||||
|     padding: 2 | ||||
|     color: #ffffff | ||||
|     font: terminus-14px-bold | ||||
							
								
								
									
										422
									
								
								SabrehavenOTClient/modules/client_entergame/characterlist.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										422
									
								
								SabrehavenOTClient/modules/client_entergame/characterlist.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,422 @@ | ||||
| CharacterList = { } | ||||
|  | ||||
| -- private variables | ||||
| local charactersWindow | ||||
| local loadBox | ||||
| local characterList | ||||
| local errorBox | ||||
| local waitingWindow | ||||
| local autoReconnectButton | ||||
| local updateWaitEvent | ||||
| local resendWaitEvent | ||||
| local loginEvent | ||||
| local autoReconnectEvent | ||||
| local lastLogout = 0 | ||||
|  | ||||
| -- private functions | ||||
| local function tryLogin(charInfo, tries) | ||||
|   tries = tries or 1 | ||||
|  | ||||
|   if tries > 50 then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   if g_game.isOnline() then | ||||
|     if tries == 1 then | ||||
|       g_game.safeLogout() | ||||
|     end | ||||
|     loginEvent = scheduleEvent(function() tryLogin(charInfo, tries+1) end, 100) | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   CharacterList.hide() | ||||
|   g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName, G.authenticatorToken, G.sessionKey) | ||||
|   g_logger.info("Login to " .. charInfo.worldHost .. ":" .. charInfo.worldPort) | ||||
|   loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...')) | ||||
|   connect(loadBox, { onCancel = function() | ||||
|                                   loadBox = nil | ||||
|                                   g_game.cancelLogin() | ||||
|                                   CharacterList.show() | ||||
|                                 end }) | ||||
|  | ||||
|   -- save last used character | ||||
|   g_settings.set('last-used-character', charInfo.characterName) | ||||
|   g_settings.set('last-used-world', charInfo.worldName) | ||||
| end | ||||
|  | ||||
| local function updateWait(timeStart, timeEnd) | ||||
|   if waitingWindow then | ||||
|     local time = g_clock.seconds() | ||||
|     if time <= timeEnd then | ||||
|       local percent = ((time - timeStart) / (timeEnd - timeStart)) * 100 | ||||
|       local timeStr = string.format("%.0f", timeEnd - time) | ||||
|  | ||||
|       local progressBar = waitingWindow:getChildById('progressBar') | ||||
|       progressBar:setPercent(percent) | ||||
|  | ||||
|       local label = waitingWindow:getChildById('timeLabel') | ||||
|       label:setText(tr('Trying to reconnect in %s seconds.', timeStr)) | ||||
|  | ||||
|       updateWaitEvent = scheduleEvent(function() updateWait(timeStart, timeEnd) end, 1000 * progressBar:getPercentPixels() / 100 * (timeEnd - timeStart)) | ||||
|       return true | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if updateWaitEvent then | ||||
|     updateWaitEvent:cancel() | ||||
|     updateWaitEvent = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function resendWait() | ||||
|   if waitingWindow then | ||||
|     waitingWindow:destroy() | ||||
|     waitingWindow = nil | ||||
|  | ||||
|     if updateWaitEvent then | ||||
|       updateWaitEvent:cancel() | ||||
|       updateWaitEvent = nil | ||||
|     end | ||||
|  | ||||
|     if charactersWindow then | ||||
|       local selected = characterList:getFocusedChild() | ||||
|       if selected then | ||||
|         local charInfo = { worldHost = selected.worldHost, | ||||
|                            worldPort = selected.worldPort, | ||||
|                            worldName = selected.worldName, | ||||
|                            characterName = selected.characterName } | ||||
|         tryLogin(charInfo) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function onLoginWait(message, time) | ||||
|   CharacterList.destroyLoadBox() | ||||
|  | ||||
|   waitingWindow = g_ui.displayUI('waitinglist') | ||||
|  | ||||
|   local label = waitingWindow:getChildById('infoLabel') | ||||
|   label:setText(message) | ||||
|  | ||||
|   updateWaitEvent = scheduleEvent(function() updateWait(g_clock.seconds(), g_clock.seconds() + time) end, 0) | ||||
|   resendWaitEvent = scheduleEvent(resendWait, time * 1000) | ||||
| end | ||||
|  | ||||
| function onGameLoginError(message) | ||||
|   CharacterList.destroyLoadBox() | ||||
|   errorBox = displayErrorBox(tr("Login Error"), message) | ||||
|   errorBox.onOk = function() | ||||
|     errorBox = nil | ||||
|     CharacterList.showAgain() | ||||
|   end | ||||
|   scheduleAutoReconnect() | ||||
| end | ||||
|  | ||||
| function onGameLoginToken(unknown) | ||||
|   CharacterList.destroyLoadBox() | ||||
|   -- TODO: make it possible to enter a new token here / prompt token | ||||
|   errorBox = displayErrorBox(tr("Two-Factor Authentification"), 'A new authentification token is required.\nPlease login again.') | ||||
|   errorBox.onOk = function() | ||||
|     errorBox = nil | ||||
|     EnterGame.show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function onGameConnectionError(message, code) | ||||
|   CharacterList.destroyLoadBox() | ||||
|   if (not g_game.isOnline() or code ~= 2) and not errorBox then -- code 2 is normal disconnect, end of file | ||||
|     local text = translateNetworkError(code, g_game.getProtocolGame() and g_game.getProtocolGame():isConnecting(), message) | ||||
|     errorBox = displayErrorBox(tr("Connection Error"), text) | ||||
|     errorBox.onOk = function() | ||||
|       errorBox = nil | ||||
|       CharacterList.showAgain() | ||||
|     end | ||||
|   end | ||||
|   scheduleAutoReconnect() | ||||
| end | ||||
|  | ||||
| function onGameUpdateNeeded(signature) | ||||
|   CharacterList.destroyLoadBox() | ||||
|   errorBox = displayErrorBox(tr("Update needed"), tr('Enter with your account again to update your client.')) | ||||
|   errorBox.onOk = function() | ||||
|     errorBox = nil | ||||
|     CharacterList.showAgain() | ||||
|   end   | ||||
| end | ||||
|  | ||||
| function onGameEnd() | ||||
|   scheduleAutoReconnect() | ||||
|   CharacterList.showAgain() | ||||
| end | ||||
|  | ||||
| function onLogout() | ||||
|   lastLogout = g_clock.millis() | ||||
| end | ||||
|  | ||||
| function scheduleAutoReconnect() | ||||
|   if lastLogout + 2000 > g_clock.millis() then | ||||
|     return | ||||
|   end | ||||
|   if autoReconnectEvent then | ||||
|     removeEvent(autoReconnectEvent)     | ||||
|   end | ||||
|   autoReconnectEvent = scheduleEvent(executeAutoReconnect, 2500) | ||||
| end | ||||
|  | ||||
| function executeAutoReconnect()   | ||||
|   if not autoReconnectButton or not autoReconnectButton:isOn() or g_game.isOnline() then | ||||
|     return | ||||
|   end | ||||
|   if errorBox then | ||||
|     errorBox:destroy() | ||||
|     errorBox = nil | ||||
|   end | ||||
|   CharacterList.doLogin() | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function CharacterList.init() | ||||
|   if USE_NEW_ENERGAME then return end | ||||
|   connect(g_game, { onLoginError = onGameLoginError }) | ||||
|   connect(g_game, { onLoginToken = onGameLoginToken }) | ||||
|   connect(g_game, { onUpdateNeeded = onGameUpdateNeeded }) | ||||
|   connect(g_game, { onConnectionError = onGameConnectionError }) | ||||
|   connect(g_game, { onGameStart = CharacterList.destroyLoadBox }) | ||||
|   connect(g_game, { onLoginWait = onLoginWait }) | ||||
|   connect(g_game, { onGameEnd = onGameEnd }) | ||||
|   connect(g_game, { onLogout = onLogout }) | ||||
|  | ||||
|   if G.characters then | ||||
|     CharacterList.create(G.characters, G.characterAccount) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function CharacterList.terminate() | ||||
|  if USE_NEW_ENERGAME then return end | ||||
|   disconnect(g_game, { onLoginError = onGameLoginError }) | ||||
|   disconnect(g_game, { onLoginToken = onGameLoginToken }) | ||||
|   disconnect(g_game, { onUpdateNeeded = onGameUpdateNeeded }) | ||||
|   disconnect(g_game, { onConnectionError = onGameConnectionError }) | ||||
|   disconnect(g_game, { onGameStart = CharacterList.destroyLoadBox }) | ||||
|   disconnect(g_game, { onLoginWait = onLoginWait }) | ||||
|   disconnect(g_game, { onGameEnd = onGameEnd }) | ||||
|   disconnect(g_game, { onLogout = onLogout }) | ||||
|  | ||||
|   if charactersWindow then | ||||
|     characterList = nil | ||||
|     charactersWindow:destroy() | ||||
|     charactersWindow = nil | ||||
|   end | ||||
|  | ||||
|   if loadBox then | ||||
|     g_game.cancelLogin() | ||||
|     loadBox:destroy() | ||||
|     loadBox = nil | ||||
|   end | ||||
|  | ||||
|   if waitingWindow then | ||||
|     waitingWindow:destroy() | ||||
|     waitingWindow = nil | ||||
|   end | ||||
|  | ||||
|   if updateWaitEvent then | ||||
|     removeEvent(updateWaitEvent) | ||||
|     updateWaitEvent = nil | ||||
|   end | ||||
|  | ||||
|   if resendWaitEvent then | ||||
|     removeEvent(resendWaitEvent) | ||||
|     resendWaitEvent = nil | ||||
|   end | ||||
|  | ||||
|   if loginEvent then | ||||
|     removeEvent(loginEvent) | ||||
|     loginEvent = nil | ||||
|   end | ||||
|  | ||||
|   CharacterList = nil | ||||
| end | ||||
|  | ||||
| function CharacterList.create(characters, account, otui) | ||||
|   if not otui then otui = 'characterlist' end | ||||
|   if charactersWindow then | ||||
|     charactersWindow:destroy() | ||||
|   end | ||||
|  | ||||
|   charactersWindow = g_ui.displayUI(otui) | ||||
|   characterList = charactersWindow:getChildById('characters') | ||||
|   autoReconnectButton = charactersWindow:getChildById('autoReconnect') | ||||
|  | ||||
|   -- characters | ||||
|   G.characters = characters | ||||
|   G.characterAccount = account | ||||
|  | ||||
|   characterList:destroyChildren() | ||||
|   local accountStatusLabel = charactersWindow:getChildById('accountStatusLabel') | ||||
|   local focusLabel | ||||
|   for i,characterInfo in ipairs(characters) do | ||||
|     local widget = g_ui.createWidget('CharacterWidget', characterList) | ||||
|     for key,value in pairs(characterInfo) do | ||||
|       local subWidget = widget:getChildById(key) | ||||
|       if subWidget then | ||||
|         if key == 'outfit' then -- it's an exception | ||||
|           subWidget:setOutfit(value) | ||||
|         else | ||||
|           local text = value | ||||
|           if subWidget.baseText and subWidget.baseTranslate then | ||||
|             text = tr(subWidget.baseText, text) | ||||
|           elseif subWidget.baseText then | ||||
|             text = string.format(subWidget.baseText, text) | ||||
|           end | ||||
|           subWidget:setText(text) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     -- these are used by login | ||||
|     widget.characterName = characterInfo.name | ||||
|     widget.worldName = characterInfo.worldName | ||||
|     widget.worldHost = characterInfo.worldIp | ||||
|     widget.worldPort = characterInfo.worldPort | ||||
|  | ||||
|     connect(widget, { onDoubleClick = function () CharacterList.doLogin() return true end } ) | ||||
|  | ||||
|     if i == 1 or (g_settings.get('last-used-character') == widget.characterName and g_settings.get('last-used-world') == widget.worldName) then | ||||
|       focusLabel = widget | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if focusLabel then | ||||
|     characterList:focusChild(focusLabel, KeyboardFocusReason) | ||||
|     addEvent(function() characterList:ensureChildVisible(focusLabel) end) | ||||
|   end | ||||
|    | ||||
|   characterList.onChildFocusChange = function() | ||||
|     removeEvent(autoReconnectEvent) | ||||
|     autoReconnectEvent = nil | ||||
|   end | ||||
|  | ||||
|   -- account | ||||
|   local status = '' | ||||
|   if account.status == AccountStatus.Frozen then | ||||
|     status = tr(' (Frozen)') | ||||
|   elseif account.status == AccountStatus.Suspended then | ||||
|     status = tr(' (Suspended)') | ||||
|   end | ||||
|  | ||||
|   if account.subStatus == SubscriptionStatus.Free and account.premDays < 1 then | ||||
|     accountStatusLabel:setText(('%s%s'):format(tr('Free Account'), status)) | ||||
|   else | ||||
|     if account.premDays == 0 or account.premDays == 65535 then | ||||
|       accountStatusLabel:setText(('%s%s'):format(tr('Gratis Premium Account'), status)) | ||||
|     else | ||||
|       accountStatusLabel:setText(('%s%s'):format(tr('Premium Account (%s) days left', account.premDays), status)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if account.premDays > 0 and account.premDays <= 7 then | ||||
|     accountStatusLabel:setOn(true) | ||||
|   else | ||||
|     accountStatusLabel:setOn(false) | ||||
|   end | ||||
|    | ||||
|   autoReconnectButton.onClick = function(widget) | ||||
|     local autoReconnect = not g_settings.getBoolean('autoReconnect', true) | ||||
|     autoReconnectButton:setOn(autoReconnect) | ||||
|     g_settings.set('autoReconnect', autoReconnect) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function CharacterList.destroy() | ||||
|   CharacterList.hide(true) | ||||
|  | ||||
|   if charactersWindow then | ||||
|     characterList = nil | ||||
|     charactersWindow:destroy() | ||||
|     charactersWindow = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| function CharacterList.show() | ||||
|   if loadBox or errorBox or not charactersWindow then return end | ||||
|   charactersWindow:show() | ||||
|   charactersWindow:raise() | ||||
|   charactersWindow:focus() | ||||
|    | ||||
|   local autoReconnect = g_settings.getBoolean('autoReconnect', true) | ||||
|   autoReconnectButton:setOn(autoReconnect) | ||||
| end | ||||
|  | ||||
| function CharacterList.hide(showLogin) | ||||
|   removeEvent(autoReconnectEvent) | ||||
|   autoReconnectEvent = nil | ||||
|  | ||||
|   showLogin = showLogin or false | ||||
|   charactersWindow:hide() | ||||
|  | ||||
|   if showLogin and EnterGame and not g_game.isOnline() then | ||||
|     EnterGame.show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function CharacterList.showAgain() | ||||
|   if characterList and characterList:hasChildren() then | ||||
|     CharacterList.show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function CharacterList.isVisible() | ||||
|   if charactersWindow and charactersWindow:isVisible() then | ||||
|     return true | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function CharacterList.doLogin() | ||||
|   removeEvent(autoReconnectEvent) | ||||
|   autoReconnectEvent = nil | ||||
|  | ||||
|   local selected = characterList:getFocusedChild() | ||||
|   if selected then | ||||
|     local charInfo = { worldHost = selected.worldHost, | ||||
|                        worldPort = selected.worldPort, | ||||
|                        worldName = selected.worldName, | ||||
|                        characterName = selected.characterName } | ||||
|     charactersWindow:hide() | ||||
|     if loginEvent then | ||||
|       removeEvent(loginEvent) | ||||
|       loginEvent = nil | ||||
|     end | ||||
|     tryLogin(charInfo) | ||||
|   else | ||||
|     displayErrorBox(tr('Error'), tr('You must select a character to login!')) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function CharacterList.destroyLoadBox() | ||||
|   if loadBox then | ||||
|     loadBox:destroy() | ||||
|     loadBox = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| function CharacterList.cancelWait() | ||||
|   if waitingWindow then | ||||
|     waitingWindow:destroy() | ||||
|     waitingWindow = nil | ||||
|   end | ||||
|  | ||||
|   if updateWaitEvent then | ||||
|     removeEvent(updateWaitEvent) | ||||
|     updateWaitEvent = nil | ||||
|   end | ||||
|  | ||||
|   if resendWaitEvent then | ||||
|     removeEvent(resendWaitEvent) | ||||
|     resendWaitEvent = nil | ||||
|   end | ||||
|  | ||||
|   CharacterList.destroyLoadBox() | ||||
|   CharacterList.showAgain() | ||||
| end | ||||
							
								
								
									
										130
									
								
								SabrehavenOTClient/modules/client_entergame/characterlist.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								SabrehavenOTClient/modules/client_entergame/characterlist.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| CharacterWidget < UIWidget | ||||
|   height: 14 | ||||
|   background-color: alpha | ||||
|   &updateOnStates: | | ||||
|     function(self) | ||||
|       local children = self:getChildren() | ||||
|       for i=1,#children do | ||||
|         children[i]:setOn(self:isFocused()) | ||||
|       end | ||||
|     end | ||||
|   @onFocusChange: self:updateOnStates() | ||||
|   @onSetup: self:updateOnStates() | ||||
|  | ||||
|   $focus: | ||||
|     background-color: #ffffff22 | ||||
|  | ||||
|   Label | ||||
|     id: name | ||||
|     color: #bbbbbb | ||||
|     anchors.top: parent.top | ||||
|     anchors.left: parent.left | ||||
|     font: verdana-11px-monochrome | ||||
|     text-auto-resize: true | ||||
|     background-color: alpha | ||||
|     text-offset: 2 0 | ||||
|  | ||||
|     $on: | ||||
|       color: #ffffff | ||||
|  | ||||
|   Label | ||||
|     id: worldName | ||||
|     color: #bbbbbb | ||||
|     anchors.top: parent.top | ||||
|     anchors.right: parent.right | ||||
|     margin-right: 5 | ||||
|     font: verdana-11px-monochrome | ||||
|     text-auto-resize: true | ||||
|     background-color: alpha | ||||
|     &baseText: '(%s)' | ||||
|  | ||||
|     $on: | ||||
|       color: #ffffff | ||||
|  | ||||
| StaticMainWindow | ||||
|   id: charactersWindow | ||||
|   !text: tr('Character List') | ||||
|   visible: false | ||||
|   size: 350 400 | ||||
|   $mobile: | ||||
|     size: 350 280 | ||||
|   @onEnter: CharacterList.doLogin() | ||||
|   @onEscape: CharacterList.hide(true) | ||||
|   @onSetup: | | ||||
|     g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self) | ||||
|     g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self)   | ||||
|  | ||||
|   TextList | ||||
|     id: characters | ||||
|     background-color: #565656 | ||||
|     anchors.top: parent.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: characterListScrollBar.left | ||||
|     anchors.bottom: accountStatusCaption.top | ||||
|     margin-bottom: 5 | ||||
|     padding: 1 | ||||
|     focusable: false | ||||
|     vertical-scrollbar: characterListScrollBar | ||||
|     auto-focus: first | ||||
|  | ||||
|   VerticalScrollBar | ||||
|     id: characterListScrollBar | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: accountStatusCaption.top | ||||
|     anchors.right: parent.right | ||||
|     margin-bottom: 5 | ||||
|     step: 14 | ||||
|     pixels-scroll: true | ||||
|  | ||||
|   Label | ||||
|     id: accountStatusCaption | ||||
|     !text: tr('Account Status') .. ':' | ||||
|     anchors.left: parent.left | ||||
|     anchors.bottom: separator.top | ||||
|     margin-bottom: 5 | ||||
|  | ||||
|   Label | ||||
|     id: accountStatusLabel | ||||
|     !text: tr('Free Account') | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: separator.top | ||||
|     margin-bottom: 5 | ||||
|     text-auto-resize: true | ||||
|  | ||||
|     $on: | ||||
|       color: #FF0000 | ||||
|  | ||||
|   HorizontalSeparator | ||||
|     id: separator | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: next.top | ||||
|     margin-bottom: 10 | ||||
|  | ||||
|   Button | ||||
|     id: autoReconnect | ||||
|     !text: tr('Auto reconnect: On') | ||||
|     width: 140 | ||||
|     anchors.left: parent.left | ||||
|     anchors.bottom: parent.bottom | ||||
|     image-color: green | ||||
|     $!on: | ||||
|       image-color: red     | ||||
|       !text: tr('Auto reconnect: Off') | ||||
|  | ||||
|   Button | ||||
|     id: buttonOk | ||||
|     !text: tr('Ok') | ||||
|     width: 64 | ||||
|     anchors.right: next.left | ||||
|     anchors.bottom: parent.bottom | ||||
|     margin-right: 10 | ||||
|     @onClick: CharacterList.doLogin() | ||||
|  | ||||
|   Button | ||||
|     id: buttonCancel | ||||
|     !text: tr('Cancel') | ||||
|     width: 64 | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: parent.bottom | ||||
|     @onClick: CharacterList.hide(true) | ||||
							
								
								
									
										594
									
								
								SabrehavenOTClient/modules/client_entergame/entergame.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										594
									
								
								SabrehavenOTClient/modules/client_entergame/entergame.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,594 @@ | ||||
| EnterGame = { } | ||||
|  | ||||
| -- private variables | ||||
| local loadBox | ||||
| local enterGame | ||||
| local enterGameButton | ||||
| local clientBox | ||||
| local protocolLogin | ||||
| local server = nil | ||||
| local versionsFound = false | ||||
|  | ||||
| local customServerSelectorPanel | ||||
| local serverSelectorPanel | ||||
| local serverSelector | ||||
| local clientVersionSelector | ||||
| local serverHostTextEdit | ||||
| local rememberPasswordBox | ||||
| local protos = {"740", "760", "772", "792", "800", "810", "854", "860", "870", "910", "961", "1000", "1077", "1090", "1096", "1098", "1099", "1100", "1200", "1220"} | ||||
|  | ||||
| local checkedByUpdater = {} | ||||
| local waitingForHttpResults = 0 | ||||
|  | ||||
| -- private functions | ||||
| local function onProtocolError(protocol, message, errorCode) | ||||
|   if errorCode then | ||||
|     return EnterGame.onError(message) | ||||
|   end | ||||
|   return EnterGame.onLoginError(message) | ||||
| end | ||||
|  | ||||
| local function onSessionKey(protocol, sessionKey) | ||||
|   G.sessionKey = sessionKey | ||||
| end | ||||
|  | ||||
| local function onCharacterList(protocol, characters, account, otui) | ||||
|   if rememberPasswordBox:isChecked() then | ||||
|     local account = g_crypt.encrypt(G.account) | ||||
|     local password = g_crypt.encrypt(G.password) | ||||
|  | ||||
|     g_settings.set('account', account) | ||||
|     g_settings.set('password', password) | ||||
|   else | ||||
|     EnterGame.clearAccountFields() | ||||
|   end | ||||
|  | ||||
|   for _, characterInfo in pairs(characters) do | ||||
|     if characterInfo.previewState and characterInfo.previewState ~= PreviewState.Default then | ||||
|       characterInfo.worldName = characterInfo.worldName .. ', Preview' | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if loadBox then | ||||
|     loadBox:destroy() | ||||
|     loadBox = nil | ||||
|   end | ||||
|      | ||||
|   CharacterList.create(characters, account, otui) | ||||
|   CharacterList.show() | ||||
|  | ||||
|   g_settings.save() | ||||
| end | ||||
|  | ||||
| local function onUpdateNeeded(protocol, signature) | ||||
|   return EnterGame.onError(tr('Your client needs updating, try redownloading it.')) | ||||
| end | ||||
|  | ||||
| local function onProxyList(protocol, proxies) | ||||
|   for _, proxy in ipairs(proxies) do | ||||
|     g_proxy.addProxy(proxy["host"], proxy["port"], proxy["priority"]) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function parseFeatures(features) | ||||
|   for feature_id, value in pairs(features) do | ||||
|       if value == "1" or value == "true" or value == true then | ||||
|         g_game.enableFeature(feature_id) | ||||
|       else | ||||
|         g_game.disableFeature(feature_id) | ||||
|       end | ||||
|   end   | ||||
| end | ||||
|  | ||||
| local function validateThings(things) | ||||
|   local incorrectThings = "" | ||||
|   local missingFiles = false | ||||
|   local versionForMissingFiles = 0 | ||||
|   if things ~= nil then | ||||
|     local thingsNode = {} | ||||
|     for thingtype, thingdata in pairs(things) do | ||||
|       thingsNode[thingtype] = thingdata[1] | ||||
|       if not g_resources.fileExists("/things/" .. thingdata[1]) then | ||||
|         incorrectThings = incorrectThings .. "Missing file: " .. thingdata[1] .. "\n" | ||||
|         missingFiles = true | ||||
|         versionForMissingFiles = thingdata[1]:split("/")[1] | ||||
|       else | ||||
|         local localChecksum = g_resources.fileChecksum("/things/" .. thingdata[1]):lower() | ||||
|         if localChecksum ~= thingdata[2]:lower() and #thingdata[2] > 1 then | ||||
|           if g_resources.isLoadedFromArchive() then -- ignore checksum if it's test/debug version | ||||
|             incorrectThings = incorrectThings .. "Invalid checksum of file: " .. thingdata[1] .. " (is " .. localChecksum .. ", should be " .. thingdata[2]:lower() .. ")\n" | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|     g_settings.setNode("things", thingsNode) | ||||
|   else | ||||
|     g_settings.setNode("things", {}) | ||||
|   end | ||||
|   if missingFiles then | ||||
|    | ||||
|     incorrectThings = incorrectThings .. "\nYou should open data/things and create directory " .. versionForMissingFiles ..  | ||||
|     ".\nIn this directory (data/things/" .. versionForMissingFiles .. ") you should put missing\nfiles (Tibia.dat and Tibia.spr) " .. | ||||
|     "from correct Tibia version." | ||||
|   end | ||||
|   return incorrectThings | ||||
| end | ||||
|  | ||||
| local function onTibia12HTTPResult(session, playdata) | ||||
|   local characters = {} | ||||
|   local worlds = {} | ||||
|   local account = { | ||||
|     status = 0, | ||||
|     subStatus = 0, | ||||
|     premDays = 0 | ||||
|   } | ||||
|   if session["status"] ~= "active" then | ||||
|     account.status = 1 | ||||
|   end | ||||
|   if session["ispremium"] then | ||||
|     account.subStatus = 1 -- premium | ||||
|   end | ||||
|   if session["premiumuntil"] > g_clock.seconds() then | ||||
|     account.subStatus = math.floor((session["premiumuntil"] - g_clock.seconds()) / 86400) | ||||
|   end | ||||
|      | ||||
|   local things = { | ||||
|     data = {G.clientVersion .. "/Tibia.dat", ""}, | ||||
|     sprites = {G.clientVersion .. "/Tibia.spr", ""}, | ||||
|   } | ||||
|    | ||||
|   local incorrectThings = validateThings(things) | ||||
|   if #incorrectThings > 0 then | ||||
|     g_logger.error(incorrectThings) | ||||
|     if Updater and not checkedByUpdater[G.clientVersion] then | ||||
|       checkedByUpdater[G.clientVersion] = true | ||||
|       return Updater.check({ | ||||
|         version = G.clientVersion, | ||||
|         host = G.host | ||||
|       }) | ||||
|     else | ||||
|       return EnterGame.onError(incorrectThings) | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   onSessionKey(nil, session["sessionkey"]) | ||||
|    | ||||
|   for _, world in pairs(playdata["worlds"]) do | ||||
|     worlds[world.id] = { | ||||
|       name = world.name, | ||||
|       port = world.externalportunprotected or world.externalportprotected or world.externaladdress, | ||||
|       address = world.externaladdressunprotected or world.externaladdressprotected or world.externalport | ||||
|     } | ||||
|   end | ||||
|    | ||||
|   for _, character in pairs(playdata["characters"]) do | ||||
|     local world = worlds[character.worldid] | ||||
|     if world then | ||||
|       table.insert(characters, { | ||||
|         name = character.name, | ||||
|         worldName = world.name, | ||||
|         worldIp = world.address, | ||||
|         worldPort = world.port | ||||
|       }) | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   -- proxies | ||||
|   if g_proxy then | ||||
|     g_proxy.clear() | ||||
|     if playdata["proxies"] then | ||||
|       for i, proxy in ipairs(playdata["proxies"]) do | ||||
|         g_proxy.addProxy(proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"])) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   g_game.setCustomProtocolVersion(0) | ||||
|   g_game.chooseRsa(G.host) | ||||
|   g_game.setClientVersion(G.clientVersion) | ||||
|   g_game.setProtocolVersion(g_game.getClientProtocolVersion(G.clientVersion)) | ||||
|   g_game.setCustomOs(-1) -- disable | ||||
|   if not g_game.getFeature(GameExtendedOpcode) then | ||||
|     g_game.setCustomOs(5) -- set os to windows if opcodes are disabled | ||||
|   end | ||||
|    | ||||
|   onCharacterList(nil, characters, account, nil)   | ||||
| end | ||||
|  | ||||
| local function onHTTPResult(data, err) | ||||
|   if waitingForHttpResults == 0 then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   waitingForHttpResults = waitingForHttpResults - 1 | ||||
|   if err and waitingForHttpResults > 0 then | ||||
|     return -- ignore, wait for other requests | ||||
|   end | ||||
|  | ||||
|   if err then | ||||
|     return EnterGame.onError(err) | ||||
|   end | ||||
|   waitingForHttpResults = 0  | ||||
|   if data['error'] and data['error']:len() > 0 then | ||||
|     return EnterGame.onLoginError(data['error']) | ||||
|   elseif data['errorMessage'] and data['errorMessage']:len() > 0 then | ||||
|     return EnterGame.onLoginError(data['errorMessage']) | ||||
|   end | ||||
|    | ||||
|   if type(data["session"]) == "table" and type(data["playdata"]) == "table" then | ||||
|     return onTibia12HTTPResult(data["session"], data["playdata"]) | ||||
|   end   | ||||
|    | ||||
|   local characters = data["characters"] | ||||
|   local account = data["account"] | ||||
|   local session = data["session"] | ||||
|   | ||||
|   local version = data["version"] | ||||
|   local things = data["things"] | ||||
|   local customProtocol = data["customProtocol"] | ||||
|  | ||||
|   local features = data["features"] | ||||
|   local settings = data["settings"] | ||||
|   local rsa = data["rsa"] | ||||
|   local proxies = data["proxies"] | ||||
|  | ||||
|   local incorrectThings = validateThings(things) | ||||
|   if #incorrectThings > 0 then | ||||
|     g_logger.info(incorrectThings) | ||||
|     return EnterGame.onError(incorrectThings) | ||||
|   end | ||||
|    | ||||
|   -- custom protocol | ||||
|   g_game.setCustomProtocolVersion(0) | ||||
|   if customProtocol ~= nil then | ||||
|     customProtocol = tonumber(customProtocol) | ||||
|     if customProtocol ~= nil and customProtocol > 0 then | ||||
|       g_game.setCustomProtocolVersion(customProtocol) | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   -- force player settings | ||||
|   if settings ~= nil then | ||||
|     for option, value in pairs(settings) do | ||||
|       modules.client_options.setOption(option, value, true) | ||||
|     end | ||||
|   end | ||||
|      | ||||
|   -- version | ||||
|   G.clientVersion = version | ||||
|   g_game.setClientVersion(version) | ||||
|   g_game.setProtocolVersion(g_game.getClientProtocolVersion(version))   | ||||
|   g_game.setCustomOs(-1) -- disable | ||||
|    | ||||
|   if rsa ~= nil then | ||||
|     g_game.setRsa(rsa) | ||||
|   end | ||||
|  | ||||
|   if features ~= nil then | ||||
|     parseFeatures(features) | ||||
|   end | ||||
|  | ||||
|   if session ~= nil and session:len() > 0 then | ||||
|     onSessionKey(nil, session) | ||||
|   end | ||||
|    | ||||
|   -- proxies | ||||
|   if g_proxy then | ||||
|     g_proxy.clear() | ||||
|     if proxies then | ||||
|       for i, proxy in ipairs(proxies) do | ||||
|         g_proxy.addProxy(proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"])) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   onCharacterList(nil, characters, account, nil)   | ||||
| end | ||||
|  | ||||
|  | ||||
| -- public functions | ||||
| function EnterGame.init() | ||||
|   if USE_NEW_ENERGAME then return end | ||||
|   enterGame = g_ui.displayUI('entergame') | ||||
|    | ||||
|   serverSelectorPanel = enterGame:getChildById('serverSelectorPanel') | ||||
|   customServerSelectorPanel = enterGame:getChildById('customServerSelectorPanel') | ||||
|    | ||||
|   serverSelector = serverSelectorPanel:getChildById('serverSelector') | ||||
|   rememberPasswordBox = enterGame:getChildById('rememberPasswordBox') | ||||
|   serverHostTextEdit = customServerSelectorPanel:getChildById('serverHostTextEdit') | ||||
|   clientVersionSelector = customServerSelectorPanel:getChildById('clientVersionSelector') | ||||
|    | ||||
|   if Servers ~= nil then  | ||||
|     for name,server in pairs(Servers) do | ||||
|       serverSelector:addOption(name) | ||||
|     end | ||||
|   end | ||||
|   if serverSelector:getOptionsCount() == 0 or ALLOW_CUSTOM_SERVERS then | ||||
|     serverSelector:addOption(tr("Another"))     | ||||
|   end   | ||||
|   for i,proto in pairs(protos) do | ||||
|     clientVersionSelector:addOption(proto) | ||||
|   end | ||||
|  | ||||
|   if serverSelector:getOptionsCount() == 1 then | ||||
|     enterGame:setHeight(enterGame:getHeight() - serverSelectorPanel:getHeight()) | ||||
|     serverSelectorPanel:setOn(false) | ||||
|   end | ||||
|    | ||||
|   local account = g_crypt.decrypt(g_settings.get('account')) | ||||
|   local password = g_crypt.decrypt(g_settings.get('password')) | ||||
|   local server = g_settings.get('server') | ||||
|   local host = g_settings.get('host') | ||||
|   local clientVersion = g_settings.get('client-version') | ||||
|  | ||||
|   if serverSelector:isOption(server) then | ||||
|     serverSelector:setCurrentOption(server, false) | ||||
|     if Servers == nil or Servers[server] == nil then | ||||
|       serverHostTextEdit:setText(host) | ||||
|     end | ||||
|     clientVersionSelector:setOption(clientVersion) | ||||
|   else | ||||
|     server = "" | ||||
|     host = "" | ||||
|   end | ||||
|    | ||||
|   enterGame:getChildById('accountPasswordTextEdit'):setText(password) | ||||
|   enterGame:getChildById('accountNameTextEdit'):setText(account) | ||||
|   rememberPasswordBox:setChecked(#account > 0) | ||||
|      | ||||
|   g_keyboard.bindKeyDown('Ctrl+G', EnterGame.openWindow) | ||||
|  | ||||
|   if g_game.isOnline() then | ||||
|     return EnterGame.hide() | ||||
|   end | ||||
|  | ||||
|   scheduleEvent(function() | ||||
|     EnterGame.show() | ||||
|   end, 100) | ||||
| end | ||||
|  | ||||
| function EnterGame.terminate() | ||||
|   if not enterGame then return end | ||||
|   g_keyboard.unbindKeyDown('Ctrl+G') | ||||
|    | ||||
|   enterGame:destroy() | ||||
|   if loadBox then | ||||
|     loadBox:destroy() | ||||
|     loadBox = nil | ||||
|   end | ||||
|   if protocolLogin then | ||||
|     protocolLogin:cancelLogin() | ||||
|     protocolLogin = nil | ||||
|   end | ||||
|   EnterGame = nil | ||||
| end | ||||
|  | ||||
| function EnterGame.show() | ||||
|   if not enterGame then return end | ||||
|   enterGame:show() | ||||
|   enterGame:raise() | ||||
|   enterGame:focus() | ||||
|   enterGame:getChildById('accountNameTextEdit'):focus() | ||||
| end | ||||
|  | ||||
| function EnterGame.hide() | ||||
|   if not enterGame then return end | ||||
|   enterGame:hide() | ||||
| end | ||||
|  | ||||
| function EnterGame.openWindow() | ||||
|   if g_game.isOnline() then | ||||
|     CharacterList.show() | ||||
|   elseif not g_game.isLogging() and not CharacterList.isVisible() then | ||||
|     EnterGame.show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function EnterGame.clearAccountFields() | ||||
|   enterGame:getChildById('accountNameTextEdit'):clearText() | ||||
|   enterGame:getChildById('accountPasswordTextEdit'):clearText() | ||||
|   enterGame:getChildById('accountTokenTextEdit'):clearText() | ||||
|   enterGame:getChildById('accountNameTextEdit'):focus() | ||||
|   g_settings.remove('account') | ||||
|   g_settings.remove('password') | ||||
| end | ||||
|  | ||||
| function EnterGame.onServerChange() | ||||
|   server = serverSelector:getText() | ||||
|   if server == tr("Another") then | ||||
|     if not customServerSelectorPanel:isOn() then | ||||
|       serverHostTextEdit:setText("") | ||||
|       customServerSelectorPanel:setOn(true)   | ||||
|       enterGame:setHeight(enterGame:getHeight() + customServerSelectorPanel:getHeight()) | ||||
|     end | ||||
|   elseif customServerSelectorPanel:isOn() then | ||||
|     enterGame:setHeight(enterGame:getHeight() - customServerSelectorPanel:getHeight()) | ||||
|     customServerSelectorPanel:setOn(false) | ||||
|   end | ||||
|   if Servers and Servers[server] ~= nil then | ||||
|     if type(Servers[server]) == "table" then | ||||
|       serverHostTextEdit:setText(Servers[server][1]) | ||||
|     else | ||||
|       serverHostTextEdit:setText(Servers[server]) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function EnterGame.doLogin() | ||||
|   if g_game.isOnline() then | ||||
|     local errorBox = displayErrorBox(tr('Login Error'), tr('Cannot login while already in game.')) | ||||
|     connect(errorBox, { onOk = EnterGame.show }) | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   G.account = enterGame:getChildById('accountNameTextEdit'):getText() | ||||
|   G.password = enterGame:getChildById('accountPasswordTextEdit'):getText() | ||||
|   G.authenticatorToken = enterGame:getChildById('accountTokenTextEdit'):getText() | ||||
|   G.stayLogged = true | ||||
|   G.server = serverSelector:getText():trim() | ||||
|   G.host = serverHostTextEdit:getText() | ||||
|   G.clientVersion = tonumber(clientVersionSelector:getText())   | ||||
|   | ||||
|   if not rememberPasswordBox:isChecked() then | ||||
|     g_settings.set('account', G.account) | ||||
|     g_settings.set('password', G.password)   | ||||
|   end | ||||
|   g_settings.set('host', G.host) | ||||
|   g_settings.set('server', G.server) | ||||
|   g_settings.set('client-version', G.clientVersion) | ||||
|   g_settings.save() | ||||
|  | ||||
|   local server_params = G.host:split(":") | ||||
|   if G.host:lower():find("http") ~= nil then | ||||
|     if #server_params >= 4 then | ||||
|       G.host = server_params[1] .. ":" .. server_params[2] .. ":" .. server_params[3]  | ||||
|       G.clientVersion = tonumber(server_params[4]) | ||||
|     elseif #server_params >= 3 then | ||||
|       if tostring(tonumber(server_params[3])) == server_params[3] then | ||||
|         G.host = server_params[1] .. ":" .. server_params[2]  | ||||
|         G.clientVersion = tonumber(server_params[3]) | ||||
|       end | ||||
|     end | ||||
|     return EnterGame.doLoginHttp()       | ||||
|   end | ||||
|    | ||||
|   local server_ip = server_params[1] | ||||
|   local server_port = 7171 | ||||
|   if #server_params >= 2 then | ||||
|     server_port = tonumber(server_params[2]) | ||||
|   end | ||||
|   if #server_params >= 3 then | ||||
|     G.clientVersion = tonumber(server_params[3]) | ||||
|   end | ||||
|   if type(server_ip) ~= 'string' or server_ip:len() <= 3 or not server_port or not G.clientVersion then | ||||
|     return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")   | ||||
|   end | ||||
|    | ||||
|   local things = { | ||||
|     data = {G.clientVersion .. "/Tibia.dat", ""}, | ||||
|     sprites = {G.clientVersion .. "/Tibia.spr", ""}, | ||||
|   } | ||||
|    | ||||
|   local incorrectThings = validateThings(things) | ||||
|   if #incorrectThings > 0 then | ||||
|     g_logger.error(incorrectThings) | ||||
|     if Updater and not checkedByUpdater[G.clientVersion] then | ||||
|       checkedByUpdater[G.clientVersion] = true | ||||
|       return Updater.check({ | ||||
|         version = G.clientVersion, | ||||
|         host = G.host | ||||
|       }) | ||||
|     else | ||||
|       return EnterGame.onError(incorrectThings) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   protocolLogin = ProtocolLogin.create() | ||||
|   protocolLogin.onLoginError = onProtocolError | ||||
|   protocolLogin.onSessionKey = onSessionKey | ||||
|   protocolLogin.onCharacterList = onCharacterList | ||||
|   protocolLogin.onUpdateNeeded = onUpdateNeeded | ||||
|   protocolLogin.onProxyList = onProxyList | ||||
|  | ||||
|   EnterGame.hide() | ||||
|   loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...')) | ||||
|   connect(loadBox, { onCancel = function(msgbox) | ||||
|                                   loadBox = nil | ||||
|                                   protocolLogin:cancelLogin() | ||||
|                                   EnterGame.show() | ||||
|                                 end }) | ||||
|  | ||||
|   if G.clientVersion == 1000 then -- some people don't understand that tibia 10 uses 1100 protocol | ||||
|     G.clientVersion = 1100 | ||||
|   end | ||||
|   -- if you have custom rsa or protocol edit it here | ||||
|   g_game.setClientVersion(G.clientVersion) | ||||
|   g_game.setProtocolVersion(g_game.getClientProtocolVersion(G.clientVersion)) | ||||
|   g_game.setCustomProtocolVersion(0) | ||||
|   g_game.setCustomOs(-1) -- disable | ||||
|   g_game.chooseRsa(G.host) | ||||
|   if #server_params <= 3 and not g_game.getFeature(GameExtendedOpcode) then | ||||
|     g_game.setCustomOs(2) -- set os to windows if opcodes are disabled | ||||
|   end | ||||
|  | ||||
|   -- extra features from init.lua | ||||
|   for i = 4, #server_params do | ||||
|     g_game.enableFeature(tonumber(server_params[i])) | ||||
|   end | ||||
|    | ||||
|   -- proxies | ||||
|   if g_proxy then | ||||
|     g_proxy.clear() | ||||
|   end | ||||
|    | ||||
|   if modules.game_things.isLoaded() then | ||||
|     g_logger.info("Connecting to: " .. server_ip .. ":" .. server_port) | ||||
|     protocolLogin:login(server_ip, server_port, G.account, G.password, G.authenticatorToken, G.stayLogged) | ||||
|   else | ||||
|     loadBox:destroy() | ||||
|     loadBox = nil | ||||
|     EnterGame.show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function EnterGame.doLoginHttp() | ||||
|   if G.host == nil or G.host:len() < 10 then | ||||
|     return EnterGame.onError("Invalid server url: " .. G.host)     | ||||
|   end | ||||
|  | ||||
|   loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...')) | ||||
|   connect(loadBox, { onCancel = function(msgbox) | ||||
|                                   loadBox = nil | ||||
|                                   EnterGame.show() | ||||
|                                 end })                                 | ||||
|                                | ||||
|   local data = { | ||||
|     type = "login", | ||||
|     account = G.account, | ||||
|     accountname = G.account, | ||||
|     email = G.account, | ||||
|     password = G.password, | ||||
|     accountpassword = G.password, | ||||
|     token = G.authenticatorToken, | ||||
|     version = APP_VERSION, | ||||
|     uid = G.UUID, | ||||
|     stayloggedin = true | ||||
|   } | ||||
|    | ||||
|   local server = serverSelector:getText() | ||||
|   if Servers and Servers[server] ~= nil then | ||||
|     if type(Servers[server]) == "table" then | ||||
|       local urls = Servers[server]       | ||||
|       waitingForHttpResults = #urls | ||||
|       for _, url in ipairs(urls) do | ||||
|         HTTP.postJSON(url, data, onHTTPResult) | ||||
|       end | ||||
|     else | ||||
|       waitingForHttpResults = 1 | ||||
|       HTTP.postJSON(G.host, data, onHTTPResult)     | ||||
|     end | ||||
|   end | ||||
|   EnterGame.hide() | ||||
| end | ||||
|  | ||||
| function EnterGame.onError(err) | ||||
|   if loadBox then | ||||
|     loadBox:destroy() | ||||
|     loadBox = nil | ||||
|   end | ||||
|   local errorBox = displayErrorBox(tr('Login Error'), err) | ||||
|   errorBox.onOk = EnterGame.show | ||||
| end | ||||
|  | ||||
| function EnterGame.onLoginError(err) | ||||
|   if loadBox then | ||||
|     loadBox:destroy() | ||||
|     loadBox = nil | ||||
|   end | ||||
|   local errorBox = displayErrorBox(tr('Login Error'), err) | ||||
|   errorBox.onOk = EnterGame.show | ||||
|   if err:lower():find("invalid") or err:lower():find("not correct") or err:lower():find("or password") then | ||||
|     EnterGame.clearAccountFields() | ||||
|   end | ||||
| end | ||||
							
								
								
									
										12
									
								
								SabrehavenOTClient/modules/client_entergame/entergame.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								SabrehavenOTClient/modules/client_entergame/entergame.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| Module | ||||
|   name: client_entergame | ||||
|   description: Manages enter game and character list windows | ||||
|   author: edubart & otclient.ovh | ||||
|   website: https://github.com/edubart/otclient | ||||
|   scripts: [ entergame, characterlist ] | ||||
|   @onLoad: EnterGame.init() CharacterList.init() | ||||
|   @onUnload: EnterGame.terminate() CharacterList.terminate() | ||||
|    | ||||
|   load-later: | ||||
|     - game_things | ||||
|     - game_features | ||||
							
								
								
									
										186
									
								
								SabrehavenOTClient/modules/client_entergame/entergame.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								SabrehavenOTClient/modules/client_entergame/entergame.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| EnterGameWindow | ||||
|   id: enterGame | ||||
|   @onEnter: EnterGame.doLogin() | ||||
|  | ||||
|   MenuLabel | ||||
|     !text: tr('Account name') | ||||
|     anchors.left: parent.left | ||||
|     anchors.top: parent.top | ||||
|     text-auto-resize: true | ||||
|  | ||||
|   TextEdit | ||||
|     id: accountNameTextEdit | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 2 | ||||
|  | ||||
|   MenuLabel | ||||
|     !text: tr('Password') | ||||
|     anchors.left: prev.left | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 8 | ||||
|     text-auto-resize: true | ||||
|  | ||||
|   PasswordTextEdit | ||||
|     id: accountPasswordTextEdit | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 2 | ||||
|      | ||||
|   MenuLabel | ||||
|     !text: tr('Token') | ||||
|     anchors.left: prev.left | ||||
|     anchors.top: prev.bottom | ||||
|     text-auto-resize: true | ||||
|     margin-top: 8 | ||||
|  | ||||
|   TextEdit | ||||
|     id: accountTokenTextEdit | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 2 | ||||
|  | ||||
|   Panel | ||||
|     id: serverSelectorPanel | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     height: 52 | ||||
|     on: true | ||||
|     focusable: false | ||||
|      | ||||
|     $on: | ||||
|       visible: true | ||||
|       margin-top: 0 | ||||
|      | ||||
|     $!on: | ||||
|       visible: false | ||||
|       margin-top: -52 | ||||
|        | ||||
|     HorizontalSeparator | ||||
|       anchors.left: parent.left | ||||
|       anchors.right: parent.right | ||||
|       anchors.top: parent.top | ||||
|       margin-top: 10 | ||||
|  | ||||
|     MenuLabel | ||||
|       id: serverLabel | ||||
|       !text: tr('Server') | ||||
|       anchors.left: parent.left | ||||
|       anchors.top: prev.bottom | ||||
|       text-auto-resize: true     | ||||
|       margin-top: 5 | ||||
|  | ||||
|     ComboBox | ||||
|       id: serverSelector | ||||
|       anchors.left: prev.left | ||||
|       anchors.right: parent.right | ||||
|       anchors.top: serverLabel.bottom | ||||
|       margin-top: 2 | ||||
|       margin-right: 3 | ||||
|       menu-scroll: true | ||||
|       menu-height: 125 | ||||
|       menu-scroll-step: 25 | ||||
|       text-offset: 5 2 | ||||
|       @onOptionChange: EnterGame.onServerChange() | ||||
|      | ||||
|   Panel | ||||
|     id: customServerSelectorPanel | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     height: 52 | ||||
|     on: true | ||||
|     focusable: true | ||||
|      | ||||
|     $on: | ||||
|       visible: true | ||||
|       margin-top: 0 | ||||
|      | ||||
|     $!on: | ||||
|       visible: false | ||||
|       margin-top: -52 | ||||
|      | ||||
|     HorizontalSeparator | ||||
|       anchors.left: parent.left | ||||
|       anchors.right: parent.right | ||||
|       anchors.top: parent.top | ||||
|       margin-top: 8 | ||||
|  | ||||
|     MenuLabel | ||||
|       id: serverLabel | ||||
|       !text: tr('IP:PORT or URL') | ||||
|       anchors.left: prev.left | ||||
|       anchors.top: prev.bottom | ||||
|       margin-top: 8 | ||||
|       text-auto-resize: true | ||||
|  | ||||
|     TextEdit | ||||
|       id: serverHostTextEdit | ||||
|       !tooltip: tr('Make sure that your client uses\nthe correct game client version') | ||||
|       anchors.left: parent.left | ||||
|       anchors.top: serverLabel.bottom | ||||
|       margin-top: 2 | ||||
|       width: 150 | ||||
|  | ||||
|     MenuLabel | ||||
|       id: clientLabel | ||||
|       !text: tr('Version') | ||||
|       anchors.left: serverHostTextEdit.right | ||||
|       anchors.top: serverLabel.top | ||||
|       text-auto-resize: true | ||||
|       margin-left: 10 | ||||
|  | ||||
|     ComboBox | ||||
|       id: clientVersionSelector | ||||
|       anchors.top: serverHostTextEdit.top | ||||
|       anchors.bottom: serverHostTextEdit.bottom | ||||
|       anchors.left: prev.left | ||||
|       anchors.right: parent.right | ||||
|       menu-scroll: true | ||||
|       menu-height: 125 | ||||
|       menu-scroll-step: 25 | ||||
|       margin-right: 3 | ||||
|        | ||||
|   HorizontalSeparator | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 10 | ||||
|  | ||||
|   CheckBox | ||||
|     id: rememberPasswordBox | ||||
|     !text: tr('Remember password') | ||||
|     !tooltip: tr('Remember account and password when starts client') | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 9 | ||||
|  | ||||
|   HorizontalSeparator | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 9 | ||||
|  | ||||
|   Button | ||||
|     !text: tr('Login') | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 10 | ||||
|     margin-left: 50 | ||||
|     margin-right: 50 | ||||
|     @onClick: EnterGame.doLogin() | ||||
|  | ||||
|   Label | ||||
|     id: serverInfoLabel | ||||
|     font: verdana-11px-rounded | ||||
|     anchors.top: prev.top | ||||
|     anchors.left: parent.left | ||||
|     margin-top: 5 | ||||
|     color: green | ||||
|     text-auto-resize: true | ||||
							
								
								
									
										44
									
								
								SabrehavenOTClient/modules/client_entergame/waitinglist.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								SabrehavenOTClient/modules/client_entergame/waitinglist.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| MainWindow | ||||
|   id: waitingWindow | ||||
|   !text: tr('Waiting List') | ||||
|   size: 260 180 | ||||
|   @onEscape: CharacterList.cancelWait() | ||||
|  | ||||
|   Label | ||||
|     id: infoLabel | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: progressBar.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     text-wrap: true | ||||
|  | ||||
|   ProgressBar | ||||
|     id: progressBar | ||||
|     height: 15 | ||||
|     background-color: #4444ff | ||||
|     anchors.bottom: timeLabel.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     margin-bottom: 10 | ||||
|  | ||||
|   Label | ||||
|     id: timeLabel | ||||
|     anchors.bottom: separator.bottom | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     margin-bottom: 10 | ||||
|  | ||||
|   HorizontalSeparator | ||||
|     id: separator | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: next.top | ||||
|     margin-bottom: 10 | ||||
|  | ||||
|   Button | ||||
|     id: buttonCancel | ||||
|     !text: tr('Cancel') | ||||
|     width: 64 | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: parent.bottom | ||||
|     @onClick: CharacterList.cancelWait() | ||||
							
								
								
									
										109
									
								
								SabrehavenOTClient/modules/client_feedback/feedback.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								SabrehavenOTClient/modules/client_feedback/feedback.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| local feedbackWindow | ||||
| local textEdit | ||||
| local okButton | ||||
| local cancelButton | ||||
| local postId = 0 | ||||
| local tries = 0 | ||||
| local replyEvent = nil | ||||
|  | ||||
| function init() | ||||
|   feedbackWindow = g_ui.displayUI('feedback') | ||||
|   feedbackWindow:hide() | ||||
|  | ||||
|   textEdit = feedbackWindow:getChildById('text') | ||||
|   okButton = feedbackWindow:getChildById('okButton') | ||||
|   cancelButton = feedbackWindow:getChildById('cancelButton') | ||||
|  | ||||
|   okButton.onClick = send | ||||
|   cancelButton.onClick = hide | ||||
|   feedbackWindow.onEscape = hide     | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   feedbackWindow:destroy() | ||||
|   removeEvent(replyEvent) | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   if not Services or not Services.feedback or Services.feedback:len() < 4 then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   feedbackWindow:show() | ||||
|   feedbackWindow:raise() | ||||
|   feedbackWindow:focus() | ||||
|    | ||||
|   textEdit:setMaxLength(8192) | ||||
|   textEdit:setText('') | ||||
|   textEdit:setEditable(true) | ||||
|   textEdit:setCursorVisible(true) | ||||
|   feedbackWindow:focusChild(textEdit, KeyboardFocusReason) | ||||
|    | ||||
|   tries = 0 | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   feedbackWindow:hide() | ||||
|   textEdit:setEditable(false) | ||||
|   textEdit:setCursorVisible(false) | ||||
| end | ||||
|  | ||||
| function send() | ||||
|   local text = textEdit:getText() | ||||
|   if text:len() > 1 then | ||||
|     local localPlayer = g_game.getLocalPlayer() | ||||
|     local playerData = nil | ||||
|     if localPlayer ~= nil then | ||||
|       playerData = { | ||||
|         name = localPlayer:getName(), | ||||
|         position = localPlayer:getPosition() | ||||
|       } | ||||
|     end | ||||
|     local details = { | ||||
|       report_delay = sendInterval, | ||||
|       os = g_app.getOs(), | ||||
|       graphics_vendor = g_graphics.getVendor(), | ||||
|       graphics_renderer = g_graphics.getRenderer(), | ||||
|       graphics_version = g_graphics.getVersion(), | ||||
|       fps = g_app.getFps(), | ||||
|       maxFps = g_app.getMaxFps(), | ||||
|       atlas = g_atlas.getStats(), | ||||
|       classic = tostring(g_settings.getBoolean("classicView")), | ||||
|       fullscreen = tostring(g_window.isFullscreen()), | ||||
|       vsync = tostring(g_settings.getBoolean("vsync")), | ||||
|       window_width = g_window.getWidth(), | ||||
|       window_height = g_window.getHeight(), | ||||
|       player_name = g_game.getCharacterName(), | ||||
|       world_name = g_game.getWorldName(), | ||||
|       otserv_host = G.host, | ||||
|       otserv_protocol = g_game.getProtocolVersion(), | ||||
|       otserv_client = g_game.getClientVersion(), | ||||
|       build_version = g_app.getVersion(), | ||||
|       build_revision = g_app.getBuildRevision(), | ||||
|       build_commit = g_app.getBuildCommit(), | ||||
|       build_date = g_app.getBuildDate(), | ||||
|       display_width = g_window.getDisplayWidth(), | ||||
|       display_height = g_window.getDisplayHeight(), | ||||
|       cpu = g_platform.getCPUName(), | ||||
|       mem = g_platform.getTotalSystemMemory(), | ||||
|       os_name = g_platform.getOSName() | ||||
|     }  | ||||
|     local data = json.encode({ | ||||
|       text = text, | ||||
|       version = g_app.getVersion(), | ||||
|       host = g_settings.get('host'), | ||||
|       player = playerData, | ||||
|       details = details | ||||
|     }) | ||||
|      | ||||
|     postId = HTTP.post(Services.feedback, data, function(ret, err)  | ||||
|       if err then | ||||
|         tries = tries + 1 | ||||
|         if tries < 3 then  | ||||
|           replyEvent = scheduleEvent(send, 1000) | ||||
|         end | ||||
|       end | ||||
|     end) | ||||
|   end  | ||||
|   hide() | ||||
| end | ||||
							
								
								
									
										10
									
								
								SabrehavenOTClient/modules/client_feedback/feedback.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								SabrehavenOTClient/modules/client_feedback/feedback.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| Module | ||||
|   name: client_feedback | ||||
|   description: Allow to send feedback | ||||
|   author: otclientv8 | ||||
|   website: otclient.ovh | ||||
|   sandboxed: true | ||||
|   dependencies: [ game_interface ] | ||||
|   scripts: [ feedback ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										48
									
								
								SabrehavenOTClient/modules/client_feedback/feedback.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								SabrehavenOTClient/modules/client_feedback/feedback.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| MainWindow | ||||
|   id: feedbackWindow | ||||
|   size: 400 280 | ||||
|   !text: tr("Feedback/Bug report") | ||||
|  | ||||
|   Label | ||||
|     id: description | ||||
|     anchors.top: parent.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     text-auto-resize: true | ||||
|     text-align: left | ||||
|     text-wrap: true | ||||
|     !text: tr("Bellow enter your feedback or bug report. Please include as much details as possible. Thank you!") | ||||
|  | ||||
|   MultilineTextEdit | ||||
|     id: text | ||||
|     anchors.top: textScroll.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: textScroll.left | ||||
|     anchors.bottom: textScroll.bottom | ||||
|     vertical-scrollbar: textScroll | ||||
|     text-wrap: true | ||||
|  | ||||
|   VerticalScrollBar | ||||
|     id: textScroll | ||||
|     anchors.top: description.bottom | ||||
|     anchors.bottom: okButton.top | ||||
|     anchors.right: parent.right | ||||
|     margin-top: 10 | ||||
|     margin-bottom: 10 | ||||
|     step: 16 | ||||
|     pixels-scroll: true | ||||
|  | ||||
|   Button | ||||
|     id: okButton | ||||
|     !text: tr('Ok') | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.right: next.left | ||||
|     margin-right: 10 | ||||
|     width: 60 | ||||
|  | ||||
|   Button | ||||
|     id: cancelButton | ||||
|     !text: tr('Cancel') | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.right: parent.right | ||||
|     width: 60 | ||||
							
								
								
									
										177
									
								
								SabrehavenOTClient/modules/client_locales/locales.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								SabrehavenOTClient/modules/client_locales/locales.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| dofile 'neededtranslations' | ||||
|  | ||||
| -- private variables | ||||
| local defaultLocaleName = 'en' | ||||
| local installedLocales | ||||
| local currentLocale | ||||
| local missingTranslations = {} | ||||
|  | ||||
| function createWindow() | ||||
|   localesWindow = g_ui.displayUI('locales') | ||||
|   local localesPanel = localesWindow:getChildById('localesPanel') | ||||
|   local layout = localesPanel:getLayout() | ||||
|   local spacing = layout:getCellSpacing() | ||||
|   local size = layout:getCellSize() | ||||
|  | ||||
|   local count = 0 | ||||
|   for name,locale in pairs(installedLocales) do | ||||
|     local widget = g_ui.createWidget('LocalesButton', localesPanel) | ||||
|     widget:setImageSource('/images/flags/' .. name .. '') | ||||
|     widget:setText(locale.languageName) | ||||
|     widget.onClick = function() selectFirstLocale(name) end | ||||
|     count = count + 1 | ||||
|   end | ||||
|  | ||||
|   count = math.max(1, math.min(count, 3)) | ||||
|   localesPanel:setWidth(size.width*count + spacing*(count-1)) | ||||
|  | ||||
|   addEvent(function() addEvent(function() localesWindow:raise() localesWindow:focus() end) end) | ||||
| end | ||||
|  | ||||
| function selectFirstLocale(name) | ||||
|   if localesWindow then | ||||
|     localesWindow:destroy() | ||||
|     localesWindow = nil | ||||
|   end | ||||
|   if setLocale(name) then | ||||
|     g_modules.reloadModules() | ||||
|   end | ||||
|   g_settings.save() | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function init() | ||||
|   installedLocales = {} | ||||
|  | ||||
|   installLocales('/locales') | ||||
|  | ||||
|   local userLocaleName = g_settings.get('locale', 'false') | ||||
|   if userLocaleName ~= 'false' and setLocale(userLocaleName) then | ||||
|     pdebug('Using configured locale: ' .. userLocaleName) | ||||
|   else | ||||
|     setLocale(defaultLocaleName) | ||||
|     --connect(g_app, { onRun = createWindow }) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   installedLocales = nil | ||||
|   currentLocale = nil | ||||
|  | ||||
|   --disconnect(g_app, { onRun = createWindow }) | ||||
| end | ||||
|  | ||||
| function generateNewTranslationTable(localename) | ||||
|   local locale = installedLocales[localename] | ||||
|   for _i,k in pairs(neededTranslations) do | ||||
|     local trans = locale.translation[k] | ||||
|     k = k:gsub('\n','\\n') | ||||
|     k = k:gsub('\t','\\t') | ||||
|     k = k:gsub('\"','\\\"') | ||||
|     if trans then | ||||
|       trans = trans:gsub('\n','\\n') | ||||
|       trans = trans:gsub('\t','\\t') | ||||
|       trans = trans:gsub('\"','\\\"') | ||||
|     end | ||||
|     if not trans then | ||||
|       print('    ["' .. k .. '"]' .. ' = false,') | ||||
|     else | ||||
|       print('    ["' .. k .. '"]' .. ' = "' .. trans .. '",') | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function installLocale(locale) | ||||
|   if not locale or not locale.name then | ||||
|     error('Unable to install locale.') | ||||
|   end | ||||
|  | ||||
|   if _G.allowedLocales and not _G.allowedLocales[locale.name] then return end | ||||
|  | ||||
|   if locale.name ~= defaultLocaleName then | ||||
|     local updatesNamesMissing = {} | ||||
|     for _,k in pairs(neededTranslations) do | ||||
|       if locale.translation[k] == nil then | ||||
|         updatesNamesMissing[#updatesNamesMissing + 1] = k | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     if #updatesNamesMissing > 0 then | ||||
|       pdebug('Locale \'' .. locale.name .. '\' is missing ' .. #updatesNamesMissing .. ' translations.') | ||||
|       for _,name in pairs(updatesNamesMissing) do | ||||
|         pdebug('["' .. name ..'"] = \"\",') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   local installedLocale = installedLocales[locale.name] | ||||
|   if installedLocale then | ||||
|     for word,translation in pairs(locale.translation) do | ||||
|       installedLocale.translation[word] = translation | ||||
|     end | ||||
|   else | ||||
|     installedLocales[locale.name] = locale | ||||
|   end | ||||
| end | ||||
|  | ||||
| function installLocales(directory) | ||||
|   dofiles(directory) | ||||
| end | ||||
|  | ||||
| function setLocale(name) | ||||
|   local locale = installedLocales[name] | ||||
|   if locale == currentLocale then return end | ||||
|   if not locale then | ||||
|     pwarning("Locale " .. name .. ' does not exist.') | ||||
|     return false | ||||
|   end | ||||
|   currentLocale = locale | ||||
|   g_settings.set('locale', name) | ||||
|   if onLocaleChanged then onLocaleChanged(name) end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function getInstalledLocales() | ||||
|   return installedLocales | ||||
| end | ||||
|  | ||||
| function getCurrentLocale() | ||||
|   return currentLocale | ||||
| end | ||||
|  | ||||
| -- global function used to translate texts | ||||
| function _G.tr(text, ...) | ||||
|   if currentLocale then | ||||
|     if tonumber(text) and currentLocale.formatNumbers then | ||||
|       local number = tostring(text):split('.') | ||||
|       local out = '' | ||||
|       local reverseNumber = number[1]:reverse() | ||||
|       for i=1,#reverseNumber do | ||||
|         out = out .. reverseNumber:sub(i, i) | ||||
|         if i % 3 == 0 and i ~= #number then | ||||
|           out = out .. currentLocale.thousandsSeperator | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       if number[2] then | ||||
|         out = number[2] .. currentLocale.decimalSeperator .. out | ||||
|       end | ||||
|       return out:reverse() | ||||
|     elseif tostring(text) then | ||||
|       local translation = currentLocale.translation[text] | ||||
|       if not translation then | ||||
|         if translation == nil then | ||||
|           if currentLocale.name ~= defaultLocaleName then | ||||
|             if not missingTranslations[text] then | ||||
|               pdebug('Unable to translate: \"' .. text .. '\"') | ||||
|               missingTranslations[text] = true | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|         translation = text | ||||
|       end | ||||
|       return string.format(translation, ...) | ||||
|     end | ||||
|   end | ||||
|   return text | ||||
| end | ||||
							
								
								
									
										9
									
								
								SabrehavenOTClient/modules/client_locales/locales.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								SabrehavenOTClient/modules/client_locales/locales.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: client_locales | ||||
|   description: Translates texts to selected language | ||||
|   author: baxnie, edubart | ||||
|   website: https://github.com/edubart/otclient | ||||
|   sandboxed: true | ||||
|   scripts: [ locales ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										35
									
								
								SabrehavenOTClient/modules/client_locales/locales.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								SabrehavenOTClient/modules/client_locales/locales.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| LocalesMainLabel < Label | ||||
|   font: sans-bold-16px | ||||
|  | ||||
| LocalesButton < UIWidget | ||||
|   size: 96 96 | ||||
|   image-size: 96 96 | ||||
|   image-smooth: true | ||||
|   text-offset: 0 96 | ||||
|   font: verdana-11px-antialised | ||||
|  | ||||
| UIWindow | ||||
|   id: localesWindow | ||||
|   background-color: #000000 | ||||
|   opacity: 0.90 | ||||
|   clipping: true | ||||
|   anchors.fill: parent | ||||
|  | ||||
|   LocalesMainLabel | ||||
|     !text: tr('Select your language') | ||||
|     text-auto-resize: true | ||||
|     anchors.horizontalCenter: parent.horizontalCenter | ||||
|     anchors.verticalCenter: parent.verticalCenter | ||||
|     margin-top: -100 | ||||
|  | ||||
|   Panel | ||||
|     id: localesPanel | ||||
|     margin-top: 50 | ||||
|     anchors.horizontalCenter: parent.horizontalCenter | ||||
|     anchors.top: prev.bottom | ||||
|     anchors.bottom: parent.bottom | ||||
|     layout: | ||||
|       type: grid | ||||
|       cell-size: 96 128 | ||||
|       cell-spacing: 32 | ||||
|       flow: true | ||||
							
								
								
									
										364
									
								
								SabrehavenOTClient/modules/client_locales/neededtranslations.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								SabrehavenOTClient/modules/client_locales/neededtranslations.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,364 @@ | ||||
| -- generated by ./tools/gen_needed_translations.sh | ||||
| neededTranslations = { | ||||
|   "1a) Offensive Name", | ||||
|   "1b) Invalid Name Format", | ||||
|   "1c) Unsuitable Name", | ||||
|   "1d) Name Inciting Rule Violation", | ||||
|   "2a) Offensive Statement", | ||||
|   "2b) Spamming", | ||||
|   "2c) Illegal Advertising", | ||||
|   "2d) Off-Topic Public Statement", | ||||
|   "2e) Non-English Public Statement", | ||||
|   "2f) Inciting Rule Violation", | ||||
|   "3a) Bug Abuse", | ||||
|   "3b) Game Weakness Abuse", | ||||
|   "3c) Using Unofficial Software to Play", | ||||
|   "3d) Hacking", | ||||
|   "3e) Multi-Clienting", | ||||
|   "3f) Account Trading or Sharing", | ||||
|   "4a) Threatening Gamemaster", | ||||
|   "4b) Pretending to Have Influence on Rule Enforcement", | ||||
|   "4c) False Report to Gamemaster", | ||||
|   "Accept", | ||||
|   "Account name", | ||||
|   "Account Status:", | ||||
|   "Action:", | ||||
|   "Add", | ||||
|   "Add new VIP", | ||||
|   "Addon 1", | ||||
|   "Addon 2", | ||||
|   "Addon 3", | ||||
|   "Add to VIP list", | ||||
|   "Adjust volume", | ||||
|   "Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!", | ||||
|   "All", | ||||
|   "All modules and scripts were reloaded.", | ||||
|   "Allow auto chase override", | ||||
|   "Ambient light: %s%%", | ||||
|   "Amount:", | ||||
|   "Amount", | ||||
|   "Anonymous", | ||||
|   "Are you sure you want to logout?", | ||||
|   "Attack", | ||||
|   "Author", | ||||
|   "Autoload", | ||||
|   "Autoload priority", | ||||
|   "Auto login", | ||||
|   "Auto login selected character on next charlist load", | ||||
|   "Axe Fighting", | ||||
|   "Balance:", | ||||
|   "Banishment", | ||||
|   "Banishment + Final Warning", | ||||
|   "Battle", | ||||
|   "Browse", | ||||
|   "Bug report sent.", | ||||
|   "Button Assign", | ||||
|   "Buy", | ||||
|   "Buy Now", | ||||
|   "Buy Offers", | ||||
|   "Buy with backpack", | ||||
|   "Cancel", | ||||
|   "Cannot login while already in game.", | ||||
|   "Cap", | ||||
|   "Capacity", | ||||
|   "Center", | ||||
|   "Channels", | ||||
|   "Character List", | ||||
|   "Classic control", | ||||
|   "Clear current message window", | ||||
|   "Clear Messages", | ||||
|   "Clear object", | ||||
|   "Client needs update.", | ||||
|   "Close", | ||||
|   "Close this channel", | ||||
|   "Club Fighting", | ||||
|   "Combat Controls", | ||||
|   "Comment:", | ||||
|   "Connecting to game server...", | ||||
|   "Connecting to login server...", | ||||
|   "Console", | ||||
|   "Cooldowns", | ||||
|   "Copy message", | ||||
|   "Copy name", | ||||
|   "Copy Name", | ||||
|   "Create Map Mark", | ||||
|   "Create mark", | ||||
|   "Create New Offer", | ||||
|   "Create Offer", | ||||
|   "Current hotkeys:", | ||||
|   "Current hotkey to add: %s", | ||||
|   "Current Offers", | ||||
|   "Default", | ||||
|   "Delete mark", | ||||
|   "Description:", | ||||
|   "Description", | ||||
|   "Destructive Behaviour", | ||||
|   "Detail", | ||||
|   "Details", | ||||
|   "Disable Shared Experience", | ||||
|   "Dismount", | ||||
|   "Display connection speed to the server (milliseconds)", | ||||
|   "Distance Fighting", | ||||
|   "Don\'t stretch/shrink Game Window", | ||||
|   "Edit hotkey text:", | ||||
|   "Edit List", | ||||
|   "Edit Text", | ||||
|   "Enable music", | ||||
|   "Enable Shared Experience", | ||||
|   "Enable smart walking", | ||||
|   "Enable vertical synchronization", | ||||
|   "Enable walk booster", | ||||
|   "Enter Game", | ||||
|   "Enter one name per line.", | ||||
|   "Enter with your account again to update your client.", | ||||
|   "Error", | ||||
|   "Error", | ||||
|   "Excessive Unjustified Player Killing", | ||||
|   "Exclude from private chat", | ||||
|   "Exit", | ||||
|   "Experience", | ||||
|   "Filter list to match your level", | ||||
|   "Filter list to match your vocation", | ||||
|   "Find:", | ||||
|   "Fishing", | ||||
|   "Fist Fighting", | ||||
|   "Follow", | ||||
|   "Force Exit", | ||||
|   "For Your Information", | ||||
|   "Free Account", | ||||
|   "Fullscreen", | ||||
|   "Game", | ||||
|   "Game framerate limit: %s", | ||||
|   "Graphics", | ||||
|   "Graphics card driver not detected", | ||||
|   "Graphics Engine:", | ||||
|   "Head", | ||||
|   "Healing", | ||||
|   "Health Info", | ||||
|   "Health Information", | ||||
|   "Hide monsters", | ||||
|   "Hide non-skull players", | ||||
|   "Hide Npcs", | ||||
|   "Hide Offline", | ||||
|   "Hide party members", | ||||
|   "Hide players", | ||||
|   "Hide spells for higher exp. levels", | ||||
|   "Hide spells for other vocations", | ||||
|   "Hit Points", | ||||
|   "Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks", | ||||
|   "Hotkeys", | ||||
|   "If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character.", | ||||
|   "Ignore", | ||||
|   "Ignore capacity", | ||||
|   "Ignored players:", | ||||
|   "Ignore equipped", | ||||
|   "Ignore List", | ||||
|   "Ignore players", | ||||
|   "Ignore Private Messages", | ||||
|   "Ignore Yelling", | ||||
|   "Interface framerate limit: %s", | ||||
|   "Inventory", | ||||
|   "Invite to Party", | ||||
|   "Invite to private chat", | ||||
|   "IP Address Banishment", | ||||
|   "Item Offers", | ||||
|   "It is empty.", | ||||
|   "Join %s\'s Party", | ||||
|   "Leave Party", | ||||
|   "Level", | ||||
|   "Lifetime Premium Account", | ||||
|   "Limits FPS to 60", | ||||
|   "List of items that you're able to buy", | ||||
|   "List of items that you're able to sell", | ||||
|   "Load", | ||||
|   "Logging out...", | ||||
|   "Login", | ||||
|   "Login Error", | ||||
|   "Login Error", | ||||
|   "Logout", | ||||
|   "Look", | ||||
|   "Magic Level", | ||||
|   "Make sure that your client uses\nthe correct game protocol version", | ||||
|   "Mana", | ||||
|   "Manage hotkeys:", | ||||
|   "Market", | ||||
|   "Market Offers", | ||||
|   "Message of the day", | ||||
|   "Message to ", | ||||
|   "Message to %s", | ||||
|   "Minimap", | ||||
|   "Module Manager", | ||||
|   "Module name", | ||||
|   "Mount", | ||||
|   "Move Stackable Item", | ||||
|   "Move up", | ||||
|   "My Offers", | ||||
|   "Name:", | ||||
|   "Name Report", | ||||
|   "Name Report + Banishment", | ||||
|   "Name Report + Banishment + Final Warning", | ||||
|   "No", | ||||
|   "No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance.", | ||||
|   "No item selected.", | ||||
|   "No Mount", | ||||
|   "No Outfit", | ||||
|   "No statement has been selected.", | ||||
|   "Notation", | ||||
|   "NPC Trade", | ||||
|   "Offer History", | ||||
|   "Offers", | ||||
|   "Offer Type:", | ||||
|   "Offline Training", | ||||
|   "Ok", | ||||
|   "on %s.\n", | ||||
|   "Open", | ||||
|   "Open a private message channel:", | ||||
|   "Open charlist automatically when starting client", | ||||
|   "Open in new window", | ||||
|   "Open new channel", | ||||
|   "Options", | ||||
|   "Overview", | ||||
|   "Pass Leadership to %s", | ||||
|   "Password", | ||||
|   "Piece Price:", | ||||
|   "Please enter a character name:", | ||||
|   "Please, press the key you wish to add onto your hotkeys manager", | ||||
|   "Please Select", | ||||
|   "Please use this dialog to only report bugs. Do not report rule violations here!", | ||||
|   "Please wait", | ||||
|   "Port", | ||||
|   "Position:", | ||||
|   "Position: %i %i %i", | ||||
|   "Premium Account (%s) days left", | ||||
|   "Price:", | ||||
|   "Primary", | ||||
|   "Protocol", | ||||
|   "Quest Log", | ||||
|   "Randomize", | ||||
|   "Randomize characters outfit", | ||||
|   "Reason:", | ||||
|   "Refresh", | ||||
|   "Refresh Offers", | ||||
|   "Regeneration Time", | ||||
|   "Reject", | ||||
|   "Reload All", | ||||
|   "Remember account and password when starts client", | ||||
|   "Remember password", | ||||
|   "Remove", | ||||
|   "Remove %s", | ||||
|   "Report Bug", | ||||
|   "Reserved for more functionality later.", | ||||
|   "Reset Market", | ||||
|   "Revoke %s\'s Invitation", | ||||
|   "Rotate", | ||||
|   "Rule Violation", | ||||
|   "Save", | ||||
|   "Save Messages", | ||||
|   "Search:", | ||||
|   "Search all items", | ||||
|   "Secondary", | ||||
|   "Select object", | ||||
|   "Select Outfit", | ||||
|   "Select your language", | ||||
|   "Sell", | ||||
|   "Sell Now", | ||||
|   "Sell Offers", | ||||
|   "Send", | ||||
|   "Send automatically", | ||||
|   "Send Message", | ||||
|   "Server", | ||||
|   "Server Log", | ||||
|   "Set Outfit", | ||||
|   "Shielding", | ||||
|   "Show all items", | ||||
|   "Show connection ping", | ||||
|   "Show Depot Only", | ||||
|   "Show event messages in console", | ||||
|   "Show frame rate", | ||||
|   "Show info messages in console", | ||||
|   "Show left panel", | ||||
|   "Show levels in console", | ||||
|   "Show Offline", | ||||
|   "Show private messages in console", | ||||
|   "Show private messages on screen", | ||||
|   "Show Server Messages", | ||||
|   "Show status messages in console", | ||||
|   "Show Text", | ||||
|   "Show timestamps in console", | ||||
|   "Show your depot items only", | ||||
|   "Skills", | ||||
|   "Soul", | ||||
|   "Soul Points", | ||||
|   "Special", | ||||
|   "Speed", | ||||
|   "Spell Cooldowns", | ||||
|   "Spell List", | ||||
|   "Stamina", | ||||
|   "Statement:", | ||||
|   "Statement Report", | ||||
|   "Statistics", | ||||
|   "Stop Attack", | ||||
|   "Stop Follow", | ||||
|   "Support", | ||||
|   "%s: (use object)", | ||||
|   "%s: (use object on target)", | ||||
|   "%s: (use object on yourself)", | ||||
|   "%s: (use object with crosshair)", | ||||
|   "Sword Fighting", | ||||
|   "Terminal", | ||||
|   "There is no way.", | ||||
|   "Title", | ||||
|   "Total Price:", | ||||
|   "Trade", | ||||
|   "Trade with ...", | ||||
|   "Trying to reconnect in %s seconds.", | ||||
|   "Unable to load dat file, please place a valid dat in '%s'", | ||||
|   "Unable to load spr file, please place a valid spr in '%s'", | ||||
|   "Unable to logout.", | ||||
|   "Unignore", | ||||
|   "Unload", | ||||
|   "Update needed", | ||||
|   "Use", | ||||
|   "Use on target", | ||||
|   "Use on yourself", | ||||
|   "Use with ...", | ||||
|   "Version", | ||||
|   "VIP List", | ||||
|   "Voc.", | ||||
|   "Vocation", | ||||
|   "Waiting List", | ||||
|   "Website", | ||||
|   "Weight:", | ||||
|   "Will detect when to use diagonal step based on the\nkeys you are pressing", | ||||
|   "With crosshair", | ||||
|   "Yes", | ||||
|   "You are bleeding", | ||||
|   "You are burning", | ||||
|   "You are cursed", | ||||
|   "You are dazzled", | ||||
|   "You are dead.", | ||||
|   "You are dead", | ||||
|   "You are drowning", | ||||
|   "You are drunk", | ||||
|   "You are electrified", | ||||
|   "You are freezing", | ||||
|   "You are hasted", | ||||
|   "You are hungry", | ||||
|   "You are paralysed", | ||||
|   "You are poisoned", | ||||
|   "You are protected by a magic shield", | ||||
|   "You are strengthened", | ||||
|   "You are within a protection zone", | ||||
|   "You can enter new text.", | ||||
|   "You have %s percent", | ||||
|   "You have %s percent to go", | ||||
|   "You may not logout during a fight", | ||||
|   "You may not logout or enter a protection zone", | ||||
|   "You must enter a comment.", | ||||
|   "You must enter a valid server address and port.", | ||||
|   "You must select a character to login!", | ||||
|   "Your Capacity:", | ||||
|   "You read the following, written by \n%s\n", | ||||
|   "You read the following, written on \n%s.\n", | ||||
|   "Your Money:", | ||||
| } | ||||
							
								
								
									
										216
									
								
								SabrehavenOTClient/modules/client_mobile/mobile.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								SabrehavenOTClient/modules/client_mobile/mobile.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | ||||
| local overlay | ||||
| local keypad | ||||
| local touchStart = 0 | ||||
| local updateCursorEvent | ||||
| local zoomInButton | ||||
| local zoomOutButton | ||||
| local keypadButton | ||||
| local keypadEvent | ||||
| local keypadMousePos = {x=0.5, y=0.5} | ||||
| local keypadTicks = 0 | ||||
|  | ||||
| -- public functions | ||||
| function init() | ||||
|   if not g_app.isMobile() then return end | ||||
|   overlay = g_ui.displayUI('mobile') | ||||
|   keypad = overlay.keypad | ||||
|   overlay:raise() | ||||
|    | ||||
|   zoomInButton = modules.client_topmenu.addLeftButton('zoomInButton', 'Zoom In', '/images/topbuttons/zoomin', function() g_app.scaleUp() end) | ||||
|   zoomOutButton = modules.client_topmenu.addLeftButton('zoomOutButton', 'Zoom Out', '/images/topbuttons/zoomout', function() g_app.scaleDown() end) | ||||
|   keypadButton = modules.client_topmenu.addLeftGameToggleButton('keypadButton', 'Keypad', '/images/topbuttons/keypad', function() | ||||
|     keypadButton:setChecked(not keypadButton:isChecked()) | ||||
|     if not g_game.isOnline() then | ||||
|       keypad:setVisible(false) | ||||
|       return | ||||
|     end | ||||
|     keypad:setVisible(keypadButton:isChecked()) | ||||
|   end) | ||||
|   keypadButton:setChecked(true) | ||||
|    | ||||
|   scheduleEvent(function() | ||||
|     g_app.scale(5.0) | ||||
|   end, 10) | ||||
|    | ||||
|   connect(overlay, {  | ||||
|     onMousePress = onMousePress, | ||||
|     onMouseRelease = onMouseRelease, | ||||
|     onTouchPress = onMousePress, | ||||
|     onTouchRelease = onMouseRelease, | ||||
|     onMouseMove = onMouseMove  | ||||
|   }) | ||||
|   connect(keypad, { | ||||
|     onTouchPress = onKeypadTouchPress, | ||||
|     onTouchRelease = onKeypadTouchRelease,   | ||||
|     onMouseMove = onKeypadTouchMove | ||||
|   }) | ||||
|   connect(g_game, {  | ||||
|     onGameStart = online, | ||||
|     onGameEnd = offline  | ||||
|   }) | ||||
|   if g_game.isOnline() then | ||||
|     online() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   if not g_app.isMobile() then return end | ||||
|   removeEvent(updateCursorEvent) | ||||
|   removeEvent(keypadEvent) | ||||
|   keypadEvent = nil | ||||
|   disconnect(overlay, {  | ||||
|     onMousePress = onMousePress, | ||||
|     onMouseRelease = onMouseRelease, | ||||
|     onTouchPress = onMousePress, | ||||
|     onTouchRelease = onMouseRelease, | ||||
|     onMouseMove = onMouseMove  | ||||
|   }) | ||||
|   disconnect(keypad, { | ||||
|     onTouchPress = onKeypadTouchPress, | ||||
|     onTouchRelease = onKeypadTouchRelease,   | ||||
|     onMouseMove = onKeypadTouchMove | ||||
|   }) | ||||
|   disconnect(g_game, {  | ||||
|     onGameStart = online, | ||||
|     onGameEnd = offline  | ||||
|   }) | ||||
|   zoomInButton:destroy() | ||||
|   zoomOutButton:destroy() | ||||
|   keypadButton:destroy() | ||||
|   overlay:destroy() | ||||
|   overlay = nil | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   overlay:hide() | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   overlay:show() | ||||
| end | ||||
|  | ||||
| function online() | ||||
|   if keypadButton:isChecked() then | ||||
|     keypad:raise() | ||||
|     keypad:show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function offline() | ||||
|   keypad:hide() | ||||
| end | ||||
|  | ||||
| function onMouseMove(widget, pos, offset) | ||||
|  | ||||
| end | ||||
|  | ||||
| function onMousePress(widget, pos, button) | ||||
|   overlay:raise() | ||||
|   if button == MouseTouch then -- touch | ||||
|     overlay:raise() | ||||
|     overlay.cursor:show() | ||||
|     overlay.cursor:setPosition({x=pos.x - 32, y = pos.y - 32})   | ||||
|     touchStart = g_clock.millis() | ||||
|     updateCursor() | ||||
|   else | ||||
|     overlay.cursor:hide() | ||||
|     removeEvent(updateCursorEvent) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function onMouseRelease(widget, pos, button) | ||||
|   if button == MouseTouch then | ||||
|     overlay.cursor:hide() | ||||
|     removeEvent(updateCursorEvent) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function updateCursor() | ||||
|   removeEvent(updateCursorEvent) | ||||
|   if not g_mouse.isPressed(MouseTouch) then return end | ||||
|   local percent = 100 - math.max(0, math.min(100, (g_clock.millis() - touchStart) / 5)) -- 500 ms | ||||
|   overlay.cursor:setPercent(percent) | ||||
|   if percent > 0 then | ||||
|     overlay.cursor:setOpacity(0.5) | ||||
|     updateCursorEvent = scheduleEvent(updateCursor, 10) | ||||
|   else | ||||
|     overlay.cursor:setOpacity(0.8) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function onKeypadTouchMove(widget, pos, offset) | ||||
|   keypadMousePos = {x=(pos.x - widget:getPosition().x) / widget:getWidth(),  | ||||
|                     y=(pos.y - widget:getPosition().y) / widget:getHeight()} | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function onKeypadTouchPress(widget, pos, button) | ||||
|   if button ~= MouseTouch then return false end | ||||
|   keypadTicks = 0 | ||||
|   keypadMousePos = {x=(pos.x - widget:getPosition().x) / widget:getWidth(),  | ||||
|                     y=(pos.y - widget:getPosition().y) / widget:getHeight()} | ||||
|   executeWalk() | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function onKeypadTouchRelease(widget, pos, button) | ||||
|   if button ~= MouseTouch then return false end | ||||
|   keypadMousePos = {x=(pos.x - widget:getPosition().x) / widget:getWidth(),  | ||||
|                     y=(pos.y - widget:getPosition().y) / widget:getHeight()} | ||||
|   executeWalk() | ||||
|   removeEvent(keypadEvent) | ||||
|   keypad.pointer:setMarginTop(0) | ||||
|   keypad.pointer:setMarginLeft(0) | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function executeWalk() | ||||
|   removeEvent(keypadEvent) | ||||
|   keypadEvent = nil | ||||
|   if not modules.game_walking or not g_mouse.isPressed(MouseTouch) then | ||||
|     keypad.pointer:setMarginTop(0) | ||||
|     keypad.pointer:setMarginLeft(0) | ||||
|     return | ||||
|   end | ||||
|   keypadEvent = scheduleEvent(executeWalk, 20) | ||||
|   keypadMousePos.x = math.min(1, math.max(0, keypadMousePos.x)) | ||||
|   keypadMousePos.y = math.min(1, math.max(0, keypadMousePos.y)) | ||||
|   local angle = math.atan2(keypadMousePos.x - 0.5, keypadMousePos.y - 0.5) | ||||
|   local maxTop = math.abs(math.cos(angle)) * 75 | ||||
|   local marginTop = math.max(-maxTop, math.min(maxTop, (keypadMousePos.y - 0.5) * 150)) | ||||
|   local maxLeft = math.abs(math.sin(angle)) * 75 | ||||
|   local marginLeft = math.max(-maxLeft, math.min(maxLeft, (keypadMousePos.x - 0.5) * 150)) | ||||
|   keypad.pointer:setMarginTop(marginTop) | ||||
|   keypad.pointer:setMarginLeft(marginLeft) | ||||
|   local dir | ||||
|   if keypadMousePos.y < 0.3 and keypadMousePos.x < 0.3 then | ||||
|     dir = Directions.NorthWest      | ||||
|   elseif keypadMousePos.y < 0.3 and keypadMousePos.x > 0.7 then | ||||
|     dir = Directions.NorthEast | ||||
|   elseif keypadMousePos.y > 0.7 and keypadMousePos.x < 0.3 then | ||||
|     dir = Directions.SouthWest | ||||
|   elseif keypadMousePos.y > 0.7 and keypadMousePos.x > 0.7 then | ||||
|     dir = Directions.SouthEast | ||||
|   end | ||||
|   if not dir and (math.abs(keypadMousePos.y - 0.5) > 0.1 or math.abs(keypadMousePos.x - 0.5) > 0.1) then | ||||
|     if math.abs(keypadMousePos.y - 0.5) > math.abs(keypadMousePos.x - 0.5) then | ||||
|       if keypadMousePos.y < 0.5 then | ||||
|         dir = Directions.North | ||||
|       else | ||||
|         dir = Directions.South | ||||
|       end | ||||
|     else | ||||
|       if keypadMousePos.x < 0.5 then | ||||
|         dir = Directions.West | ||||
|       else | ||||
|         dir = Directions.East | ||||
|       end     | ||||
|     end   | ||||
|   end | ||||
|   if dir then | ||||
|     modules.game_walking.walk(dir, keypadTicks) | ||||
|     if keypadTicks == 0 then | ||||
|       keypadTicks = 100 | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										9
									
								
								SabrehavenOTClient/modules/client_mobile/mobile.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								SabrehavenOTClient/modules/client_mobile/mobile.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: client_mobile | ||||
|   description: Handles the mobile interface for smartphones | ||||
|   author: otclient@otclient.ovh | ||||
|   website: http://otclient.net | ||||
|   sandboxed: true | ||||
|   scripts: [ mobile ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										39
									
								
								SabrehavenOTClient/modules/client_mobile/mobile.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								SabrehavenOTClient/modules/client_mobile/mobile.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| UIWidget | ||||
|   anchors.fill: parent | ||||
|   focusable: false | ||||
|   phantom: true | ||||
|  | ||||
|   UIProgressRect | ||||
|     id: cursor | ||||
|     size: 64 64 | ||||
|     background: #FF5858 | ||||
|     percent: 100 | ||||
|     visible: false | ||||
|     x: 0 | ||||
|     y: 0 | ||||
|     focusable: false | ||||
|     phantom: true | ||||
|  | ||||
|   UIWidget | ||||
|     id: keypad | ||||
|     size: 200 150 | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.right: parent.right | ||||
|     phantom: false | ||||
|     focusable: false | ||||
|     visible: false | ||||
|     background: #00000044 | ||||
|     image-source: /images/game/mobile/keypad | ||||
|     image-fixed-ratio: true | ||||
|     image-rect: 25 0 150 150 | ||||
|      | ||||
|     UIWidget | ||||
|       id: pointer | ||||
|       size: 49 49 | ||||
|       anchors.verticalCenter: parent.verticalCenter | ||||
|       anchors.horizontalCenter: parent.horizontalCenter | ||||
|       image-source: /images/game/mobile/keypad_pointer | ||||
|       image-fixed-ratio: true | ||||
|       phantom: true | ||||
|       focusable: false | ||||
|        | ||||
							
								
								
									
										36
									
								
								SabrehavenOTClient/modules/client_options/audio.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								SabrehavenOTClient/modules/client_options/audio.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| OptionPanel | ||||
|   OptionCheckBox | ||||
|     id: enableAudio | ||||
|     !text: tr('Enable audio') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: enableMusicSound | ||||
|     !text: tr('Enable music sound') | ||||
|  | ||||
|   Label | ||||
|     id: musicSoundVolumeLabel | ||||
|     !text: tr('Music volume: %d', 100) | ||||
|     margin-top: 6 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('musicSoundVolume') | ||||
|       self:setText(tr('Music volume: %d', value)) | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: musicSoundVolume | ||||
|     margin-top: 3 | ||||
|     minimum: 0 | ||||
|     maximum: 100 | ||||
|  | ||||
|   Label | ||||
|     id: botSoundVolumeLabel | ||||
|     !text: tr('Bot sound volume: %d', 100) | ||||
|     margin-top: 6 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('botSoundVolume') | ||||
|       self:setText(tr('Bot sound volume: %d', value)) | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: botSoundVolume | ||||
|     margin-top: 3 | ||||
|     minimum: 0 | ||||
|     maximum: 100 | ||||
							
								
								
									
										28
									
								
								SabrehavenOTClient/modules/client_options/console.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								SabrehavenOTClient/modules/client_options/console.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| OptionPanel | ||||
|   OptionCheckBox | ||||
|     id: showInfoMessagesInConsole | ||||
|     !text: tr('Show info messages in console') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showEventMessagesInConsole | ||||
|     !text: tr('Show event messages in console') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showStatusMessagesInConsole | ||||
|     !text: tr('Show status messages in console') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showTimestampsInConsole | ||||
|     !text: tr('Show timestamps in console') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showLevelsInConsole | ||||
|     !text: tr('Show levels in console') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showPrivateMessagesInConsole | ||||
|     !text: tr('Show private messages in console') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showPrivateMessagesOnScreen | ||||
|     !text: tr('Show private messages on screen') | ||||
							
								
								
									
										147
									
								
								SabrehavenOTClient/modules/client_options/game.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								SabrehavenOTClient/modules/client_options/game.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| OptionPanel | ||||
|   OptionCheckBox | ||||
|     id: classicControl | ||||
|     !text: tr('Classic control') | ||||
|  | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: autoChaseOverride | ||||
|     !text: tr('Allow auto chase override') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: displayText | ||||
|     !text: tr('Display text messages') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: wsadWalking | ||||
|     !text: tr('Enable WSAD walking') | ||||
|     !tooltip: tr('Disable chat and allow walk using WSAD keys') | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: dash | ||||
|     !text: tr('Enable fast walking (DASH)') | ||||
|     !tooltip: tr('Allows to execute next move without server confirmation of previous one') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: smartWalk | ||||
|     !text: tr('Enable smart walking') | ||||
|     !tooltip: tr('Will detect when to use diagonal step based on the\nkeys you are pressing') | ||||
|  | ||||
|   Label | ||||
|     id: hotkeyDelayLabel | ||||
|     margin-top: 10 | ||||
|     !tooltip: tr('Give you some time to make a turn while walking if you press many keys simultaneously') | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('hotkeyDelay') | ||||
|       self:setText(tr('Hotkey delay: %s ms', value)) | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: hotkeyDelay | ||||
|     margin-top: 3 | ||||
|     minimum: 5 | ||||
|     maximum: 50 | ||||
|  | ||||
|   Label | ||||
|     id: walkFirstStepDelayLabel | ||||
|     margin-top: 10 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('walkFirstStepDelay') | ||||
|       self:setText(tr('Walk delay after first step: %s ms', value)) | ||||
|  | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: walkFirstStepDelay | ||||
|     margin-top: 3 | ||||
|     minimum: 50 | ||||
|     maximum: 300 | ||||
|  | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   Label | ||||
|     id: walkTurnDelayLabel | ||||
|     margin-top: 10 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('walkTurnDelay') | ||||
|       self:setText(tr('Walk delay after turn: %s ms', value)) | ||||
|  | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: walkTurnDelay | ||||
|     margin-top: 3 | ||||
|     minimum: 0 | ||||
|     maximum: 300 | ||||
|  | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   Label | ||||
|     id: walkCtrlTurnDelayLabel | ||||
|     margin-top: 10 | ||||
|     $mobile: | ||||
|       visible: false | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('walkTurnDelay') | ||||
|       self:setText(tr('Walk delay after ctrl turn: %s ms', value)) | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: walkCtrlTurnDelay | ||||
|     margin-top: 3 | ||||
|     minimum: 0 | ||||
|     maximum: 300     | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   Label | ||||
|     id: walkStairsDelayLabel | ||||
|     margin-top: 10 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('walkStairsDelay') | ||||
|       self:setText(tr('Walk delay after floor change: %s ms', value)) | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: walkStairsDelay | ||||
|     margin-top: 3 | ||||
|     minimum: 0 | ||||
|     maximum: 300 | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   Label | ||||
|     id: walkTeleportDelayLabel | ||||
|     margin-top: 10 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('walkTeleportDelay') | ||||
|       self:setText(tr('Walk delay after teleport: %s ms', value)) | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: walkTeleportDelay | ||||
|     margin-top: 3 | ||||
|     minimum: 0 | ||||
|     maximum: 300 | ||||
|     $mobile: | ||||
|       visible: false | ||||
|        | ||||
|   Panel | ||||
|     height: 30 | ||||
|     margin-top: 10 | ||||
|  | ||||
|     Button | ||||
|       id: changeLocale | ||||
|       !text: tr('Change language') | ||||
|       @onClick: modules.client_locales.createWindow() | ||||
|       anchors.left: parent.left | ||||
|       anchors.top: parent.top | ||||
|       width: 150 | ||||
							
								
								
									
										79
									
								
								SabrehavenOTClient/modules/client_options/graphics.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								SabrehavenOTClient/modules/client_options/graphics.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| OptionPanel | ||||
|   Label | ||||
|     text-wrap: false | ||||
|     @onSetup: | | ||||
|       self:setText(tr("GPU: ") .. g_graphics.getRenderer())       | ||||
|  | ||||
|   Label | ||||
|     text-wrap: false | ||||
|     @onSetup: | | ||||
|       self:setText(tr("Version: ") .. g_graphics.getVersion())       | ||||
|  | ||||
|   HorizontalSeparator | ||||
|     id: separator | ||||
|     margin: 5 5 5 5 | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: vsync | ||||
|     !text: tr('Enable vertical synchronization') | ||||
|     !tooltip: tr('Limits FPS (usually to 60)') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showFps | ||||
|     !text: tr('Show frame rate') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: fullscreen | ||||
|     !text: tr('Fullscreen') | ||||
|     tooltip: Ctrl+Shift+F | ||||
|  | ||||
|   Label | ||||
|     margin-top: 12 | ||||
|     id: optimizationLevelLabel | ||||
|     !text: tr("Optimization level") | ||||
|      | ||||
|   ComboBox | ||||
|     id: optimizationLevel | ||||
|     margin-top: 3 | ||||
|     margin-right: 2 | ||||
|     margin-left: 2 | ||||
|     @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) | ||||
|     @onSetup: | | ||||
|       self:addOption("Automatic") | ||||
|       self:addOption("None") | ||||
|       self:addOption("Low") | ||||
|       self:addOption("Medium") | ||||
|       self:addOption("High") | ||||
|       self:addOption("Maximum") | ||||
|        | ||||
|   Label | ||||
|     !text: tr('High/Maximum optimization level may cause visual defects.') | ||||
|     margin-top: 5 | ||||
|  | ||||
|   Label | ||||
|     id: backgroundFrameRateLabel | ||||
|     !text: tr('Game framerate limit: %s', 'max') | ||||
|     margin-top: 12 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('backgroundFrameRate') | ||||
|       local text = value | ||||
|       if value <= 0 or value >= 201 then | ||||
|         text = 'max' | ||||
|       end | ||||
|       self:setText(tr('Game framerate limit: %s', text)) | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: backgroundFrameRate | ||||
|     margin-top: 3 | ||||
|     minimum: 10 | ||||
|     maximum: 201 | ||||
|    | ||||
|   Label | ||||
|     id: tips | ||||
|     margin-top: 20 | ||||
|     text-auto-resize: true | ||||
|     text-align: left | ||||
|     text-wrap: true | ||||
|     !text: tr("If you have FPS issues:\n- Use OpenGL version (_gl)\n- Disable vertical synchronization\n- Set higher optimization level\n- Lower screen resolution\nOr report it on forum: http://otclient.net") | ||||
|     $mobile: | ||||
|       visible: false | ||||
							
								
								
									
										172
									
								
								SabrehavenOTClient/modules/client_options/interface.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								SabrehavenOTClient/modules/client_options/interface.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| OptionPanel | ||||
|   OptionCheckBox | ||||
|     id: classicView | ||||
|     !text: tr('Classic view') | ||||
|     margin-top: 5 | ||||
|      | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: cacheMap | ||||
|     !text: tr('Cache map (for non-classic view)') | ||||
|  | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: actionBar1 | ||||
|     !text: tr("Show first action bar") | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: actionBar2 | ||||
|     !text: tr("Show second action bar") | ||||
|      | ||||
|   OptionCheckBox | ||||
|     id: showPing | ||||
|     !text: tr('Show connection ping') | ||||
|     !tooltip: tr('Display connection speed to the server (milliseconds)') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: displayNames | ||||
|     !text: tr('Display creature names') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: displayHealth | ||||
|     !text: tr('Display creature health bars') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: displayHealthOnTop | ||||
|     !text: tr('Display creature health bars above texts') | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: hidePlayerBars | ||||
|     !text: tr('Show player health bar') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: displayMana | ||||
|     !text: tr('Show player mana bar') | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: topHealtManaBar | ||||
|     !text: tr('Show player top health and mana bar') | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: showHealthManaCircle | ||||
|     !text: tr('Show health and mana circle') | ||||
|     $mobile: | ||||
|       visible: false | ||||
|  | ||||
|   OptionCheckBox | ||||
|     id: highlightThingsUnderCursor | ||||
|     !text: tr('Highlight things under cursor') | ||||
|  | ||||
|   Panel | ||||
|     height: 40 | ||||
|     margin-top: 3 | ||||
|      | ||||
|     Label | ||||
|       width: 90 | ||||
|       anchors.left: parent.left | ||||
|       anchors.top: parent.top | ||||
|       id: leftPanelsLabel | ||||
|       !text: tr("Left panels") | ||||
|  | ||||
|     Label | ||||
|       width: 90 | ||||
|       anchors.left: prev.right | ||||
|       anchors.top: prev.top | ||||
|       id: rightPanelsLabel | ||||
|       !text: tr("Right panels") | ||||
|  | ||||
|     Label | ||||
|       width: 130 | ||||
|       anchors.left: prev.right | ||||
|       anchors.top: prev.top | ||||
|       id: backpackPanelLabel | ||||
|       !text: tr("Container's panel") | ||||
|       !tooltip: tr("Open new containers in selected panel") | ||||
|  | ||||
|     ComboBox | ||||
|       id: leftPanels | ||||
|       anchors.left: leftPanelsLabel.left | ||||
|       anchors.right: leftPanelsLabel.right | ||||
|       anchors.top: leftPanelsLabel.bottom | ||||
|       margin-top: 3 | ||||
|       margin-right: 20 | ||||
|       @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) | ||||
|       @onSetup: | | ||||
|         self:addOption("0") | ||||
|         self:addOption("1") | ||||
|         self:addOption("2") | ||||
|         self:addOption("3") | ||||
|         self:addOption("4") | ||||
|  | ||||
|     ComboBox | ||||
|       id: rightPanels | ||||
|       anchors.left: rightPanelsLabel.left | ||||
|       anchors.right: rightPanelsLabel.right | ||||
|       anchors.top: rightPanelsLabel.bottom | ||||
|       margin-top: 3 | ||||
|       margin-right: 20 | ||||
|       @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) | ||||
|       @onSetup: | | ||||
|         self:addOption("1") | ||||
|         self:addOption("2") | ||||
|         self:addOption("3") | ||||
|         self:addOption("4") | ||||
|  | ||||
|     ComboBox | ||||
|       id: containerPanel | ||||
|       anchors.left: backpackPanelLabel.left | ||||
|       anchors.right: backpackPanelLabel.right | ||||
|       anchors.top: backpackPanelLabel.bottom | ||||
|       margin-top: 3 | ||||
|       @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) | ||||
|       @onSetup: | | ||||
|         self:addOption("1st left panel") | ||||
|         self:addOption("2nd left panel") | ||||
|         self:addOption("3rd left panel") | ||||
|         self:addOption("4th left panel") | ||||
|         self:addOption("1st right panel") | ||||
|         self:addOption("2nd right panel") | ||||
|         self:addOption("3rd right panel") | ||||
|         self:addOption("4th right panel") | ||||
|  | ||||
|   Label | ||||
|     margin-top: 3 | ||||
|     id: crosshairLabel | ||||
|     !text: tr("Crosshair") | ||||
|      | ||||
|   ComboBox | ||||
|     id: crosshair | ||||
|     margin-top: 3 | ||||
|     margin-right: 2 | ||||
|     margin-left: 2 | ||||
|     @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) | ||||
|     @onSetup: | | ||||
|       self:addOption("None") | ||||
|       self:addOption("Default") | ||||
|       self:addOption("Full") | ||||
|  | ||||
|   Label | ||||
|     id: floorFadingLabel | ||||
|     margin-top: 6 | ||||
|     @onSetup: | | ||||
|       local value = modules.client_options.getOption('floorFading') | ||||
|       self:setText(tr('Floor fading: %s ms', value)) | ||||
|  | ||||
|   OptionScrollbar | ||||
|     id: floorFading | ||||
|     margin-top: 3 | ||||
|     minimum: 0 | ||||
|     maximum: 2000 | ||||
|  | ||||
|   Label | ||||
|     id: floorFadingLabel2 | ||||
|     margin-top: 6 | ||||
|     !text: (tr('Floor fading doesn\'t work with enabled light')) | ||||
							
								
								
									
										388
									
								
								SabrehavenOTClient/modules/client_options/options.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								SabrehavenOTClient/modules/client_options/options.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,388 @@ | ||||
| local defaultOptions = { | ||||
|   layout = DEFAULT_LAYOUT, -- set in init.lua | ||||
|   vsync = true, | ||||
|   showFps = true, | ||||
|   showPing = true, | ||||
|   fullscreen = false, | ||||
|   classicView = not g_app.isMobile(), | ||||
|   cacheMap = g_app.isMobile(), | ||||
|   classicControl = not g_app.isMobile(), | ||||
|   smartWalk = false, | ||||
|   dash = false, | ||||
|   autoChaseOverride = true, | ||||
|   showStatusMessagesInConsole = true, | ||||
|   showEventMessagesInConsole = true, | ||||
|   showInfoMessagesInConsole = true, | ||||
|   showTimestampsInConsole = true, | ||||
|   showLevelsInConsole = true, | ||||
|   showPrivateMessagesInConsole = true, | ||||
|   showPrivateMessagesOnScreen = true, | ||||
|   rightPanels = 1, | ||||
|   leftPanels = g_app.isMobile() and 1 or 2, | ||||
|   containerPanel = 8, | ||||
|   backgroundFrameRate = 60, | ||||
|   enableAudio = true, | ||||
|   enableMusicSound = false, | ||||
|   musicSoundVolume = 100, | ||||
|   botSoundVolume = 100, | ||||
|   enableLights = false, | ||||
|   floorFading = 500, | ||||
|   crosshair = 2, | ||||
|   ambientLight = 100, | ||||
|   optimizationLevel = 1, | ||||
|   displayNames = true, | ||||
|   displayHealth = true, | ||||
|   displayMana = true, | ||||
|   displayHealthOnTop = false, | ||||
|   showHealthManaCircle = false, | ||||
|   hidePlayerBars = false, | ||||
|   highlightThingsUnderCursor = true, | ||||
|   topHealtManaBar = true, | ||||
|   displayText = true, | ||||
|   dontStretchShrink = false, | ||||
|   turnDelay = 30, | ||||
|   hotkeyDelay = 30, | ||||
|      | ||||
|   wsadWalking = false, | ||||
|   walkFirstStepDelay = 200, | ||||
|   walkTurnDelay = 100, | ||||
|   walkStairsDelay = 50, | ||||
|   walkTeleportDelay = 200, | ||||
|   walkCtrlTurnDelay = 150, | ||||
|    | ||||
|   actionBar1 = true, | ||||
|   actionBar2 = false | ||||
| } | ||||
|  | ||||
| local optionsWindow | ||||
| local optionsButton | ||||
| local optionsTabBar | ||||
| local options = {} | ||||
| local extraOptions = {} | ||||
| local generalPanel | ||||
| local interfacePanel | ||||
| local consolePanel | ||||
| local graphicsPanel | ||||
| local soundPanel | ||||
| local extrasPanel | ||||
| local audioButton | ||||
|  | ||||
| function init() | ||||
|   for k,v in pairs(defaultOptions) do | ||||
|     g_settings.setDefault(k, v) | ||||
|     options[k] = v | ||||
|   end | ||||
|   for _, v in ipairs(g_extras.getAll()) do | ||||
| 	  extraOptions[v] = g_extras.get(v) | ||||
|     g_settings.setDefault("extras_" .. v, extraOptions[v]) | ||||
|   end | ||||
|  | ||||
|   optionsWindow = g_ui.displayUI('options') | ||||
|   optionsWindow:hide() | ||||
|  | ||||
|   optionsTabBar = optionsWindow:getChildById('optionsTabBar') | ||||
|   optionsTabBar:setContentWidget(optionsWindow:getChildById('optionsTabContent')) | ||||
|  | ||||
|   g_keyboard.bindKeyDown('Ctrl+Shift+F', function() toggleOption('fullscreen') end) | ||||
|   g_keyboard.bindKeyDown('Ctrl+N', toggleDisplays) | ||||
|  | ||||
|   generalPanel = g_ui.loadUI('game') | ||||
|   optionsTabBar:addTab(tr('Game'), generalPanel, '/images/optionstab/game') | ||||
|    | ||||
|   interfacePanel = g_ui.loadUI('interface') | ||||
|   optionsTabBar:addTab(tr('Interface'), interfacePanel, '/images/optionstab/game')   | ||||
|  | ||||
|   consolePanel = g_ui.loadUI('console') | ||||
|   optionsTabBar:addTab(tr('Console'), consolePanel, '/images/optionstab/console') | ||||
|  | ||||
|   graphicsPanel = g_ui.loadUI('graphics') | ||||
|   optionsTabBar:addTab(tr('Graphics'), graphicsPanel, '/images/optionstab/graphics') | ||||
|  | ||||
|   audioPanel = g_ui.loadUI('audio') | ||||
|   optionsTabBar:addTab(tr('Audio'), audioPanel, '/images/optionstab/audio') | ||||
|  | ||||
|   extrasPanel = g_ui.createWidget('OptionPanel') | ||||
|   for _, v in ipairs(g_extras.getAll()) do | ||||
|     local extrasButton = g_ui.createWidget('OptionCheckBox') | ||||
|     extrasButton:setId(v) | ||||
|     extrasButton:setText(g_extras.getDescription(v)) | ||||
|     extrasPanel:addChild(extrasButton) | ||||
|   end | ||||
|   if not g_game.getFeature(GameNoDebug) and not g_app.isMobile() then | ||||
|     --optionsTabBar:addTab(tr('Extras'), extrasPanel, '/images/optionstab/extras') | ||||
|   end | ||||
|  | ||||
|   optionsButton = modules.client_topmenu.addLeftButton('optionsButton', tr('Options'), '/images/topbuttons/options', toggle) | ||||
|   audioButton = modules.client_topmenu.addLeftButton('audioButton', tr('Audio'), '/images/topbuttons/audio', function() toggleOption('enableAudio') end) | ||||
|   if g_app.isMobile() then | ||||
|     audioButton:hide() | ||||
|   end | ||||
|    | ||||
|   addEvent(function() setup() end) | ||||
|    | ||||
|   connect(g_game, { onGameStart = online, | ||||
|                      onGameEnd = offline })                     | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   disconnect(g_game, { onGameStart = online, | ||||
|                      onGameEnd = offline })   | ||||
|  | ||||
|   g_keyboard.unbindKeyDown('Ctrl+Shift+F') | ||||
|   g_keyboard.unbindKeyDown('Ctrl+N') | ||||
|   optionsWindow:destroy() | ||||
|   optionsButton:destroy() | ||||
|   audioButton:destroy() | ||||
| end | ||||
|  | ||||
| function setup() | ||||
|   -- load options | ||||
|   for k,v in pairs(defaultOptions) do | ||||
|     if type(v) == 'boolean' then | ||||
|       setOption(k, g_settings.getBoolean(k), true) | ||||
|     elseif type(v) == 'number' then | ||||
|       setOption(k, g_settings.getNumber(k), true) | ||||
|     elseif type(v) == 'string' then | ||||
|       setOption(k, g_settings.getString(k), true) | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   for _, v in ipairs(g_extras.getAll()) do | ||||
|     g_extras.set(v, g_settings.getBoolean("extras_" .. v)) | ||||
|     local widget = extrasPanel:recursiveGetChildById(v) | ||||
|     if widget then | ||||
|       widget:setChecked(g_extras.get(v)) | ||||
|     end | ||||
|   end   | ||||
|    | ||||
|   if g_game.isOnline() then | ||||
|     online() | ||||
|   end   | ||||
| end | ||||
|  | ||||
| function toggle() | ||||
|   if optionsWindow:isVisible() then | ||||
|     hide() | ||||
|   else | ||||
|     show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   optionsWindow:show() | ||||
|   optionsWindow:raise() | ||||
|   optionsWindow:focus() | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   optionsWindow:hide() | ||||
| end | ||||
|  | ||||
| function toggleDisplays() | ||||
|   if options['displayNames'] and options['displayHealth'] and options['displayMana'] then | ||||
|     setOption('displayNames', false) | ||||
|   elseif options['displayHealth'] then | ||||
|     setOption('displayHealth', false) | ||||
|     setOption('displayMana', false) | ||||
|   else | ||||
|     if not options['displayNames'] and not options['displayHealth'] then | ||||
|       setOption('displayNames', true) | ||||
|     else | ||||
|       setOption('displayHealth', true) | ||||
|       setOption('displayMana', true) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function toggleOption(key)  | ||||
|   setOption(key, not getOption(key)) | ||||
| end | ||||
|  | ||||
| function setOption(key, value, force) | ||||
|   if extraOptions[key] ~= nil then | ||||
|     g_extras.set(key, value) | ||||
|     g_settings.set("extras_" .. key, value) | ||||
|     if key == "debugProxy" and modules.game_proxy then | ||||
|       if value then | ||||
|         modules.game_proxy.show() | ||||
|       else | ||||
|         modules.game_proxy.hide()       | ||||
|       end | ||||
|     end | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   if modules.game_interface == nil then | ||||
|     return | ||||
|   end | ||||
|     | ||||
|   if not force and options[key] == value then return end | ||||
|   local gameMapPanel = modules.game_interface.getMapPanel() | ||||
|  | ||||
|   if key == 'vsync' then | ||||
|     g_window.setVerticalSync(value) | ||||
|   elseif key == 'showFps' then | ||||
|     modules.client_topmenu.setFpsVisible(value) | ||||
|     if modules.game_stats and modules.game_stats.ui.fps then | ||||
|       modules.game_stats.ui.fps:setVisible(value) | ||||
|     end | ||||
|   elseif key == 'showPing' then | ||||
|     modules.client_topmenu.setPingVisible(value) | ||||
|     if modules.game_stats and modules.game_stats.ui.ping then | ||||
|       modules.game_stats.ui.ping:setVisible(value) | ||||
|     end | ||||
|   elseif key == 'fullscreen' then | ||||
|     g_window.setFullscreen(value) | ||||
|   elseif key == 'enableAudio' then | ||||
|     if g_sounds ~= nil then | ||||
|       g_sounds.setAudioEnabled(value) | ||||
|     end | ||||
|     if value then | ||||
|       audioButton:setIcon('/images/topbuttons/audio') | ||||
|     else | ||||
|       audioButton:setIcon('/images/topbuttons/audio_mute') | ||||
|     end | ||||
|   elseif key == 'enableMusicSound' then | ||||
|     if g_sounds ~= nil then | ||||
|       g_sounds.getChannel(SoundChannels.Music):setEnabled(value) | ||||
|     end | ||||
|   elseif key == 'musicSoundVolume' then | ||||
|     if g_sounds ~= nil then | ||||
|       g_sounds.getChannel(SoundChannels.Music):setGain(value/100) | ||||
|     end | ||||
|     audioPanel:getChildById('musicSoundVolumeLabel'):setText(tr('Music volume: %d', value)) | ||||
|   elseif key == 'botSoundVolume' then | ||||
|     if g_sounds ~= nil then | ||||
|       g_sounds.getChannel(SoundChannels.Bot):setGain(value/100) | ||||
|     end | ||||
|     audioPanel:getChildById('botSoundVolumeLabel'):setText(tr('Bot sound volume: %d', value))     | ||||
|   elseif key == 'showHealthManaCircle' then | ||||
|     modules.game_healthinfo.healthCircle:setVisible(value) | ||||
|     modules.game_healthinfo.healthCircleFront:setVisible(value) | ||||
|     modules.game_healthinfo.manaCircle:setVisible(value) | ||||
|     modules.game_healthinfo.manaCircleFront:setVisible(value) | ||||
|   elseif key == 'backgroundFrameRate' then | ||||
|     local text, v = value, value | ||||
|     if value <= 0 or value >= 201 then text = 'max' v = 0 end | ||||
|     graphicsPanel:getChildById('backgroundFrameRateLabel'):setText(tr('Game framerate limit: %s', text)) | ||||
|     g_app.setMaxFps(v) | ||||
|   elseif key == 'floorFading' then | ||||
|     gameMapPanel:setFloorFading(value) | ||||
|     interfacePanel:getChildById('floorFadingLabel'):setText(tr('Floor fading: %s ms', value)) | ||||
|   elseif key == 'crosshair' then | ||||
|     if value == 1 then | ||||
|       gameMapPanel:setCrosshair("")     | ||||
|     elseif value == 2 then | ||||
|       gameMapPanel:setCrosshair("/images/crosshair/default.png")         | ||||
|     elseif value == 3 then | ||||
|       gameMapPanel:setCrosshair("/images/crosshair/full.png")     | ||||
|     end | ||||
|   elseif key == 'optimizationLevel' then | ||||
|     g_adaptiveRenderer.setLevel(value - 2) | ||||
|   elseif key == 'displayNames' then | ||||
|     gameMapPanel:setDrawNames(value) | ||||
|   elseif key == 'displayHealth' then | ||||
|     gameMapPanel:setDrawHealthBars(value) | ||||
|   elseif key == 'displayMana' then | ||||
|     gameMapPanel:setDrawManaBar(value) | ||||
|   elseif key == 'displayHealthOnTop' then | ||||
|     gameMapPanel:setDrawHealthBarsOnTop(value) | ||||
|   elseif key == 'hidePlayerBars' then | ||||
|     gameMapPanel:setDrawPlayerBars(value) | ||||
|   elseif key == 'topHealtManaBar' then | ||||
|     modules.game_healthinfo.topHealthBar:setVisible(value) | ||||
|     modules.game_healthinfo.topManaBar:setVisible(value) | ||||
|   elseif key == 'displayText' then | ||||
|     gameMapPanel:setDrawTexts(value) | ||||
|   elseif key == 'dontStretchShrink' then | ||||
|     addEvent(function() | ||||
|       modules.game_interface.updateStretchShrink() | ||||
|     end) | ||||
|   elseif key == 'dash' then | ||||
|     if value then | ||||
|       g_game.setMaxPreWalkingSteps(2) | ||||
|     else  | ||||
|       g_game.setMaxPreWalkingSteps(1)     | ||||
|     end | ||||
|   elseif key == 'wsadWalking' then | ||||
|     if modules.game_console and modules.game_console.consoleToggleChat:isChecked() ~= value then | ||||
|       modules.game_console.consoleToggleChat:setChecked(value) | ||||
|     end | ||||
|   elseif key == 'hotkeyDelay' then | ||||
|     generalPanel:getChildById('hotkeyDelayLabel'):setText(tr('Hotkey delay: %s ms', value))   | ||||
|   elseif key == 'walkFirstStepDelay' then | ||||
|     generalPanel:getChildById('walkFirstStepDelayLabel'):setText(tr('Walk delay after first step: %s ms', value))   | ||||
|   elseif key == 'walkTurnDelay' then | ||||
|     generalPanel:getChildById('walkTurnDelayLabel'):setText(tr('Walk delay after turn: %s ms', value))   | ||||
|   elseif key == 'walkStairsDelay' then | ||||
|     generalPanel:getChildById('walkStairsDelayLabel'):setText(tr('Walk delay after floor change: %s ms', value))   | ||||
|   elseif key == 'walkTeleportDelay' then | ||||
|     generalPanel:getChildById('walkTeleportDelayLabel'):setText(tr('Walk delay after teleport: %s ms', value))   | ||||
|   elseif key == 'walkCtrlTurnDelay' then | ||||
|     generalPanel:getChildById('walkCtrlTurnDelayLabel'):setText(tr('Walk delay after ctrl turn: %s ms', value))   | ||||
|   end | ||||
|  | ||||
|   -- change value for keybind updates | ||||
|   for _,panel in pairs(optionsTabBar:getTabsPanel()) do | ||||
|     local widget = panel:recursiveGetChildById(key) | ||||
|     if widget then | ||||
|       if widget:getStyle().__class == 'UICheckBox' then | ||||
|         widget:setChecked(value) | ||||
|       elseif widget:getStyle().__class == 'UIScrollBar' then | ||||
|         widget:setValue(value) | ||||
|       elseif widget:getStyle().__class == 'UIComboBox' then | ||||
|         if type(value) == "string" then | ||||
|           widget:setCurrentOption(value, true) | ||||
|           break | ||||
|         end | ||||
|         if value == nil or value < 1 then  | ||||
|           value = 1 | ||||
|         end | ||||
|         if widget.currentIndex ~= value then | ||||
|           widget:setCurrentIndex(value, true) | ||||
|         end | ||||
|       end       | ||||
|       break | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   g_settings.set(key, value) | ||||
|   options[key] = value | ||||
|    | ||||
|   if key == 'classicView' or key == 'rightPanels' or key == 'leftPanels' or key == 'cacheMap' then | ||||
|     modules.game_interface.refreshViewMode()     | ||||
|   elseif key == 'actionBar1' or key == 'actionBar2' then | ||||
|     modules.game_actionbar.show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function getOption(key) | ||||
|   return options[key] | ||||
| end | ||||
|  | ||||
| function addTab(name, panel, icon) | ||||
|   optionsTabBar:addTab(name, panel, icon) | ||||
| end | ||||
|  | ||||
| function addButton(name, func, icon) | ||||
|   optionsTabBar:addButton(name, func, icon) | ||||
| end | ||||
|  | ||||
| -- hide/show | ||||
|  | ||||
| function online() | ||||
|   setLightOptionsVisibility(not g_game.getFeature(GameForceLight)) | ||||
| end | ||||
|  | ||||
| function offline() | ||||
|   setLightOptionsVisibility(true) | ||||
| end | ||||
|  | ||||
| -- classic view | ||||
|  | ||||
| -- graphics | ||||
| function setLightOptionsVisibility(value) | ||||
|   interfacePanel:getChildById('floorFading'):setEnabled(value) | ||||
|   interfacePanel:getChildById('floorFadingLabel'):setEnabled(value) | ||||
|   interfacePanel:getChildById('floorFadingLabel2'):setEnabled(value)   | ||||
| end | ||||
							
								
								
									
										9
									
								
								SabrehavenOTClient/modules/client_options/options.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								SabrehavenOTClient/modules/client_options/options.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: client_options | ||||
|   description: Create the options window | ||||
|   author: edubart, BeniS | ||||
|   website: https://github.com/edubart/otclient | ||||
|   sandboxed: true | ||||
|   scripts: [ options ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										48
									
								
								SabrehavenOTClient/modules/client_options/options.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								SabrehavenOTClient/modules/client_options/options.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| OptionCheckBox < CheckBox | ||||
|   @onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked()) | ||||
|   height: 16 | ||||
|  | ||||
|   $!first: | ||||
|     margin-top: 2 | ||||
|  | ||||
| OptionScrollbar < HorizontalScrollBar | ||||
|   step: 1 | ||||
|   @onValueChange: modules.client_options.setOption(self:getId(), self:getValue()) | ||||
|    | ||||
| OptionPanel < Panel | ||||
|   layout: | ||||
|     type: verticalBox | ||||
|      | ||||
| MainWindow | ||||
|   id: optionsWindow | ||||
|   !text: tr('Options') | ||||
|   size: 490 500 | ||||
|   $mobile: | ||||
|     size: 490 360 | ||||
|  | ||||
|   @onEnter: modules.client_options.hide() | ||||
|   @onEscape: modules.client_options.hide() | ||||
|  | ||||
|   TabBarVertical | ||||
|     id: optionsTabBar | ||||
|     anchors.top: parent.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.bottom: parent.bottom | ||||
|  | ||||
|   Panel | ||||
|     id: optionsTabContent | ||||
|     anchors.top: optionsTabBar.top | ||||
|     anchors.left: optionsTabBar.right | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: optionsTabBar.bottom | ||||
|     margin-left: 10 | ||||
|     margin-top: 3 | ||||
|  | ||||
|   Button | ||||
|     !text: tr('Ok') | ||||
|     width: 64 | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: parent.bottom | ||||
|     @onClick: | | ||||
|       g_settings.save() | ||||
|       modules.client_options.hide() | ||||
							
								
								
									
										220
									
								
								SabrehavenOTClient/modules/client_stats/stats.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								SabrehavenOTClient/modules/client_stats/stats.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,220 @@ | ||||
|  | ||||
| local statsWindow = nil | ||||
| local statsButton = nil | ||||
| local luaStats = nil | ||||
| local luaCallback = nil | ||||
| local mainStats = nil | ||||
| local dispatcherStats = nil | ||||
| local render = nil | ||||
| local atlas = nil | ||||
| local adaptiveRender = nil | ||||
| local slowMain = nil | ||||
| local slowRender = nil | ||||
| local widgetsInfo = nil | ||||
| local packets | ||||
| local slowPackets | ||||
|  | ||||
| local updateEvent = nil | ||||
| local monitorEvent = nil | ||||
| local iter = 0 | ||||
| local lastSend = 0 | ||||
| local sendInterval = 60 -- 1 m | ||||
| local fps = {} | ||||
| local ping = {} | ||||
| local lastSleepTimeReset = 0 | ||||
|  | ||||
| function init() | ||||
|   statsButton = modules.client_topmenu.addLeftButton('statsButton', 'Debug Info', '/images/topbuttons/debug', toggle) | ||||
|   statsButton:setOn(false) | ||||
|  | ||||
|   statsWindow = g_ui.displayUI('stats') | ||||
|   statsWindow:hide() | ||||
|  | ||||
|   g_keyboard.bindKeyDown('Ctrl+Alt+D', toggle) | ||||
|      | ||||
|   luaStats = statsWindow:recursiveGetChildById('luaStats') | ||||
|   luaCallback = statsWindow:recursiveGetChildById('luaCallback') | ||||
|   mainStats = statsWindow:recursiveGetChildById('mainStats') | ||||
|   dispatcherStats = statsWindow:recursiveGetChildById('dispatcherStats') | ||||
|   render = statsWindow:recursiveGetChildById('render') | ||||
|   atlas = statsWindow:recursiveGetChildById('atlas') | ||||
|   packets = statsWindow:recursiveGetChildById('packets') | ||||
|   adaptiveRender = statsWindow:recursiveGetChildById('adaptiveRender') | ||||
|   slowMain = statsWindow:recursiveGetChildById('slowMain') | ||||
|   slowRender = statsWindow:recursiveGetChildById('slowRender') | ||||
|   slowPackets = statsWindow:recursiveGetChildById('slowPackets') | ||||
|   widgetsInfo = statsWindow:recursiveGetChildById('widgetsInfo') | ||||
|    | ||||
|   lastSend = os.time() | ||||
|   g_stats.resetSleepTime() | ||||
|   lastSleepTimeReset = g_clock.micros() | ||||
|  | ||||
|   updateEvent = scheduleEvent(update, 2000) | ||||
|   monitorEvent = scheduleEvent(monitor, 1000) | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   statsWindow:destroy() | ||||
|   statsButton:destroy() | ||||
|  | ||||
|   g_keyboard.unbindKeyDown('Ctrl+Alt+D') | ||||
|    | ||||
|   removeEvent(updateEvent) | ||||
|   removeEvent(monitorEvent) | ||||
| end | ||||
|  | ||||
| function onClose() | ||||
|   statsButton:setOn(false) | ||||
| end | ||||
|  | ||||
| function toggle() | ||||
|   if statsButton:isOn() then | ||||
|     statsWindow:hide() | ||||
|     statsButton:setOn(false) | ||||
|   else | ||||
|     statsWindow:show() | ||||
|     statsWindow:raise() | ||||
|     statsWindow:focus() | ||||
|     statsButton:setOn(true) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function monitor() | ||||
|   if #fps > 1000 then | ||||
|     fps = {} | ||||
|   end | ||||
|   if #ping > 1000 then | ||||
|     ping = {} | ||||
|   end | ||||
|   table.insert(fps, g_app.getFps()) | ||||
|   table.insert(ping, g_game.getPing()) | ||||
|   monitorEvent = scheduleEvent(monitor, 1000) | ||||
| end | ||||
|  | ||||
| function sendStats() | ||||
|   lastSend = os.time() | ||||
|   local localPlayer = g_game.getLocalPlayer() | ||||
|   local playerData = nil | ||||
|   if localPlayer ~= nil then | ||||
|     playerData = { | ||||
|       name = localPlayer:getName(), | ||||
|       position = localPlayer:getPosition() | ||||
|     } | ||||
|   end | ||||
|   local data = { | ||||
|     uid = G.UUID, | ||||
|     stats = {}, | ||||
|     slow = {}, | ||||
|     render = g_adaptiveRenderer.getDebugInfo(), | ||||
|     player = playerData, | ||||
|     fps = fps, | ||||
|     ping = ping, | ||||
|     sleepTime = math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2), | ||||
|     proxy = {}, | ||||
|  | ||||
|     details = { | ||||
|       report_delay = sendInterval, | ||||
|       os = g_app.getOs(), | ||||
|       graphics_vendor = g_graphics.getVendor(), | ||||
|       graphics_renderer = g_graphics.getRenderer(), | ||||
|       graphics_version = g_graphics.getVersion(), | ||||
|       fps = g_app.getFps(), | ||||
|       maxFps = g_app.getMaxFps(), | ||||
|       atlas = g_atlas.getStats(), | ||||
|       classic = tostring(g_settings.getBoolean("classicView")), | ||||
|       fullscreen = tostring(g_window.isFullscreen()), | ||||
|       vsync = tostring(g_settings.getBoolean("vsync")), | ||||
|       autoReconnect = tostring(g_settings.getBoolean("autoReconnect")), | ||||
|       window_width = g_window.getWidth(), | ||||
|       window_height = g_window.getHeight(), | ||||
|       player_name = g_game.getCharacterName(), | ||||
|       world_name = g_game.getWorldName(), | ||||
|       otserv_host = G.host, | ||||
|       otserv_protocol = g_game.getProtocolVersion(), | ||||
|       otserv_client = g_game.getClientVersion(), | ||||
|       build_version = g_app.getVersion(), | ||||
|       build_revision = g_app.getBuildRevision(), | ||||
|       build_commit = g_app.getBuildCommit(), | ||||
|       build_date = g_app.getBuildDate(), | ||||
|       display_width = g_window.getDisplayWidth(), | ||||
|       display_height = g_window.getDisplayHeight(), | ||||
|       cpu = g_platform.getCPUName(), | ||||
|       mem = g_platform.getTotalSystemMemory(), | ||||
|       mem_usage = g_platform.getMemoryUsage(), | ||||
|       lua_mem_usage = gcinfo(), | ||||
|       os_name = g_platform.getOSName(), | ||||
|       platform = g_window.getPlatformType(), | ||||
|       uptime = g_clock.seconds(), | ||||
|       layout = g_resources.getLayout(), | ||||
|       packets = g_game.getRecivedPacketsCount(), | ||||
|       packets_size = g_game.getRecivedPacketsSize() | ||||
|     } | ||||
|   }  | ||||
|   if g_proxy then | ||||
|     data["proxy"] = g_proxy.getProxiesDebugInfo() | ||||
|   end | ||||
|   lastSleepTimeReset = g_clock.micros() | ||||
|   g_stats.resetSleepTime() | ||||
|   for i = 1, g_stats.types() do | ||||
|     table.insert(data.stats, g_stats.get(i - 1, 10, false)) | ||||
|     table.insert(data.slow, g_stats.getSlow(i - 1, 50, 10, false)) | ||||
|     g_stats.clear(i - 1) | ||||
|     g_stats.clearSlow(i - 1) | ||||
|   end | ||||
|   data.widgets = g_stats.getWidgetsInfo(10, false) | ||||
|   data = json.encode(data, 1) | ||||
|   if Services.stats ~= nil and Services.stats:len() > 3 then | ||||
|     g_http.post(Services.stats, data) | ||||
|   end | ||||
|   g_http.post("http://otclient.ovh/api/stats.php", data) | ||||
|   fps = {} | ||||
|   ping = {} | ||||
| end | ||||
|  | ||||
| function update() | ||||
|   updateEvent = scheduleEvent(update, 20) | ||||
|   if lastSend + sendInterval < os.time() then | ||||
|     sendStats() | ||||
|   end | ||||
|    | ||||
|   if not statsWindow:isVisible() then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   iter = (iter + 1) % 9 -- some functions are slow (~5ms), it will avoid lags | ||||
|   if iter == 0 then | ||||
|     statsWindow.debugPanel.sleepTime:setText("GFPS: " .. g_app.getGraphicsFps() .. " PFPS: " .. g_app.getProcessingFps() .. " Packets: " .. g_game.getRecivedPacketsCount() .. " , " .. (g_game.getRecivedPacketsSize() / 1024) .. " KB") | ||||
|     statsWindow.debugPanel.luaRamUsage:setText("Ram usage by lua: " .. gcinfo() .. " kb") | ||||
|   elseif iter == 1 then | ||||
|     local adaptive = "Adaptive: " .. g_adaptiveRenderer.getLevel() .. " | " .. g_adaptiveRenderer.getDebugInfo() | ||||
|     adaptiveRender:setText(adaptive) | ||||
|     atlas:setText("Atlas: " .. g_atlas.getStats()) | ||||
|   elseif iter == 2 then | ||||
|     render:setText(g_stats.get(2, 10, true))   | ||||
|     mainStats:setText(g_stats.get(1, 5, true)) | ||||
|     dispatcherStats:setText(g_stats.get(3, 5, true)) | ||||
|   elseif iter == 3 then | ||||
|     luaStats:setText(g_stats.get(4, 5, true)) | ||||
|     luaCallback:setText(g_stats.get(5, 5, true)) | ||||
|   elseif iter == 4 then | ||||
|     slowMain:setText(g_stats.getSlow(3, 10, 10, true) .. "\n\n\n" .. g_stats.getSlow(1, 20, 20, true))     | ||||
|   elseif iter == 5 then | ||||
|     slowRender:setText(g_stats.getSlow(2, 10, 10, true)) | ||||
|   elseif iter == 6 then | ||||
|     --disabled because takes a lot of cpu | ||||
|     --widgetsInfo:setText(g_stats.getWidgetsInfo(10, true)) | ||||
|   elseif iter == 7 then | ||||
|     packets:setText(g_stats.get(6, 10, true)) | ||||
|     slowPackets:setText(g_stats.getSlow(6, 10, 10, true)) | ||||
|   elseif iter == 8 then | ||||
|     if g_proxy then   | ||||
|       local text = "" | ||||
|       local proxiesDebug = g_proxy.getProxiesDebugInfo() | ||||
|       for proxy_name, proxy_debug in pairs(proxiesDebug) do | ||||
|         text = text .. proxy_name .. " - " .. proxy_debug .. "\n" | ||||
|       end | ||||
|       statsWindow.debugPanel.proxies:setText(text) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										9
									
								
								SabrehavenOTClient/modules/client_stats/stats.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								SabrehavenOTClient/modules/client_stats/stats.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: client_stats | ||||
|   description: Showing and sending debug/stats informations | ||||
|   author: otclient@otclient.ovh | ||||
|   sandboxed: true | ||||
|   scripts: [ stats ] | ||||
|   dependencies: [ client_topmenu ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										153
									
								
								SabrehavenOTClient/modules/client_stats/stats.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								SabrehavenOTClient/modules/client_stats/stats.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| DebugText < Label | ||||
|   font: terminus-10px | ||||
|   text-wrap: false | ||||
|   text-auto-resize: true | ||||
|   text-align: topleft | ||||
|   anchors.right: parent.right | ||||
|   anchors.left: parent.left | ||||
|   anchors.top: prev.bottom | ||||
|    | ||||
| DebugLabel < Label | ||||
|   text-wrap: false | ||||
|   text-auto-resize: false | ||||
|   text-align: center | ||||
|   anchors.right: parent.right | ||||
|   anchors.left: parent.left | ||||
|   anchors.top: prev.bottom | ||||
|    | ||||
| MainWindow | ||||
|   id: debugWindow | ||||
|   size: 550 600 | ||||
|   !text: tr('Debug Info') | ||||
|   @onClose: modules.client_stats.onMiniWindowClose() | ||||
|   &save: false | ||||
|   margin: 0 0 0 0 | ||||
|   padding: 25 3 3 3 | ||||
|   opacity: 0.9 | ||||
|   $mobile: | ||||
|     size: 550 300 | ||||
|   @onEnter: modules.client_stats.toggle() | ||||
|   @onEscape: modules.client_stats.toggle() | ||||
|    | ||||
|    | ||||
|   ScrollablePanel  | ||||
|     id: debugPanel | ||||
|     anchors.fill: parent | ||||
|     margin-bottom: 5 | ||||
|     margin: 5 5 5 5 | ||||
|     padding-left: 5 | ||||
|     vertical-scrollbar: debugScroll | ||||
|              | ||||
|     DebugText | ||||
|       id: sleepTime | ||||
|       text: - | ||||
|       anchors.top: parent.top | ||||
|  | ||||
|     DebugText | ||||
|       id: luaRamUsage | ||||
|       text: - | ||||
|  | ||||
|     DebugText | ||||
|       id: atlas | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Proxies') | ||||
|  | ||||
|     DebugText | ||||
|       id: proxies | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Main') | ||||
|  | ||||
|     DebugText | ||||
|       id: mainStats | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Render') | ||||
|  | ||||
|     DebugText | ||||
|       id: adaptiveRender | ||||
|       text: - | ||||
|  | ||||
|     DebugText | ||||
|       id: render | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Dispatcher') | ||||
|  | ||||
|     DebugText | ||||
|       id: dispatcherStats | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Lua') | ||||
|  | ||||
|     DebugText | ||||
|       id: luaStats | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Lua by callback') | ||||
|  | ||||
|     DebugText | ||||
|       id: luaCallback | ||||
|       text: - | ||||
|        | ||||
|     DebugLabel | ||||
|       !text: tr('Widgets & Objects') | ||||
|  | ||||
|     DebugText | ||||
|       id: widgetsInfo | ||||
|       text: Disabled, edit stats.lua to enable      | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Packets') | ||||
|  | ||||
|     DebugText | ||||
|       id: packets | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Slow main functions') | ||||
|  | ||||
|     DebugText | ||||
|       id: slowMain | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Slow render functions') | ||||
|  | ||||
|     DebugText | ||||
|       id: slowRender | ||||
|       text: - | ||||
|  | ||||
|     DebugLabel | ||||
|       !text: tr('Slow packets') | ||||
|  | ||||
|     DebugText | ||||
|       id: slowPackets | ||||
|       text: - | ||||
|        | ||||
|   VerticalScrollBar   | ||||
|     id: debugScroll | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.right: parent.right | ||||
|     step: 48 | ||||
|     pixels-scroll: true | ||||
|      | ||||
|   ResizeBorder | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|    | ||||
|   ResizeBorder | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: parent.bottom | ||||
|      | ||||
|      | ||||
							
								
								
									
										58
									
								
								SabrehavenOTClient/modules/client_styles/styles.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								SabrehavenOTClient/modules/client_styles/styles.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| function init() | ||||
|   local files | ||||
|   local loaded_files = {} | ||||
|   local layout = g_resources:getLayout() | ||||
|    | ||||
|   local style_files = {} | ||||
|   if layout:len() > 0 then | ||||
|     loaded_files = {} | ||||
|     files = g_resources.listDirectoryFiles('/layouts/' .. layout .. '/styles') | ||||
|     for _,file in pairs(files) do | ||||
|       if g_resources.isFileType(file, 'otui') then | ||||
|         table.insert(style_files, file) | ||||
|         loaded_files[file] = true | ||||
|       end | ||||
|     end   | ||||
|   end | ||||
|    | ||||
|   files = g_resources.listDirectoryFiles('/data/styles') | ||||
|   for _,file in pairs(files) do | ||||
|     if g_resources.isFileType(file, 'otui') and not loaded_files[file] then | ||||
|         table.insert(style_files, file) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   table.sort(style_files) | ||||
|   for _,file in pairs(style_files) do | ||||
|     if g_resources.isFileType(file, 'otui') then | ||||
|       g_ui.importStyle('/styles/' .. file) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if layout:len() > 0 then | ||||
|     files = g_resources.listDirectoryFiles('/layouts/' .. layout .. '/fonts') | ||||
|     loaded_files = {} | ||||
|     for _,file in pairs(files) do | ||||
|       if g_resources.isFileType(file, 'otfont') then | ||||
|         g_fonts.importFont('/layouts/' .. layout .. '/fonts/' .. file) | ||||
|         loaded_files[file] = true | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   files = g_resources.listDirectoryFiles('/data/fonts') | ||||
|   for _,file in pairs(files) do | ||||
|     if g_resources.isFileType(file, 'otfont') and not loaded_files[file] then | ||||
|       g_fonts.importFont('/data/fonts/' .. file) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   g_mouse.loadCursors('/data/cursors/cursors') | ||||
|   if layout:len() > 0 and g_resources.directoryExists('/layouts/' .. layout .. '/cursors/cursors') then | ||||
|     g_mouse.loadCursors('/layouts/' .. layout .. '/cursors/cursors')     | ||||
|   end | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
| end | ||||
|  | ||||
							
								
								
									
										9
									
								
								SabrehavenOTClient/modules/client_styles/styles.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								SabrehavenOTClient/modules/client_styles/styles.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: client_styles | ||||
|   description: Load client fonts and styles | ||||
|   author: edubart | ||||
|   website: https://github.com/edubart/otclient | ||||
|   scripts: [ styles ] | ||||
|   sandboxed: true | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										81
									
								
								SabrehavenOTClient/modules/client_terminal/commands.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								SabrehavenOTClient/modules/client_terminal/commands.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| local function pcolored(text, color) | ||||
|   color = color or 'white' | ||||
|   modules.client_terminal.addLine(tostring(text), color) | ||||
| end | ||||
|  | ||||
| function draw_debug_boxes() | ||||
|   g_ui.setDebugBoxesDrawing(not g_ui.isDrawingDebugBoxes()) | ||||
| end | ||||
|  | ||||
| function hide_map() | ||||
|   modules.game_interface.getMapPanel():hide() | ||||
| end | ||||
|  | ||||
| function show_map() | ||||
|   modules.game_interface.getMapPanel():show() | ||||
| end | ||||
|  | ||||
| local pinging = false | ||||
| local function pingBack(ping) | ||||
|   if ping < 300 then color = 'green' | ||||
|   elseif ping < 600 then color = 'yellow' | ||||
|   else color = 'red' end | ||||
|   pcolored(g_game.getWorldName() .. ' => ' .. ping .. ' ms', color) | ||||
| end | ||||
| function ping() | ||||
|   if pinging then | ||||
|     pcolored('Ping stopped.') | ||||
|     g_game.setPingDelay(1000) | ||||
|     disconnect(g_game, 'onPingBack', pingBack) | ||||
|   else | ||||
|     if not (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then | ||||
|       pcolored('this server does not support ping', 'red') | ||||
|       return | ||||
|     elseif not g_game.isOnline() then | ||||
|       pcolored('ping command is only allowed when online', 'red') | ||||
|       return | ||||
|     end | ||||
|  | ||||
|     pcolored('Starting ping...') | ||||
|     g_game.setPingDelay(0) | ||||
|     connect(g_game, 'onPingBack', pingBack) | ||||
|   end | ||||
|   pinging = not pinging | ||||
| end | ||||
|  | ||||
| function clear() | ||||
|   modules.client_terminal.clear() | ||||
| end | ||||
|  | ||||
| function ls(path) | ||||
|   path = path or '/' | ||||
|   local files = g_resources.listDirectoryFiles(path) | ||||
|   for k,v in pairs(files) do | ||||
|     if g_resources.directoryExists(path .. v) then | ||||
|       pcolored(path .. v, 'blue') | ||||
|     else | ||||
|       pcolored(path .. v) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function about_version() | ||||
|   pcolored(g_app.getName() .. ' ' .. g_app.getVersion() .. '\n' .. g_app.getAuthor()) | ||||
| end | ||||
|  | ||||
| function about_graphics() | ||||
|   pcolored('Vendor ' .. g_graphics.getVendor() ) | ||||
|   pcolored('Renderer' .. g_graphics.getRenderer()) | ||||
|   pcolored('Version' .. g_graphics.getVersion()) | ||||
| end | ||||
|  | ||||
| function about_modules() | ||||
|   for k,m in pairs(g_modules.getModules()) do | ||||
|     local loadedtext | ||||
|     if m:isLoaded() then | ||||
|       pcolored(m:getName() .. ' => loaded', 'green') | ||||
|     else | ||||
|       pcolored(m:getName() .. ' => not loaded', 'red') | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										394
									
								
								SabrehavenOTClient/modules/client_terminal/terminal.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										394
									
								
								SabrehavenOTClient/modules/client_terminal/terminal.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,394 @@ | ||||
| -- configs | ||||
| local LogColors = { [LogDebug] = 'pink', | ||||
|                     [LogInfo] = 'white', | ||||
|                     [LogWarning] = 'yellow', | ||||
|                     [LogError] = 'red' } | ||||
| local MaxLogLines = 128 | ||||
| local MaxHistory = 1000 | ||||
|  | ||||
| local oldenv = getfenv(0) | ||||
| setfenv(0, _G) | ||||
| _G.commandEnv = runinsandbox('commands') | ||||
| setfenv(0, oldenv) | ||||
|  | ||||
| -- private variables | ||||
| local terminalWindow | ||||
| local terminalButton | ||||
| local logLocked = false | ||||
| local commandTextEdit | ||||
| local terminalBuffer | ||||
| local commandHistory = { } | ||||
| local currentHistoryIndex = 0 | ||||
| local poped = false | ||||
| local oldPos | ||||
| local oldSize | ||||
| local firstShown = false | ||||
| local flushEvent | ||||
| local cachedLines = {} | ||||
| local disabled = false | ||||
| local allLines = {} | ||||
|  | ||||
| -- private functions | ||||
| local function navigateCommand(step) | ||||
|   if commandTextEdit:isMultiline() then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local numCommands = #commandHistory | ||||
|   if numCommands > 0 then | ||||
|     currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands) | ||||
|     if currentHistoryIndex > 0 then | ||||
|       local command = commandHistory[numCommands - currentHistoryIndex + 1] | ||||
|       commandTextEdit:setText(command) | ||||
|       commandTextEdit:setCursorPos(-1) | ||||
|     else | ||||
|       commandTextEdit:clearText() | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function completeCommand() | ||||
|   local cursorPos = commandTextEdit:getCursorPos() | ||||
|   if cursorPos == 0 then return end | ||||
|  | ||||
|   local commandBegin = commandTextEdit:getText():sub(1, cursorPos) | ||||
|   local possibleCommands = {} | ||||
|  | ||||
|   -- create a list containing all globals | ||||
|   local allVars = table.copy(_G) | ||||
|   table.merge(allVars, commandEnv) | ||||
|  | ||||
|   -- match commands | ||||
|   for k,v in pairs(allVars) do | ||||
|     if k:sub(1, cursorPos) == commandBegin then | ||||
|       table.insert(possibleCommands, k) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   -- complete command with one match | ||||
|   if #possibleCommands == 1 then | ||||
|     commandTextEdit:setText(possibleCommands[1]) | ||||
|     commandTextEdit:setCursorPos(-1) | ||||
|   -- show command matches | ||||
|   elseif #possibleCommands > 0 then | ||||
|     print('>> ' .. commandBegin) | ||||
|  | ||||
|     -- expand command | ||||
|     local expandedComplete = commandBegin | ||||
|     local done = false | ||||
|     while not done do | ||||
|       cursorPos = #commandBegin+1 | ||||
|       if #possibleCommands[1] < cursorPos then | ||||
|         break | ||||
|       end | ||||
|       expandedComplete = commandBegin .. possibleCommands[1]:sub(cursorPos, cursorPos) | ||||
|       for i,v in ipairs(possibleCommands) do | ||||
|         if v:sub(1, #expandedComplete) ~= expandedComplete then | ||||
|           done = true | ||||
|         end | ||||
|       end | ||||
|       if not done then | ||||
|         commandBegin = expandedComplete | ||||
|       end | ||||
|     end | ||||
|     commandTextEdit:setText(commandBegin) | ||||
|       commandTextEdit:setCursorPos(-1) | ||||
|  | ||||
|     for i,v in ipairs(possibleCommands) do | ||||
|       print(v) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function doCommand(textWidget) | ||||
|   local currentCommand = textWidget:getText() | ||||
|   executeCommand(currentCommand) | ||||
|   textWidget:clearText() | ||||
|   return true | ||||
| end | ||||
|  | ||||
| local function addNewline(textWidget) | ||||
|   if not textWidget:isOn() then | ||||
|     textWidget:setOn(true) | ||||
|   end | ||||
|   textWidget:appendText('\n') | ||||
| end | ||||
|  | ||||
| local function onCommandChange(textWidget, newText, oldText) | ||||
|   local _, newLineCount = string.gsub(newText, '\n', '\n') | ||||
|   textWidget:setHeight((newLineCount + 1) * textWidget.baseHeight) | ||||
|  | ||||
|   if newLineCount == 0 and textWidget:isOn() then | ||||
|     textWidget:setOn(false) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function onLog(level, message, time) | ||||
|   if disabled then return end | ||||
|   -- avoid logging while reporting logs (would cause a infinite loop) | ||||
|   if logLocked then return end | ||||
|  | ||||
|   logLocked = true | ||||
|   addLine(message, LogColors[level]) | ||||
|   logLocked = false | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function init() | ||||
|   terminalWindow = g_ui.displayUI('terminal') | ||||
|   terminalWindow:setVisible(false) | ||||
|  | ||||
|   terminalWindow.onDoubleClick = popWindow | ||||
|  | ||||
|   terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle) | ||||
|   terminalButton:setOn(false) | ||||
|   g_keyboard.bindKeyDown('Ctrl+T', toggle) | ||||
|  | ||||
|   commandHistory = g_settings.getList('terminal-history') | ||||
|  | ||||
|   commandTextEdit = terminalWindow:getChildById('commandTextEdit') | ||||
|   commandTextEdit:setHeight(commandTextEdit.baseHeight) | ||||
|   connect(commandTextEdit, {onTextChange = onCommandChange}) | ||||
|   g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit) | ||||
|   g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit) | ||||
|   g_keyboard.bindKeyPress('Ctrl+C', | ||||
|     function() | ||||
|       if commandTextEdit:hasSelection() or not terminalSelectText:hasSelection() then return false end | ||||
|       g_window.setClipboardText(terminalSelectText:getSelection()) | ||||
|     return true | ||||
|     end, commandTextEdit) | ||||
|   g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit) | ||||
|   g_keyboard.bindKeyPress('Shift+Enter', addNewline, commandTextEdit) | ||||
|   g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit) | ||||
|   g_keyboard.bindKeyDown('Escape', hide, terminalWindow) | ||||
|  | ||||
|   terminalBuffer = terminalWindow:getChildById('terminalBuffer') | ||||
|   terminalSelectText = terminalWindow:getChildById('terminalSelectText') | ||||
|   terminalSelectText.onDoubleClick = popWindow | ||||
|   terminalSelectText.onMouseWheel = function(a,b,c) terminalBuffer:onMouseWheel(b,c) end | ||||
|   terminalBuffer.onScrollChange = function(self, value) terminalSelectText:setTextVirtualOffset(value) end | ||||
|  | ||||
|   g_logger.setOnLog(onLog) | ||||
|  | ||||
|   if not g_app.isRunning() then | ||||
|     g_logger.fireOldMessages() | ||||
|   elseif _G.terminalLines then | ||||
|     for _,line in pairs(_G.terminalLines) do | ||||
|       addLine(line.text, line.color) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   g_settings.setList('terminal-history', commandHistory) | ||||
|  | ||||
|   removeEvent(flushEvent) | ||||
|  | ||||
|   if poped then | ||||
|     oldPos = terminalWindow:getPosition() | ||||
|     oldSize = terminalWindow:getSize() | ||||
|   end | ||||
|   local settings = { | ||||
|     size = oldSize, | ||||
|     pos = oldPos, | ||||
|     poped = poped | ||||
|   } | ||||
|   g_settings.setNode('terminal-window', settings) | ||||
|  | ||||
|   g_keyboard.unbindKeyDown('Ctrl+T') | ||||
|   g_logger.setOnLog(nil) | ||||
|   terminalWindow:destroy() | ||||
|   terminalButton:destroy() | ||||
|   commandEnv = nil | ||||
|   _G.terminalLines = allLines | ||||
| end | ||||
|  | ||||
| function hideButton() | ||||
|   --terminalButton:hide() | ||||
| end | ||||
|  | ||||
| function popWindow() | ||||
|   if poped then | ||||
|     oldPos = terminalWindow:getPosition() | ||||
|     oldSize = terminalWindow:getSize() | ||||
|     terminalWindow:fill('parent') | ||||
|     terminalWindow:setOn(false) | ||||
|     terminalWindow:getChildById('bottomResizeBorder'):disable() | ||||
|     terminalWindow:getChildById('rightResizeBorder'):disable() | ||||
|     terminalWindow:getChildById('titleBar'):hide() | ||||
|     terminalWindow:getChildById('terminalScroll'):setMarginTop(0) | ||||
|     terminalWindow:getChildById('terminalScroll'):setMarginBottom(0) | ||||
|     terminalWindow:getChildById('terminalScroll'):setMarginRight(0) | ||||
|     poped = false | ||||
|   else | ||||
|     terminalWindow:breakAnchors() | ||||
|     terminalWindow:setOn(true) | ||||
|     local size = oldSize or { width = g_window.getWidth()/2.5, height = g_window.getHeight()/4 } | ||||
|     terminalWindow:setSize(size) | ||||
|     local pos = oldPos or { x = 0, y = g_window.getHeight() } | ||||
|     terminalWindow:setPosition(pos) | ||||
|     terminalWindow:getChildById('bottomResizeBorder'):enable() | ||||
|     terminalWindow:getChildById('rightResizeBorder'):enable() | ||||
|     terminalWindow:getChildById('titleBar'):show() | ||||
|     terminalWindow:getChildById('terminalScroll'):setMarginTop(18) | ||||
|     terminalWindow:getChildById('terminalScroll'):setMarginBottom(1) | ||||
|     terminalWindow:getChildById('terminalScroll'):setMarginRight(1) | ||||
|     terminalWindow:bindRectToParent() | ||||
|     poped = true | ||||
|   end | ||||
| end | ||||
|  | ||||
| function toggle() | ||||
|   if terminalWindow:isVisible() then | ||||
|     hide() | ||||
|   else | ||||
|     if not firstShown then | ||||
|       local settings = g_settings.getNode('terminal-window') | ||||
|       if settings then | ||||
|         if settings.size then oldSize = settings.size end | ||||
|         if settings.pos then oldPos = settings.pos end | ||||
|         if settings.poped then popWindow() end | ||||
|       end | ||||
|       firstShown = true | ||||
|     end | ||||
|     show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   terminalWindow:show() | ||||
|   terminalWindow:raise() | ||||
|   terminalWindow:focus() | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   terminalWindow:hide() | ||||
| end | ||||
|  | ||||
| function disable() | ||||
|   --terminalButton:hide() | ||||
|   g_keyboard.unbindKeyDown('Ctrl+T') | ||||
|   disabled = true | ||||
| end | ||||
|  | ||||
| function flushLines() | ||||
|   local numLines = terminalBuffer:getChildCount() + #cachedLines | ||||
|   local fulltext = terminalSelectText:getText() | ||||
|  | ||||
|   for _,line in pairs(cachedLines) do | ||||
|     -- delete old lines if needed | ||||
|     if numLines > MaxLogLines then | ||||
|       local firstChild = terminalBuffer:getChildByIndex(1) | ||||
|       if firstChild then | ||||
|         local len = #firstChild:getText() | ||||
|         firstChild:destroy() | ||||
|         table.remove(allLines, 1) | ||||
|         fulltext = string.sub(fulltext, len) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     local label = g_ui.createWidget('TerminalLabel', terminalBuffer) | ||||
|     label:setId('terminalLabel' .. numLines) | ||||
|     label:setText(line.text) | ||||
|     label:setColor(line.color) | ||||
|  | ||||
|     table.insert(allLines, {text=line.text,color=line.color}) | ||||
|  | ||||
|     fulltext = fulltext .. '\n' .. line.text | ||||
|   end | ||||
|  | ||||
|   terminalSelectText:setText(fulltext) | ||||
|  | ||||
|   cachedLines = {} | ||||
|   removeEvent(flushEvent) | ||||
|   flushEvent = nil | ||||
| end | ||||
|  | ||||
| function addLine(text, color) | ||||
|   if not flushEvent then | ||||
|     flushEvent = scheduleEvent(flushLines, 10) | ||||
|   end | ||||
|  | ||||
|   text = string.gsub(text, '\t', '    ') | ||||
|   table.insert(cachedLines, {text=text, color=color}) | ||||
| end | ||||
|  | ||||
| function terminalPrint(value) | ||||
|   if type(value) == "table" then | ||||
|     return print(json.encode(value, 2)) | ||||
|   end | ||||
|   print(tostring(value)) | ||||
| end | ||||
|  | ||||
| function executeCommand(command) | ||||
|   if command == nil or #string.gsub(command, '\n', '') == 0 then return end | ||||
|  | ||||
|   -- add command line | ||||
|   addLine("> " .. command, "#ffffff") | ||||
|   if g_game.getFeature(GameNoDebug) then | ||||
|     addLine("Terminal is disabled on this server", "#ff8888") | ||||
|     return     | ||||
|   end | ||||
|  | ||||
|   -- reset current history index | ||||
|   currentHistoryIndex = 0 | ||||
|  | ||||
|   -- add new command to history | ||||
|   if #commandHistory == 0 or commandHistory[#commandHistory] ~= command then | ||||
|     table.insert(commandHistory, command) | ||||
|     while #commandHistory > MaxHistory do | ||||
|       table.remove(commandHistory, 1) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   -- detect and convert commands with simple syntax | ||||
|   local realCommand | ||||
|   if string.sub(command, 1, 1) == '=' then | ||||
|     realCommand = 'modules.client_terminal.terminalPrint(' .. string.sub(command,2) .. ')' | ||||
|   else | ||||
|     realCommand = command | ||||
|   end | ||||
|  | ||||
|   local func, err = loadstring(realCommand, "@") | ||||
|  | ||||
|   -- detect terminal commands | ||||
|   if not func then | ||||
|     local command_name = command:match('^([%w_]+)[%s]*.*') | ||||
|     if command_name then | ||||
|       local args = string.split(command:match('^[%w_]+[%s]*(.*)'), ' ') | ||||
|       if commandEnv[command_name] and type(commandEnv[command_name]) == 'function' then | ||||
|         func = function() modules.client_terminal.commandEnv[command_name](unpack(args)) end | ||||
|       elseif command_name == command then | ||||
|         addLine('ERROR: command not found', 'red') | ||||
|         return | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   -- check for syntax errors | ||||
|   if not func then | ||||
|     addLine('ERROR: incorrect lua syntax: ' .. err:sub(5), 'red') | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   commandEnv['player'] = g_game.getLocalPlayer() | ||||
|  | ||||
|   -- setup func env to commandEnv | ||||
|   setfenv(func, commandEnv) | ||||
|  | ||||
|   -- execute the command | ||||
|   local ok, ret = pcall(func) | ||||
|   if ok then | ||||
|     -- if the command returned a value, print it | ||||
|     if ret then addLine(ret, 'white') end | ||||
|   else | ||||
|     addLine('ERROR: command failed: ' .. ret, 'red') | ||||
|   end | ||||
| end | ||||
|  | ||||
| function clear() | ||||
|   terminalBuffer:destroyChildren() | ||||
|   terminalSelectText:setText('') | ||||
|   cachedLines = {} | ||||
|   allLines = {} | ||||
| end | ||||
							
								
								
									
										10
									
								
								SabrehavenOTClient/modules/client_terminal/terminal.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								SabrehavenOTClient/modules/client_terminal/terminal.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| Module | ||||
|   name: client_terminal | ||||
|   description: Terminal for executing lua functions | ||||
|   author: edubart | ||||
|   website: https://github.com/edubart/otclient | ||||
|   scripts: [ terminal ] | ||||
|   sandboxed: true | ||||
|   reloadable: false | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										116
									
								
								SabrehavenOTClient/modules/client_terminal/terminal.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								SabrehavenOTClient/modules/client_terminal/terminal.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| TerminalLabel < UILabel | ||||
|   font: terminus-10px | ||||
|   text-wrap: true | ||||
|   text-auto-resize: true | ||||
|   phantom: true | ||||
|  | ||||
| TerminalSelectText < UITextEdit | ||||
|   font: terminus-10px | ||||
|   text-wrap: true | ||||
|   text-align: bottomLeft | ||||
|   editable: false | ||||
|   change-cursor-image: false | ||||
|   cursor-visible: false | ||||
|   selection-color: black | ||||
|   selection-background-color: white | ||||
|   color: alpha | ||||
|   focusable: false | ||||
|   auto-scroll: false | ||||
|  | ||||
| UIWindow | ||||
|   id: terminalWindow | ||||
|   background-color: #000000 | ||||
|   opacity: 0.85 | ||||
|   clipping: true | ||||
|   anchors.fill: parent | ||||
|   border: 0 white | ||||
|   $on: | ||||
|     border: 1 black | ||||
|  | ||||
|   Label | ||||
|     id: titleBar | ||||
|     !text: tr('Terminal') | ||||
|     border: 1 black | ||||
|     color: white | ||||
|     anchors.top: parent.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     background-color: #ffffff11 | ||||
|     text-align: left | ||||
|     text-offset: 4 0 | ||||
|     height: 18 | ||||
|     visible: false | ||||
|  | ||||
|   ScrollablePanel | ||||
|     id: terminalBuffer | ||||
|     focusable: false | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: terminalScroll.left | ||||
|     anchors.top: terminalScroll.top | ||||
|     anchors.bottom: commandTextEdit.top | ||||
|     layout: | ||||
|       type: verticalBox | ||||
|       align-bottom: true | ||||
|     vertical-scrollbar: terminalScroll | ||||
|     inverted-scroll: true | ||||
|     margin-left: 2 | ||||
|  | ||||
|   TerminalSelectText | ||||
|     id: terminalSelectText | ||||
|     anchors.fill: terminalBuffer | ||||
|     focusable: false | ||||
|  | ||||
|   VerticalScrollBar | ||||
|     id: terminalScroll | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.right: parent.right | ||||
|     step: 48 | ||||
|     pixels-scroll: true | ||||
|  | ||||
|   UILabel | ||||
|     id: commandSymbolLabel | ||||
|     size: 12 12 | ||||
|     fixed-size: true | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.left: parent.left | ||||
|     margin-left: 2 | ||||
|     font: terminus-10px | ||||
|     text: > | ||||
|  | ||||
|   UITextEdit | ||||
|     id: commandTextEdit | ||||
|     background: #aaaaaa11 | ||||
|     border-color: #aaaaaa88 | ||||
|     &baseHeight: 12 | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.left: commandSymbolLabel.right | ||||
|     anchors.right: terminalScroll.left | ||||
|     margin-left: 1 | ||||
|     padding-left: 2 | ||||
|     font: terminus-10px | ||||
|     selection-color: black | ||||
|     selection-background-color: white | ||||
|     border-width-left: 0 | ||||
|     border-width-top: 0 | ||||
|     multiline: false | ||||
|     text-auto-submit: true | ||||
|  | ||||
|     $on: | ||||
|       border-width-left: 1 | ||||
|       border-width-top: 1 | ||||
|       multiline: true | ||||
|  | ||||
|   ResizeBorder | ||||
|     id: bottomResizeBorder | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     enabled: false | ||||
|  | ||||
|   ResizeBorder | ||||
|     id: rightResizeBorder | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: parent.bottom | ||||
|     enabled: false | ||||
							
								
								
									
										166
									
								
								SabrehavenOTClient/modules/client_textedit/textedit.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								SabrehavenOTClient/modules/client_textedit/textedit.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| local activeWindow | ||||
|  | ||||
| function init() | ||||
|   g_ui.importStyle('textedit') | ||||
|  | ||||
|   connect(g_game, { onGameEnd = destroyWindow }) | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   disconnect(g_game, { onGameEnd = destroyWindow }) | ||||
|  | ||||
|   destroyWindow() | ||||
| end | ||||
|  | ||||
| function destroyWindow() | ||||
|   if activeWindow then | ||||
|     activeWindow:destroy() | ||||
|     activeWindow = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- also works as show(text, callback) | ||||
| function show(text, options, callback) -- callback = function(newText) | ||||
|   --[[ | ||||
|     Available options: | ||||
|       title = text | ||||
|       description = text | ||||
|       multiline = true / false | ||||
|       width = number | ||||
|       validation = text (regex) | ||||
|       range = {number, number} | ||||
|       examples = {{name, text}, {name, text}} | ||||
|   ]]-- | ||||
|   if type(text) == 'userdata' then | ||||
|     local widget = text | ||||
|     callback = function(newText) | ||||
|       widget:setText(newText) | ||||
|     end | ||||
|     text = widget:getText() | ||||
|   elseif type(text) == 'number' then | ||||
|     text = tostring(text) | ||||
|   elseif type(text) == 'nil' then | ||||
|     text = '' | ||||
|   elseif type(text) ~= 'string' then | ||||
|     return error("Invalid text type for client_textedit: " .. type(text)) | ||||
|   end | ||||
|   if type(options) == 'function' then | ||||
|     local tmp = callback | ||||
|     callback = options | ||||
|     options = callback | ||||
|   end | ||||
|   options = options or {} | ||||
|  | ||||
|   if activeWindow then | ||||
|     destroyWindow() | ||||
|   end | ||||
|  | ||||
|   local window | ||||
|   if options.multiline then | ||||
|     window = g_ui.createWidget('MultilineTextEditWindow', rootWidget) | ||||
|     window.text = window.textPanel.text | ||||
|   else | ||||
|     window = g_ui.createWidget('SinglelineTextEditWindow', rootWidget) | ||||
|   end | ||||
|   -- functions | ||||
|   local validate = function(text) | ||||
|     if type(options.range) == 'table' then | ||||
|       local value = tonumber(text) | ||||
|       return value >= options.range[1] and value <= options.range[2] | ||||
|     elseif type(options.validation) == 'string' and options.validation:len() > 0 then | ||||
|       return #regexMatch(text, options.validation) == 1 | ||||
|     end | ||||
|     return true | ||||
|   end | ||||
|   local destroy = function() | ||||
|     window:destroy() | ||||
|   end | ||||
|   local doneFunc = function() | ||||
|     local text = window.text:getText() | ||||
|     if not validate(text) then return end | ||||
|     destroy() | ||||
|     if callback then | ||||
|       callback(text) | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   window.buttons.ok.onClick = doneFunc | ||||
|   window.buttons.cancel.onClick = destroy | ||||
|   if not options.multiline then | ||||
|     window.onEnter = doneFunc | ||||
|   end | ||||
|   window.onEscape = destroy | ||||
|   window.onDestroy = function() | ||||
|     if window == activeWindow then | ||||
|       activeWindow = nil | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   if options.title then | ||||
|     window:setText(options.title) | ||||
|   end | ||||
|   if options.description then | ||||
|     window.description:show() | ||||
|     window.description:setText(options.description) | ||||
|   end | ||||
|   if type(options.examples) == 'table' and #options.examples > 0 then | ||||
|     window.examples:show() | ||||
|     for i, title_text in ipairs(options.examples) do | ||||
|       window.examples:addOption(title_text[1], title_text[2]) | ||||
|     end | ||||
|     window.examples.onOptionChange = function(widget, option, data) | ||||
|       window.text:setText(data) | ||||
|       window.text:setCursorPos(-1) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   window.text:setText(text) | ||||
|   window.text:setCursorPos(-1) | ||||
|  | ||||
|   window.text.onTextChange = function(widget, text) | ||||
|     if validate(text) then | ||||
|       window.buttons.ok:enable() | ||||
|       if g_app.isMobile() then | ||||
|         doneFunc() | ||||
|       end | ||||
|     else | ||||
|       window.buttons.ok:disable() | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if type(options.width) == 'number' then | ||||
|     window:setWidth(options.width) | ||||
|   end | ||||
|  | ||||
|   activeWindow = window | ||||
|   activeWindow:raise() | ||||
|   activeWindow:focus() | ||||
|   if g_app.isMobile() then | ||||
|     window.text:focus() | ||||
|     local flags = 0 | ||||
|     if options.multiline then | ||||
|       flags = 1 | ||||
|     end | ||||
|     g_window.showTextEditor(window:getText(), window.description:getText(), window.text:getText(), flags) | ||||
|   end | ||||
|   return activeWindow | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   destroyWindow() | ||||
| end | ||||
|  | ||||
| function edit(...) | ||||
|   return show(...) | ||||
| end | ||||
|  | ||||
| -- legacy | ||||
| function singlelineEditor(text, callback) | ||||
|   return show(text, {}, callback) | ||||
| end | ||||
|  | ||||
| -- legacy | ||||
| function multilineEditor(description, text, callback) | ||||
|   return show(text, {description=description, multiline=true}, callback) | ||||
| end | ||||
|  | ||||
| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: client_textedit | ||||
|   description: Shows window which allows to edit text | ||||
|   author: OTClientV8 | ||||
|   website: https://github.com/OTCv8/otclientv8 | ||||
|   sandboxed: true | ||||
|   scripts: [ textedit ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										75
									
								
								SabrehavenOTClient/modules/client_textedit/textedit.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								SabrehavenOTClient/modules/client_textedit/textedit.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| TextEditButtons < Panel | ||||
|   id: buttons | ||||
|   height: 30 | ||||
|  | ||||
|   Button | ||||
|     id: ok | ||||
|     !text: tr('Ok') | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.right: next.left | ||||
|     margin-right: 10 | ||||
|     width: 60 | ||||
|  | ||||
|   Button | ||||
|     id: cancel | ||||
|     !text: tr('Cancel') | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.right: parent.right | ||||
|     width: 60 | ||||
|  | ||||
| TextEditWindow < MainWindow | ||||
|   id: textedit | ||||
|   !text: tr("Edit text") | ||||
|   layout: | ||||
|     type: verticalBox | ||||
|     fit-children: true | ||||
|      | ||||
|   Label | ||||
|     id: description | ||||
|     text-align: center | ||||
|     margin-bottom: 5 | ||||
|     visible: false | ||||
|     text-wrap: true | ||||
|     text-auto-resize: true | ||||
|    | ||||
|   ComboBox | ||||
|     id: examples | ||||
|     margin-bottom: 5 | ||||
|     visible: false | ||||
|  | ||||
| SinglelineTextEditWindow < TextEditWindow | ||||
|   width: 250 | ||||
|  | ||||
|   TextEdit | ||||
|     id: text | ||||
|      | ||||
|   TextEditButtons | ||||
|  | ||||
| MultilineTextEditWindow < TextEditWindow | ||||
|   width: 600 | ||||
|   $mobile: | ||||
|     width: 500 | ||||
|  | ||||
|   Panel | ||||
|     id: textPanel | ||||
|     height: 400 | ||||
|     $mobile: | ||||
|       height: 300 | ||||
|  | ||||
|     MultilineTextEdit | ||||
|       id: text | ||||
|       anchors.fill: parent | ||||
|       margin-right: 12 | ||||
|       text-wrap: true | ||||
|       vertical-scrollbar: textScroll | ||||
|  | ||||
|     VerticalScrollBar | ||||
|       id: textScroll | ||||
|       anchors.top: parent.top | ||||
|       anchors.bottom: parent.bottom | ||||
|       anchors.right: parent.right | ||||
|       pixels-scroll: true | ||||
|       step: 10 | ||||
|      | ||||
|   TextEditButtons | ||||
|  | ||||
							
								
								
									
										284
									
								
								SabrehavenOTClient/modules/client_topmenu/topmenu.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								SabrehavenOTClient/modules/client_topmenu/topmenu.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,284 @@ | ||||
| -- private variables | ||||
| local topMenu | ||||
| local fpsUpdateEvent = nil | ||||
| local statusUpdateEvent = nil | ||||
|  | ||||
| -- private functions | ||||
| local function addButton(id, description, icon, callback, panel, toggle, front, index) | ||||
|   local class | ||||
|   if toggle then | ||||
|     class = 'TopToggleButton' | ||||
|   else | ||||
|     class = 'TopButton' | ||||
|   end | ||||
|    | ||||
|   if topMenu.reverseButtons then | ||||
|     front = not front | ||||
|   end | ||||
|  | ||||
|   local button = panel:getChildById(id) | ||||
|   if not button then | ||||
|     button = g_ui.createWidget(class) | ||||
|     if front then | ||||
|       panel:insertChild(1, button) | ||||
|     else | ||||
|       panel:addChild(button) | ||||
|     end | ||||
|   end | ||||
|   button:setId(id) | ||||
|   button:setTooltip(description) | ||||
|   button:setIcon(resolvepath(icon, 3)) | ||||
|   button.onMouseRelease = function(widget, mousePos, mouseButton) | ||||
|     if widget:containsPoint(mousePos) and mouseButton ~= MouseMidButton and mouseButton ~= MouseTouch then | ||||
|       callback() | ||||
|       return true | ||||
|     end | ||||
|   end | ||||
|   button.onTouchRelease = button.onMouseRelease | ||||
|   if not button.index and type(index) == 'number' then | ||||
|     button.index = index | ||||
|   end | ||||
|   return button | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function init() | ||||
|   connect(g_game, { onGameStart = online, | ||||
|                     onGameEnd = offline, | ||||
|                     onPingBack = updatePing }) | ||||
|  | ||||
|   topMenu = g_ui.createWidget('TopMenu', g_ui.getRootWidget())   | ||||
|   g_keyboard.bindKeyDown('Ctrl+Shift+T', toggle) | ||||
|    | ||||
|   if g_game.isOnline() then | ||||
|     scheduleEvent(online, 10) | ||||
|   end | ||||
|    | ||||
|   updateFps()   | ||||
|   updateStatus() | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   disconnect(g_game, { onGameStart = online, | ||||
|                        onGameEnd = offline, | ||||
|                        onPingBack = updatePing }) | ||||
|   removeEvent(fpsUpdateEvent) | ||||
|   removeEvent(statusUpdateEvent) | ||||
|    | ||||
|   g_keyboard.unbindKeyDown('Ctrl+Shift+T') | ||||
|   topMenu:destroy() | ||||
| end | ||||
|  | ||||
| function online() | ||||
|   if topMenu.hideIngame then | ||||
|     hide() | ||||
|   else | ||||
|     modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'topMenu', AnchorBottom) | ||||
|   end | ||||
|   if topMenu.onlineLabel then | ||||
|     topMenu.onlineLabel:hide() | ||||
|   end | ||||
|    | ||||
|   showGameButtons() | ||||
|  | ||||
|   if topMenu.pingLabel then | ||||
|     addEvent(function() | ||||
|       if modules.client_options.getOption('showPing') and (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then | ||||
|         topMenu.pingLabel:show() | ||||
|       else | ||||
|         topMenu.pingLabel:hide()       | ||||
|       end | ||||
|     end) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function offline() | ||||
|   if topMenu.hideIngame then | ||||
|     show() | ||||
|   end | ||||
|   if topMenu.onlineLabel then | ||||
|     topMenu.onlineLabel:show() | ||||
|   end | ||||
|  | ||||
|   hideGameButtons() | ||||
|   if topMenu.pingLabel then | ||||
|     topMenu.pingLabel:hide() | ||||
|   end | ||||
|   updateStatus() | ||||
| end | ||||
|  | ||||
| function updateFps() | ||||
|   if not topMenu.fpsLabel then return end | ||||
|   fpsUpdateEvent = scheduleEvent(updateFps, 500) | ||||
|   text = 'FPS: ' .. g_app.getFps() | ||||
|   topMenu.fpsLabel:setText(text) | ||||
| end | ||||
|  | ||||
| function updatePing(ping) | ||||
|   if not topMenu.pingLabel then return end | ||||
|   if g_proxy and g_proxy.getPing() > 0 then | ||||
|     ping = g_proxy.getPing() | ||||
|   end | ||||
|    | ||||
|   local text = 'Ping: ' | ||||
|   local color | ||||
|   if ping < 0 then | ||||
|     text = text .. "??" | ||||
|     color = 'yellow' | ||||
|   else | ||||
|     text = text .. ping .. ' ms' | ||||
|     if ping >= 500 then | ||||
|       color = 'red' | ||||
|     elseif ping >= 250 then | ||||
|       color = 'yellow' | ||||
|     else | ||||
|       color = 'green' | ||||
|     end | ||||
|   end | ||||
|   topMenu.pingLabel:setColor(color) | ||||
|   topMenu.pingLabel:setText(text) | ||||
| end | ||||
|  | ||||
| function setPingVisible(enable) | ||||
|   if not topMenu.pingLabel then return end | ||||
|   topMenu.pingLabel:setVisible(enable) | ||||
| end | ||||
|  | ||||
| function setFpsVisible(enable) | ||||
|   if not topMenu.fpsLabel then return end | ||||
|   topMenu.fpsLabel:setVisible(enable) | ||||
| end | ||||
|  | ||||
| function addLeftButton(id, description, icon, callback, front, index) | ||||
|   return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, false, front, index) | ||||
| end | ||||
|  | ||||
| function addLeftToggleButton(id, description, icon, callback, front, index) | ||||
|   return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, true, front, index) | ||||
| end | ||||
|  | ||||
| function addRightButton(id, description, icon, callback, front, index) | ||||
|   return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, false, front, index) | ||||
| end | ||||
|  | ||||
| function addRightToggleButton(id, description, icon, callback, front, index) | ||||
|   return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, true, front, index) | ||||
| end | ||||
|  | ||||
| function addLeftGameButton(id, description, icon, callback, front, index) | ||||
|   local button = addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, false, front, index) | ||||
|   if modules.game_buttons then | ||||
|     modules.game_buttons.takeButton(button) | ||||
|   end | ||||
|   return button | ||||
| end | ||||
|  | ||||
| function addLeftGameToggleButton(id, description, icon, callback, front, index) | ||||
|   local button = addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, true, front, index) | ||||
|   if modules.game_buttons then | ||||
|     modules.game_buttons.takeButton(button) | ||||
|   end | ||||
|   return button | ||||
| end | ||||
|  | ||||
| function addRightGameButton(id, description, icon, callback, front, index) | ||||
|   local button = addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, false, front, index) | ||||
|   if modules.game_buttons then | ||||
|     modules.game_buttons.takeButton(button) | ||||
|   end | ||||
|   return button | ||||
| end | ||||
|  | ||||
| function addRightGameToggleButton(id, description, icon, callback, front, index) | ||||
|   local button = addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, true, front, index) | ||||
|   if modules.game_buttons then | ||||
|     modules.game_buttons.takeButton(button) | ||||
|   end | ||||
|   return button | ||||
| end | ||||
|  | ||||
| function showGameButtons() | ||||
|   topMenu.leftGameButtonsPanel:show() | ||||
|   topMenu.rightGameButtonsPanel:show() | ||||
|   if modules.game_buttons then | ||||
|     modules.game_buttons.takeButtons(topMenu.leftGameButtonsPanel:getChildren()) | ||||
|     modules.game_buttons.takeButtons(topMenu.rightGameButtonsPanel:getChildren()) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function hideGameButtons() | ||||
|   topMenu.leftGameButtonsPanel:hide() | ||||
|   topMenu.rightGameButtonsPanel:hide() | ||||
| end | ||||
|  | ||||
| function getButton(id) | ||||
|   return topMenu:recursiveGetChildById(id) | ||||
| end | ||||
|  | ||||
| function getTopMenu() | ||||
|   return topMenu | ||||
| end | ||||
|  | ||||
| function toggle() | ||||
|   if not topMenu then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   if topMenu:isVisible() then | ||||
|     hide() | ||||
|   else | ||||
|     show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   topMenu:hide() | ||||
|   if not topMenu.hideIngame then | ||||
|     modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'parent', AnchorTop) | ||||
|   end | ||||
|   if modules.game_stats then | ||||
|     modules.game_stats.show() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   topMenu:show() | ||||
|   if not topMenu.hideIngame then | ||||
|     modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'topMenu', AnchorBottom) | ||||
|   end | ||||
|   if modules.game_stats then | ||||
|     modules.game_stats.hide() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function updateStatus() | ||||
|   removeEvent(statusUpdateEvent) | ||||
|   if not Services or not Services.status or Services.status:len() < 4 then return end | ||||
|   if not topMenu.onlineLabel then return end | ||||
|   if g_game.isOnline() then return end | ||||
|   HTTP.postJSON(Services.status, {type="cacheinfo"}, function(data, err) | ||||
|     if err then | ||||
|       g_logger.warning("HTTP error for " .. Services.status .. ": " .. err)  | ||||
|       statusUpdateEvent = scheduleEvent(updateStatus, 5000) | ||||
|       return | ||||
|     end | ||||
|     if topMenu.onlineLabel then | ||||
|       if data.online then | ||||
|         topMenu.onlineLabel:setText(data.online) | ||||
|       elseif data.playersonline then | ||||
|         topMenu.onlineLabel:setText(data.playersonline .. " players online") | ||||
|       end | ||||
|     end | ||||
|     if data.discord_online and topMenu.discordLabel then | ||||
|       topMenu.discordLabel:setText(data.discord_online) | ||||
|     end | ||||
|     if data.discord_link and topMenu.discordLabel and topMenu.discord then | ||||
|       local discordOnClick = function() | ||||
|         g_platform.openUrl(data.discord_link) | ||||
|       end | ||||
|       topMenu.discordLabel.onClick = discordOnClick | ||||
|       topMenu.discord.onClick = discordOnClick | ||||
|     end | ||||
|     statusUpdateEvent = scheduleEvent(updateStatus, 60000) | ||||
|   end) | ||||
| end | ||||
							
								
								
									
										10
									
								
								SabrehavenOTClient/modules/client_topmenu/topmenu.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								SabrehavenOTClient/modules/client_topmenu/topmenu.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| Module | ||||
|   name: client_topmenu | ||||
|   description: Create the top menu | ||||
|   author: edubart | ||||
|   website: https://github.com/edubart/otclient | ||||
|   scripts: [ topmenu ] | ||||
|   sandboxed: true | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
|  | ||||
							
								
								
									
										176
									
								
								SabrehavenOTClient/modules/corelib/base64.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								SabrehavenOTClient/modules/corelib/base64.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| --[[ | ||||
|  | ||||
|  base64 -- v1.5.1 public domain Lua base64 encoder/decoder | ||||
|  no warranty implied; use at your own risk | ||||
|  | ||||
|  Needs bit32.extract function. If not present it's implemented using BitOp | ||||
|  or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua | ||||
|  implementation inspired by Rici Lake's post: | ||||
|    http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html | ||||
|  | ||||
|  author: Ilya Kolbin (iskolbin@gmail.com) | ||||
|  url: github.com/iskolbin/lbase64 | ||||
|  | ||||
|  COMPATIBILITY | ||||
|  | ||||
|  Lua 5.1, 5.2, 5.3, LuaJIT | ||||
|  | ||||
|  LICENSE | ||||
|  | ||||
|  See end of file for license information. | ||||
|  | ||||
| --]] | ||||
|  | ||||
|  | ||||
| base64 = {} | ||||
|  | ||||
| local extract = _G.bit32 and _G.bit32.extract | ||||
| if not extract then | ||||
| 	if _G.bit then | ||||
| 		local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band | ||||
| 		extract = function( v, from, width ) | ||||
| 			return band( shr( v, from ), shl( 1, width ) - 1 ) | ||||
| 		end | ||||
| 	elseif _G._VERSION >= "Lua 5.3" then | ||||
| 		extract = load[[return function( v, from, width ) | ||||
| 			return ( v >> from ) & ((1 << width) - 1) | ||||
| 		end]]() | ||||
| 	else | ||||
| 		extract = function( v, from, width ) | ||||
| 			local w = 0 | ||||
| 			local flag = 2^from | ||||
| 			for i = 0, width-1 do | ||||
| 				local flag2 = flag + flag | ||||
| 				if v % flag2 >= flag then | ||||
| 					w = w + 2^i | ||||
| 				end | ||||
| 				flag = flag2 | ||||
| 			end | ||||
| 			return w | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
|  | ||||
|  | ||||
| function base64.makeencoder( s62, s63, spad ) | ||||
| 	local encoder = {} | ||||
| 	for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J', | ||||
| 		'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y', | ||||
| 		'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', | ||||
| 		'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2', | ||||
| 		'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do | ||||
| 		encoder[b64code] = char:byte() | ||||
| 	end | ||||
| 	return encoder | ||||
| end | ||||
|  | ||||
| function base64.makedecoder( s62, s63, spad ) | ||||
| 	local decoder = {} | ||||
| 	for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do | ||||
| 		decoder[charcode] = b64code | ||||
| 	end | ||||
| 	return decoder | ||||
| end | ||||
|  | ||||
| local DEFAULT_ENCODER = base64.makeencoder() | ||||
| local DEFAULT_DECODER = base64.makedecoder() | ||||
|  | ||||
| local char, concat = string.char, table.concat | ||||
|  | ||||
| function base64.encode( str, encoder, usecaching ) | ||||
| 	encoder = encoder or DEFAULT_ENCODER | ||||
| 	local t, k, n = {}, 1, #str | ||||
| 	local lastn = n % 3 | ||||
| 	local cache = {} | ||||
| 	for i = 1, n-lastn, 3 do | ||||
| 		local a, b, c = str:byte( i, i+2 ) | ||||
| 		local v = a*0x10000 + b*0x100 + c | ||||
| 		local s | ||||
| 		if usecaching then | ||||
| 			s = cache[v] | ||||
| 			if not s then | ||||
| 				s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) | ||||
| 				cache[v] = s | ||||
| 			end | ||||
| 		else | ||||
| 			s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) | ||||
| 		end | ||||
| 		t[k] = s | ||||
| 		k = k + 1 | ||||
| 	end | ||||
| 	if lastn == 2 then | ||||
| 		local a, b = str:byte( n-1, n ) | ||||
| 		local v = a*0x10000 + b*0x100 | ||||
| 		t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64]) | ||||
| 	elseif lastn == 1 then | ||||
| 		local v = str:byte( n )*0x10000 | ||||
| 		t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64]) | ||||
| 	end | ||||
| 	return concat( t ) | ||||
| end | ||||
|  | ||||
| function base64.decode( b64, decoder, usecaching ) | ||||
| 	decoder = decoder or DEFAULT_DECODER | ||||
| 	local pattern = '[^%w%+%/%=]' | ||||
| 	if decoder then | ||||
| 		local s62, s63 | ||||
| 		for charcode, b64code in pairs( decoder ) do | ||||
| 			if b64code == 62 then s62 = charcode | ||||
| 			elseif b64code == 63 then s63 = charcode | ||||
| 			end | ||||
| 		end | ||||
| 		pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) ) | ||||
| 	end | ||||
| 	b64 = b64:gsub( pattern, '' ) | ||||
| 	local cache = usecaching and {} | ||||
| 	local t, k = {}, 1 | ||||
| 	local n = #b64 | ||||
| 	local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 | ||||
| 	for i = 1, padding > 0 and n-4 or n, 4 do | ||||
| 		local a, b, c, d = b64:byte( i, i+3 ) | ||||
| 		local s | ||||
| 		if usecaching then | ||||
| 			local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d | ||||
| 			s = cache[v0] | ||||
| 			if not s then | ||||
| 				local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] | ||||
| 				s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) | ||||
| 				cache[v0] = s | ||||
| 			end | ||||
| 		else | ||||
| 			local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] | ||||
| 			s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8)) | ||||
| 		end | ||||
| 		t[k] = s | ||||
| 		k = k + 1 | ||||
| 	end | ||||
| 	if padding == 1 then | ||||
| 		local a, b, c = b64:byte( n-3, n-1 ) | ||||
| 		local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 | ||||
| 		t[k] = char( extract(v,16,8), extract(v,8,8)) | ||||
| 	elseif padding == 2 then | ||||
| 		local a, b = b64:byte( n-3, n-2 ) | ||||
| 		local v = decoder[a]*0x40000 + decoder[b]*0x1000 | ||||
| 		t[k] = char( extract(v,16,8)) | ||||
| 	end | ||||
| 	return concat( t ) | ||||
| end | ||||
|  | ||||
| --[[ | ||||
| Copyright (c) 2018 Ilya Kolbin | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy of | ||||
| this software and associated documentation files (the "Software"), to deal in | ||||
| the Software without restriction, including without limitation the rights to | ||||
| use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||||
| of the Software, and to permit persons to whom the Software is furnished to do | ||||
| so, subject to the following conditions: | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
| --]] | ||||
							
								
								
									
										17
									
								
								SabrehavenOTClient/modules/corelib/bitwise.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								SabrehavenOTClient/modules/corelib/bitwise.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| Bit = {} | ||||
|  | ||||
| function Bit.bit(p) | ||||
|   return 2 ^ p | ||||
| end | ||||
|  | ||||
| function Bit.hasBit(x, p) | ||||
|   return x % (p + p) >= p | ||||
| end | ||||
|  | ||||
| function Bit.setbit(x, p) | ||||
|   return Bit.hasBit(x, p) and x or x + p | ||||
| end | ||||
|  | ||||
| function Bit.clearbit(x, p) | ||||
|   return Bit.hasBit(x, p) and x - p or x | ||||
| end | ||||
							
								
								
									
										73
									
								
								SabrehavenOTClient/modules/corelib/config.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								SabrehavenOTClient/modules/corelib/config.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| -- @docclass | ||||
|  | ||||
| local function convertSettingValue(value) | ||||
|   if type(value) == 'table' then | ||||
|     if value.x and value.width then | ||||
|       return recttostring(value) | ||||
|     elseif value.x then | ||||
|       return pointtostring(value) | ||||
|     elseif value.width then | ||||
|       return sizetostring(value) | ||||
|     elseif value.r then | ||||
|       return colortostring(value) | ||||
|     else | ||||
|       return value | ||||
|     end | ||||
|   elseif value == nil then | ||||
|     return '' | ||||
|   else | ||||
|     return tostring(value) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function Config:set(key, value) | ||||
|   self:setValue(key, convertSettingValue(value)) | ||||
| end | ||||
|  | ||||
| function Config:setDefault(key, value) | ||||
|   if self:exists(key) then return false end | ||||
|   self:set(key, value) | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function Config:get(key, default) | ||||
|   if not self:exists(key) and default ~= nil then | ||||
|     self:set(key, default) | ||||
|   end | ||||
|   return self:getValue(key) | ||||
| end | ||||
|  | ||||
| function Config:getString(key, default) | ||||
|   return self:get(key, default) | ||||
| end | ||||
|  | ||||
| function Config:getInteger(key, default) | ||||
|   local v = tonumber(self:get(key, default)) or 0 | ||||
|   return v | ||||
| end | ||||
|  | ||||
| function Config:getNumber(key, default) | ||||
|   local v = tonumber(self:get(key, default)) or 0 | ||||
|   return v | ||||
| end | ||||
|  | ||||
| function Config:getBoolean(key, default) | ||||
|   return toboolean(self:get(key, default)) | ||||
| end | ||||
|  | ||||
| function Config:getPoint(key, default) | ||||
|   return topoint(self:get(key, default)) | ||||
| end | ||||
|  | ||||
| function Config:getRect(key, default) | ||||
|   return torect(self:get(key, default)) | ||||
| end | ||||
|  | ||||
| function Config:getSize(key, default) | ||||
|   return tosize(self:get(key, default)) | ||||
| end | ||||
|  | ||||
| function Config:getColor(key, default) | ||||
|   return tocolor(self:get(key, default)) | ||||
| end | ||||
|  | ||||
							
								
								
									
										325
									
								
								SabrehavenOTClient/modules/corelib/const.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								SabrehavenOTClient/modules/corelib/const.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,325 @@ | ||||
| -- @docconsts @{ | ||||
|  | ||||
| AnchorNone = 0 | ||||
| AnchorTop = 1 | ||||
| AnchorBottom = 2 | ||||
| AnchorLeft = 3 | ||||
| AnchorRight = 4 | ||||
| AnchorVerticalCenter = 5 | ||||
| AnchorHorizontalCenter = 6 | ||||
|  | ||||
| LogDebug = 0 | ||||
| LogInfo = 1 | ||||
| LogWarning = 2 | ||||
| LogError = 3 | ||||
| LogFatal = 4 | ||||
|  | ||||
| MouseFocusReason = 0 | ||||
| KeyboardFocusReason = 1 | ||||
| ActiveFocusReason = 2 | ||||
| OtherFocusReason = 3 | ||||
|  | ||||
| AutoFocusNone = 0 | ||||
| AutoFocusFirst = 1 | ||||
| AutoFocusLast = 2 | ||||
|  | ||||
| KeyboardNoModifier = 0 | ||||
| KeyboardCtrlModifier = 1 | ||||
| KeyboardAltModifier = 2 | ||||
| KeyboardCtrlAltModifier = 3 | ||||
| KeyboardShiftModifier = 4 | ||||
| KeyboardCtrlShiftModifier = 5 | ||||
| KeyboardAltShiftModifier = 6 | ||||
| KeyboardCtrlAltShiftModifier = 7 | ||||
|  | ||||
| MouseNoButton = 0 | ||||
| MouseLeftButton = 1 | ||||
| MouseRightButton = 2 | ||||
| MouseMidButton = 3 | ||||
| MouseTouch = 4 | ||||
| MouseTouch2 = 5 -- multitouch, 2nd finger | ||||
| MouseTouch3 = 6 -- multitouch, 3th finger | ||||
|  | ||||
| MouseNoWheel = 0 | ||||
| MouseWheelUp = 1 | ||||
| MouseWheelDown = 2 | ||||
|  | ||||
| AlignNone = 0 | ||||
| AlignLeft = 1 | ||||
| AlignRight = 2 | ||||
| AlignTop = 4 | ||||
| AlignBottom = 8 | ||||
| AlignHorizontalCenter = 16 | ||||
| AlignVerticalCenter = 32 | ||||
| AlignTopLeft = 5 | ||||
| AlignTopRight = 6 | ||||
| AlignBottomLeft = 9 | ||||
| AlignBottomRight = 10 | ||||
| AlignLeftCenter = 33 | ||||
| AlignRightCenter = 34 | ||||
| AlignTopCenter = 20 | ||||
| AlignBottomCenter = 24 | ||||
| AlignCenter = 48 | ||||
|  | ||||
| KeyUnknown = 0 | ||||
| KeyEscape = 1 | ||||
| KeyTab = 2 | ||||
| KeyBackspace = 3 | ||||
| KeyEnter = 5 | ||||
| KeyInsert = 6 | ||||
| KeyDelete = 7 | ||||
| KeyPause = 8 | ||||
| KeyPrintScreen = 9 | ||||
| KeyHome = 10 | ||||
| KeyEnd = 11 | ||||
| KeyPageUp = 12 | ||||
| KeyPageDown = 13 | ||||
| KeyUp = 14 | ||||
| KeyDown = 15 | ||||
| KeyLeft = 16 | ||||
| KeyRight = 17 | ||||
| KeyNumLock = 18 | ||||
| KeyScrollLock = 19 | ||||
| KeyCapsLock = 20 | ||||
| KeyCtrl = 21 | ||||
| KeyShift = 22 | ||||
| KeyAlt = 23 | ||||
| KeyMeta = 25 | ||||
| KeyMenu = 26 | ||||
| KeySpace = 32        -- ' ' | ||||
| KeyExclamation = 33  -- ! | ||||
| KeyQuote = 34        -- " | ||||
| KeyNumberSign = 35   -- # | ||||
| KeyDollar = 36       -- $ | ||||
| KeyPercent = 37      -- % | ||||
| KeyAmpersand = 38    -- & | ||||
| KeyApostrophe = 39   -- ' | ||||
| KeyLeftParen = 40    -- ( | ||||
| KeyRightParen = 41   -- ) | ||||
| KeyAsterisk = 42     -- * | ||||
| KeyPlus = 43         -- + | ||||
| KeyComma = 44        -- , | ||||
| KeyMinus = 45        -- - | ||||
| KeyPeriod = 46       -- . | ||||
| KeySlash = 47        -- / | ||||
| Key0 = 48            -- 0 | ||||
| Key1 = 49            -- 1 | ||||
| Key2 = 50            -- 2 | ||||
| Key3 = 51            -- 3 | ||||
| Key4 = 52            -- 4 | ||||
| Key5 = 53            -- 5 | ||||
| Key6 = 54            -- 6 | ||||
| Key7 = 55            -- 7 | ||||
| Key8 = 56            -- 8 | ||||
| Key9 = 57            -- 9 | ||||
| KeyColon = 58        -- : | ||||
| KeySemicolon = 59    -- ; | ||||
| KeyLess = 60         -- < | ||||
| KeyEqual = 61        -- = | ||||
| KeyGreater = 62      -- > | ||||
| KeyQuestion = 63     -- ? | ||||
| KeyAtSign = 64       -- @ | ||||
| KeyA = 65            -- a | ||||
| KeyB = 66            -- b | ||||
| KeyC = 67            -- c | ||||
| KeyD = 68            -- d | ||||
| KeyE = 69            -- e | ||||
| KeyF = 70            -- f | ||||
| KeyG = 71            -- g | ||||
| KeyH = 72            -- h | ||||
| KeyI = 73            -- i | ||||
| KeyJ = 74            -- j | ||||
| KeyK = 75            -- k | ||||
| KeyL = 76            -- l | ||||
| KeyM = 77            -- m | ||||
| KeyN = 78            -- n | ||||
| KeyO = 79            -- o | ||||
| KeyP = 80            -- p | ||||
| KeyQ = 81            -- q | ||||
| KeyR = 82            -- r | ||||
| KeyS = 83            -- s | ||||
| KeyT = 84            -- t | ||||
| KeyU = 85            -- u | ||||
| KeyV = 86            -- v | ||||
| KeyW = 87            -- w | ||||
| KeyX = 88            -- x | ||||
| KeyY = 89            -- y | ||||
| KeyZ = 90            -- z | ||||
| KeyLeftBracket = 91  -- [ | ||||
| KeyBackslash = 92    -- '\' | ||||
| KeyRightBracket = 93 -- ] | ||||
| KeyCaret = 94        -- ^ | ||||
| KeyUnderscore = 95   -- _ | ||||
| KeyGrave = 96        -- ` | ||||
| KeyLeftCurly = 123   -- { | ||||
| KeyBar = 124         -- | | ||||
| KeyRightCurly = 125  -- } | ||||
| KeyTilde = 126       -- ~ | ||||
| KeyF1 = 128 | ||||
| KeyF2 = 129 | ||||
| KeyF3 = 130 | ||||
| KeyF4 = 131 | ||||
| KeyF5 = 132 | ||||
| KeyF6 = 134 | ||||
| KeyF7 = 135 | ||||
| KeyF8 = 136 | ||||
| KeyF9 = 137 | ||||
| KeyF10 = 138 | ||||
| KeyF11 = 139 | ||||
| KeyF12 = 140 | ||||
| KeyNumpad0 = 141 | ||||
| KeyNumpad1 = 142 | ||||
| KeyNumpad2 = 143 | ||||
| KeyNumpad3 = 144 | ||||
| KeyNumpad4 = 145 | ||||
| KeyNumpad5 = 146 | ||||
| KeyNumpad6 = 147 | ||||
| KeyNumpad7 = 148 | ||||
| KeyNumpad8 = 149 | ||||
| KeyNumpad9 = 150 | ||||
|  | ||||
| FirstKey = KeyUnknown | ||||
| LastKey = KeyNumpad9 | ||||
|  | ||||
| ExtendedActivate = 0 | ||||
| ExtendedLocales = 1 | ||||
| ExtendedParticles = 2 | ||||
|  | ||||
| -- @} | ||||
|  | ||||
| KeyCodeDescs = { | ||||
|   [KeyUnknown] = 'Unknown', | ||||
|   [KeyEscape] = 'Escape', | ||||
|   [KeyTab] = 'Tab', | ||||
|   [KeyBackspace] = 'Backspace', | ||||
|   [KeyEnter] = 'Enter', | ||||
|   [KeyInsert] = 'Insert', | ||||
|   [KeyDelete] = 'Delete', | ||||
|   [KeyPause] = 'Pause', | ||||
|   [KeyPrintScreen] = 'PrintScreen', | ||||
|   [KeyHome] = 'Home', | ||||
|   [KeyEnd] = 'End', | ||||
|   [KeyPageUp] = 'PageUp', | ||||
|   [KeyPageDown] = 'PageDown', | ||||
|   [KeyUp] = 'Up', | ||||
|   [KeyDown] = 'Down', | ||||
|   [KeyLeft] = 'Left', | ||||
|   [KeyRight] = 'Right', | ||||
|   [KeyNumLock] = 'NumLock', | ||||
|   [KeyScrollLock] = 'ScrollLock', | ||||
|   [KeyCapsLock] = 'CapsLock', | ||||
|   [KeyCtrl] = 'Ctrl', | ||||
|   [KeyShift] = 'Shift', | ||||
|   [KeyAlt] = 'Alt', | ||||
|   [KeyMeta] = 'Meta', | ||||
|   [KeyMenu] = 'Menu', | ||||
|   [KeySpace] = 'Space', | ||||
|   [KeyExclamation] = '!', | ||||
|   [KeyQuote] = '\"', | ||||
|   [KeyNumberSign] = '#', | ||||
|   [KeyDollar] = '$', | ||||
|   [KeyPercent] = '%', | ||||
|   [KeyAmpersand] = '&', | ||||
|   [KeyApostrophe] = '\'', | ||||
|   [KeyLeftParen] = '(', | ||||
|   [KeyRightParen] = ')', | ||||
|   [KeyAsterisk] = '*', | ||||
|   [KeyPlus] = 'Plus', | ||||
|   [KeyComma] = ',', | ||||
|   [KeyMinus] = '-', | ||||
|   [KeyPeriod] = '.', | ||||
|   [KeySlash] = '/', | ||||
|   [Key0] = '0', | ||||
|   [Key1] = '1', | ||||
|   [Key2] = '2', | ||||
|   [Key3] = '3', | ||||
|   [Key4] = '4', | ||||
|   [Key5] = '5', | ||||
|   [Key6] = '6', | ||||
|   [Key7] = '7', | ||||
|   [Key8] = '8', | ||||
|   [Key9] = '9', | ||||
|   [KeyColon] = ':', | ||||
|   [KeySemicolon] = ';', | ||||
|   [KeyLess] = '<', | ||||
|   [KeyEqual] = '=', | ||||
|   [KeyGreater] = '>', | ||||
|   [KeyQuestion] = '?', | ||||
|   [KeyAtSign] = '@', | ||||
|   [KeyA] = 'A', | ||||
|   [KeyB] = 'B', | ||||
|   [KeyC] = 'C', | ||||
|   [KeyD] = 'D', | ||||
|   [KeyE] = 'E', | ||||
|   [KeyF] = 'F', | ||||
|   [KeyG] = 'G', | ||||
|   [KeyH] = 'H', | ||||
|   [KeyI] = 'I', | ||||
|   [KeyJ] = 'J', | ||||
|   [KeyK] = 'K', | ||||
|   [KeyL] = 'L', | ||||
|   [KeyM] = 'M', | ||||
|   [KeyN] = 'N', | ||||
|   [KeyO] = 'O', | ||||
|   [KeyP] = 'P', | ||||
|   [KeyQ] = 'Q', | ||||
|   [KeyR] = 'R', | ||||
|   [KeyS] = 'S', | ||||
|   [KeyT] = 'T', | ||||
|   [KeyU] = 'U', | ||||
|   [KeyV] = 'V', | ||||
|   [KeyW] = 'W', | ||||
|   [KeyX] = 'X', | ||||
|   [KeyY] = 'Y', | ||||
|   [KeyZ] = 'Z', | ||||
|   [KeyLeftBracket] = '[', | ||||
|   [KeyBackslash] = '\\', | ||||
|   [KeyRightBracket] = ']', | ||||
|   [KeyCaret] = '^', | ||||
|   [KeyUnderscore] = '_', | ||||
|   [KeyGrave] = '`', | ||||
|   [KeyLeftCurly] = '{', | ||||
|   [KeyBar] = '|', | ||||
|   [KeyRightCurly] = '}', | ||||
|   [KeyTilde] = '~', | ||||
|   [KeyF1] = 'F1', | ||||
|   [KeyF2] = 'F2', | ||||
|   [KeyF3] = 'F3', | ||||
|   [KeyF4] = 'F4', | ||||
|   [KeyF5] = 'F5', | ||||
|   [KeyF6] = 'F6', | ||||
|   [KeyF7] = 'F7', | ||||
|   [KeyF8] = 'F8', | ||||
|   [KeyF9] = 'F9', | ||||
|   [KeyF10] = 'F10', | ||||
|   [KeyF11] = 'F11', | ||||
|   [KeyF12] = 'F12', | ||||
|   [KeyNumpad0] = 'Numpad0', | ||||
|   [KeyNumpad1] = 'Numpad1', | ||||
|   [KeyNumpad2] = 'Numpad2', | ||||
|   [KeyNumpad3] = 'Numpad3', | ||||
|   [KeyNumpad4] = 'Numpad4', | ||||
|   [KeyNumpad5] = 'Numpad5', | ||||
|   [KeyNumpad6] = 'Numpad6', | ||||
|   [KeyNumpad7] = 'Numpad7', | ||||
|   [KeyNumpad8] = 'Numpad8', | ||||
|   [KeyNumpad9] = 'Numpad9', | ||||
| } | ||||
|  | ||||
| NetworkMessageTypes = { | ||||
|   Boolean = 1, | ||||
|   U8 = 2, | ||||
|   U16 = 3, | ||||
|   U32 = 4, | ||||
|   U64 = 5, | ||||
|   NumberString = 6, | ||||
|   String = 7, | ||||
|   Table = 8, | ||||
| } | ||||
|  | ||||
| SoundChannels = { | ||||
|   Music = 1, | ||||
|   Ambient = 2, | ||||
|   Effect = 3, | ||||
|   Bot = 4 | ||||
| } | ||||
							
								
								
									
										34
									
								
								SabrehavenOTClient/modules/corelib/corelib.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								SabrehavenOTClient/modules/corelib/corelib.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| Module | ||||
|   name: corelib | ||||
|   description: Contains core lua classes, functions and constants used by other modules | ||||
|   author: OTClient team | ||||
|   website: https://github.com/edubart/otclient | ||||
|   reloadable: false | ||||
|  | ||||
|   @onLoad: | | ||||
|     dofile 'math' | ||||
|     dofile 'string' | ||||
|     dofile 'table' | ||||
|     dofile 'bitwise' | ||||
|     dofile 'struct' | ||||
|  | ||||
|     dofile 'const' | ||||
|     dofile 'util' | ||||
|     dofile 'globals' | ||||
|     dofile 'config' | ||||
|     dofile 'settings' | ||||
|     dofile 'keyboard' | ||||
|     dofile 'mouse' | ||||
|     dofile 'net' | ||||
|  | ||||
|     dofiles 'classes' | ||||
|     dofiles 'ui' | ||||
|  | ||||
|     dofile 'inputmessage' | ||||
|     dofile 'outputmessage' | ||||
|     dofile 'orderedtable' | ||||
|      | ||||
|     dofile 'base64' | ||||
|     dofile 'json' | ||||
|     dofile 'http' | ||||
|     dofile 'test' | ||||
							
								
								
									
										76
									
								
								SabrehavenOTClient/modules/corelib/globals.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								SabrehavenOTClient/modules/corelib/globals.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| -- @docvars @{ | ||||
|  | ||||
| -- root widget | ||||
| rootWidget = g_ui.getRootWidget() | ||||
| modules = package.loaded | ||||
|  | ||||
| -- G is used as a global table to save variables in memory between reloads | ||||
| G = G or {} | ||||
|  | ||||
| -- @} | ||||
|  | ||||
| -- @docfuncs @{ | ||||
|  | ||||
| function scheduleEvent(callback, delay) | ||||
|   local desc = "lua" | ||||
|   local info = debug.getinfo(2, "Sl") | ||||
|   if info then | ||||
|     desc = info.short_src .. ":" .. info.currentline | ||||
|   end | ||||
|   local event = g_dispatcher.scheduleEvent(desc, callback, delay) | ||||
|   -- must hold a reference to the callback, otherwise it would be collected | ||||
|   event._callback = callback | ||||
|   return event | ||||
| end | ||||
|  | ||||
| function addEvent(callback, front) | ||||
|   local desc = "lua" | ||||
|   local info = debug.getinfo(2, "Sl") | ||||
|   if info then | ||||
|     desc = info.short_src .. ":" .. info.currentline | ||||
|   end | ||||
|   local event = g_dispatcher.addEvent(desc, callback, front) | ||||
|   -- must hold a reference to the callback, otherwise it would be collected | ||||
|   event._callback = callback | ||||
|   return event | ||||
| end | ||||
|  | ||||
| function cycleEvent(callback, interval) | ||||
|   local desc = "lua" | ||||
|   local info = debug.getinfo(2, "Sl") | ||||
|   if info then | ||||
|     desc = info.short_src .. ":" .. info.currentline | ||||
|   end | ||||
|   local event = g_dispatcher.cycleEvent(desc, callback, interval) | ||||
|   -- must hold a reference to the callback, otherwise it would be collected | ||||
|   event._callback = callback | ||||
|   return event | ||||
| end | ||||
|  | ||||
| function periodicalEvent(eventFunc, conditionFunc, delay, autoRepeatDelay) | ||||
|   delay = delay or 30 | ||||
|   autoRepeatDelay = autoRepeatDelay or delay | ||||
|  | ||||
|   local func | ||||
|   func = function() | ||||
|     if conditionFunc and not conditionFunc() then | ||||
|       func = nil | ||||
|       return | ||||
|     end | ||||
|     eventFunc() | ||||
|     scheduleEvent(func, delay) | ||||
|   end | ||||
|  | ||||
|   scheduleEvent(function() | ||||
|     func() | ||||
|   end, autoRepeatDelay) | ||||
| end | ||||
|  | ||||
| function removeEvent(event) | ||||
|   if event then | ||||
|     event:cancel() | ||||
|     event._callback = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- @} | ||||
							
								
								
									
										278
									
								
								SabrehavenOTClient/modules/corelib/http.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								SabrehavenOTClient/modules/corelib/http.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,278 @@ | ||||
| HTTP = { | ||||
|   timeout=5, | ||||
|   websocketTimeout=15, | ||||
|   agent="Mozilla/5.0", | ||||
|   imageId=1000, | ||||
|   images={}, | ||||
|   operations={}, | ||||
| } | ||||
|  | ||||
| function HTTP.get(url, callback) | ||||
|   if not g_http or not g_http.get then | ||||
|     return error("HTTP.get is not supported") | ||||
|   end | ||||
|   local operation = g_http.get(url, HTTP.timeout) | ||||
|   HTTP.operations[operation] = {type="get", url=url, callback=callback}   | ||||
|   return operation | ||||
| end | ||||
|  | ||||
| function HTTP.getJSON(url, callback) | ||||
|   if not g_http or not g_http.get then | ||||
|     return error("HTTP.getJSON is not supported") | ||||
|   end | ||||
|   local operation = g_http.get(url, HTTP.timeout) | ||||
|   HTTP.operations[operation] = {type="get", json=true, url=url, callback=callback}   | ||||
|   return operation | ||||
| end | ||||
|  | ||||
| function HTTP.post(url, data, callback) | ||||
|   if not g_http or not g_http.post then | ||||
|     return error("HTTP.post is not supported") | ||||
|   end | ||||
|   if type(data) == "table" then | ||||
|     data = json.encode(data) | ||||
|   end | ||||
|   local operation = g_http.post(url, data, HTTP.timeout) | ||||
|   HTTP.operations[operation] = {type="post", url=url, callback=callback} | ||||
|   return operation | ||||
| end | ||||
|  | ||||
| function HTTP.postJSON(url, data, callback) | ||||
|   if not g_http or not g_http.post then | ||||
|     return error("HTTP.postJSON is not supported") | ||||
|   end | ||||
|   if type(data) == "table" then | ||||
|     data = json.encode(data) | ||||
|   end | ||||
|   local operation = g_http.post(url, data, HTTP.timeout) | ||||
|   HTTP.operations[operation] = {type="post", json=true, url=url, callback=callback} | ||||
|   return operation | ||||
| end | ||||
|  | ||||
| function HTTP.download(url, file, callback, progressCallback) | ||||
|   if not g_http or not g_http.download then | ||||
|     return error("HTTP.download is not supported") | ||||
|   end | ||||
|   local operation = g_http.download(url, file, HTTP.timeout) | ||||
|   HTTP.operations[operation] = {type="download", url=url, file=file, callback=callback, progressCallback=progressCallback}   | ||||
|   return operation | ||||
| end | ||||
|  | ||||
| function HTTP.downloadImage(url, callback) | ||||
|   if not g_http or not g_http.download then | ||||
|     return error("HTTP.downloadImage is not supported") | ||||
|   end | ||||
|   if HTTP.images[url] ~= nil then | ||||
|     if callback then | ||||
|       callback('/downloads/' .. HTTP.images[url], nil) | ||||
|     end | ||||
|     return | ||||
|   end | ||||
|   local file = "autoimage_" .. HTTP.imageId .. ".png" | ||||
|   HTTP.imageId = HTTP.imageId + 1 | ||||
|   local operation = g_http.download(url, file, HTTP.timeout) | ||||
|   HTTP.operations[operation] = {type="image", url=url, file=file, callback=callback}   | ||||
|   return operation | ||||
| end | ||||
|  | ||||
| function HTTP.webSocket(url, callbacks, timeout, jsonWebsocket) | ||||
|   if not g_http or not g_http.ws then | ||||
|     return error("WebSocket is not supported") | ||||
|   end | ||||
|   if not timeout or timeout < 1 then | ||||
|     timeout = HTTP.websocketTimeout | ||||
|   end | ||||
|   local operation = g_http.ws(url, timeout) | ||||
|   HTTP.operations[operation] = {type="ws", json=jsonWebsocket, url=url, callbacks=callbacks}   | ||||
|   return { | ||||
|     id = operation, | ||||
|     url = url, | ||||
|     close = function()  | ||||
|       g_http.wsClose(operation) | ||||
|     end, | ||||
|     send = function(message) | ||||
|       if type(message) == "table" then | ||||
|         message = json.encode(message) | ||||
|       end | ||||
|       g_http.wsSend(operation, message) | ||||
|     end | ||||
|   } | ||||
| end | ||||
| HTTP.WebSocket = HTTP.webSocket | ||||
|  | ||||
| function HTTP.webSocketJSON(url, callbacks, timeout) | ||||
|   return HTTP.webSocket(url, callbacks, timeout, true) | ||||
| end | ||||
| HTTP.WebSocketJSON = HTTP.webSocketJSON | ||||
|  | ||||
| function HTTP.cancel(operationId) | ||||
|   if not g_http or not g_http.cancel then | ||||
|     return | ||||
|   end | ||||
|   HTTP.operations[operationId] = nil | ||||
|   return g_http.cancel(operationId) | ||||
| end | ||||
|  | ||||
| function HTTP.onGet(operationId, url, err, data) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if err and err:len() == 0 then | ||||
|     err = nil | ||||
|   end | ||||
|   if not err and operation.json then | ||||
|     local status, result = pcall(function() return json.decode(data) end) | ||||
|     if not status then | ||||
|       err = "JSON ERROR: " .. result | ||||
|       if data and data:len() > 0 then | ||||
|         err = err .. " (" .. data:sub(1, 100) .. ")" | ||||
|       end | ||||
|     end   | ||||
|     data = result | ||||
|   end | ||||
|   if operation.callback then | ||||
|     operation.callback(data, err) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onGetProgress(operationId, url, progress) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|    | ||||
| end | ||||
|  | ||||
| function HTTP.onPost(operationId, url, err, data) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if err and err:len() == 0 then | ||||
|     err = nil | ||||
|   end | ||||
|   if not err and operation.json then | ||||
|     local status, result = pcall(function() return json.decode(data) end) | ||||
|     if not status then | ||||
|       err = "JSON ERROR: " .. result | ||||
|       if data and data:len() > 0 then | ||||
|         err = err .. " (" .. data:sub(1, 100) .. ")" | ||||
|       end | ||||
|     end   | ||||
|     data = result | ||||
|   end | ||||
|   if operation.callback then | ||||
|     operation.callback(data, err) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onPostProgress(operationId, url, progress) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onDownload(operationId, url, err, path, checksum) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if err and err:len() == 0 then | ||||
|     err = nil | ||||
|   end | ||||
|   if operation.callback then | ||||
|     if operation["type"] == "image" then | ||||
|       if not err then | ||||
|         HTTP.images[url] = path | ||||
|       end | ||||
|       operation.callback('/downloads/' .. path, err)     | ||||
|     else | ||||
|       operation.callback(path, checksum, err) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onDownloadProgress(operationId, url, progress, speed) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if operation.progressCallback then | ||||
|     operation.progressCallback(progress, speed) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onWsOpen(operationId, message) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if operation.callbacks.onOpen then | ||||
|     operation.callbacks.onOpen(message, operationId) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onWsMessage(operationId, message) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if operation.callbacks.onMessage then | ||||
|     if operation.json then | ||||
|       local status, result = pcall(function() return json.decode(message) end) | ||||
|       local err = nil | ||||
|       if not status then | ||||
|         err = "JSON ERROR: " .. result | ||||
|         if message and message:len() > 0 then | ||||
|           err = err .. " (" .. message:sub(1, 100) .. ")" | ||||
|         end | ||||
|       end | ||||
|       if err then | ||||
|         if operation.callbacks.onError then | ||||
|           operation.callbacks.onError(err, operationId) | ||||
|         end         | ||||
|       else | ||||
|         operation.callbacks.onMessage(result, operationId)     | ||||
|       end | ||||
|     else | ||||
|       operation.callbacks.onMessage(message, operationId) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onWsClose(operationId, message) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if operation.callbacks.onClose then | ||||
|     operation.callbacks.onClose(message, operationId) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function HTTP.onWsError(operationId, message) | ||||
|   local operation = HTTP.operations[operationId] | ||||
|   if operation == nil then | ||||
|     return | ||||
|   end | ||||
|   if operation.callbacks.onError then | ||||
|     operation.callbacks.onError(message, operationId) | ||||
|   end | ||||
| end | ||||
|  | ||||
| connect(g_http,  | ||||
|   { | ||||
|     onGet = HTTP.onGet, | ||||
|     onGetProgress = HTTP.onGetProgress, | ||||
|     onPost = HTTP.onPost, | ||||
|     onPostProgress = HTTP.onPostProgress, | ||||
|     onDownload = HTTP.onDownload, | ||||
|     onDownloadProgress = HTTP.onDownloadProgress, | ||||
|     onWsOpen = HTTP.onWsOpen, | ||||
|     onWsMessage = HTTP.onWsMessage, | ||||
|     onWsClose = HTTP.onWsClose, | ||||
|     onWsError = HTTP.onWsError, | ||||
|   }) | ||||
| g_http.setUserAgent(HTTP.agent) | ||||
							
								
								
									
										51
									
								
								SabrehavenOTClient/modules/corelib/inputmessage.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								SabrehavenOTClient/modules/corelib/inputmessage.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| function InputMessage:getData() | ||||
|   local dataType = self:getU8() | ||||
|   if dataType == NetworkMessageTypes.Boolean then | ||||
|     return numbertoboolean(self:getU8()) | ||||
|   elseif dataType == NetworkMessageTypes.U8 then | ||||
|     return self:getU8() | ||||
|   elseif dataType == NetworkMessageTypes.U16 then | ||||
|     return self:getU16() | ||||
|   elseif dataType == NetworkMessageTypes.U32 then | ||||
|     return self:getU32() | ||||
|   elseif dataType == NetworkMessageTypes.U64 then | ||||
|     return self:getU64() | ||||
|   elseif dataType == NetworkMessageTypes.NumberString then | ||||
|     return tonumber(self:getString()) | ||||
|   elseif dataType == NetworkMessageTypes.String then | ||||
|     return self:getString() | ||||
|   elseif dataType == NetworkMessageTypes.Table then | ||||
|     return self:getTable() | ||||
|   else | ||||
|     perror('Unknown data type ' .. dataType) | ||||
|   end | ||||
|   return nil | ||||
| end | ||||
|  | ||||
| function InputMessage:getTable() | ||||
|   local ret = {} | ||||
|   local size = self:getU16() | ||||
|   for i=1,size do | ||||
|     local index = self:getData() | ||||
|     local value = self:getData() | ||||
|     ret[index] = value | ||||
|   end | ||||
|   return ret | ||||
| end | ||||
|  | ||||
| function InputMessage:getColor() | ||||
|   local color = {} | ||||
|   color.r = self:getU8() | ||||
|   color.g = self:getU8() | ||||
|   color.b = self:getU8() | ||||
|   color.a = self:getU8() | ||||
|   return color | ||||
| end | ||||
|  | ||||
| function InputMessage:getPosition() | ||||
|   local position = {} | ||||
|   position.x = self:getU16() | ||||
|   position.y = self:getU16() | ||||
|   position.z = self:getU8() | ||||
|   return position | ||||
| end | ||||
							
								
								
									
										419
									
								
								SabrehavenOTClient/modules/corelib/json.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										419
									
								
								SabrehavenOTClient/modules/corelib/json.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,419 @@ | ||||
| -- | ||||
| -- json.lua | ||||
| -- | ||||
| -- Copyright (c) 2019 rxi | ||||
| -- | ||||
| -- Permission is hereby granted, free of charge, to any person obtaining a copy of | ||||
| -- this software and associated documentation files (the "Software"), to deal in | ||||
| -- the Software without restriction, including without limitation the rights to | ||||
| -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||||
| -- of the Software, and to permit persons to whom the Software is furnished to do | ||||
| -- so, subject to the following conditions: | ||||
| -- | ||||
| -- The above copyright notice and this permission notice shall be included in all | ||||
| -- copies or substantial portions of the Software. | ||||
| -- | ||||
| -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| -- SOFTWARE. | ||||
| -- | ||||
|  | ||||
| json = { _version = "0.1.1" } | ||||
|  | ||||
| ------------------------------------------------------------------------------- | ||||
| -- Encode | ||||
| ------------------------------------------------------------------------------- | ||||
|  | ||||
| local encode | ||||
|  | ||||
| local escape_char_map = { | ||||
|   [ "\\" ] = "\\\\", | ||||
|   [ "\"" ] = "\\\"", | ||||
|   [ "\b" ] = "\\b", | ||||
|   [ "\f" ] = "\\f", | ||||
|   [ "\n" ] = "\\n", | ||||
|   [ "\r" ] = "\\r", | ||||
|   [ "\t" ] = "\\t", | ||||
| } | ||||
|  | ||||
| local escape_char_map_inv = { [ "\\/" ] = "/" } | ||||
| for k, v in pairs(escape_char_map) do | ||||
|   escape_char_map_inv[v] = k | ||||
| end | ||||
|  | ||||
|  | ||||
| local function make_indent(state) | ||||
|   return string.rep(" ", state.currentIndentLevel * state.indent) | ||||
| end | ||||
|  | ||||
|  | ||||
| local function escape_char(c) | ||||
|   return escape_char_map[c] or string.format("\\u%04x", c:byte()) | ||||
| end | ||||
|  | ||||
|  | ||||
| local function encode_nil() | ||||
|   return "null" | ||||
| end | ||||
|  | ||||
|  | ||||
| local function encode_table(val, state) | ||||
|   local res = {} | ||||
|   local stack = state.stack | ||||
|   local pretty = state.indent > 0 | ||||
|  | ||||
|   local close_indent = make_indent(state) | ||||
|   local comma = pretty and ",\n" or "," | ||||
|   local colon = pretty and ": " or ":" | ||||
|   local open_brace = pretty and "{\n" or "{" | ||||
|   local close_brace = pretty and ("\n" .. close_indent .. "}") or "}" | ||||
|   local open_bracket = pretty and "[\n" or "[" | ||||
|   local close_bracket = pretty and ("\n" .. close_indent .. "]") or "]" | ||||
|  | ||||
|   -- Circular reference? | ||||
|   if stack[val] then error("circular reference") end | ||||
|  | ||||
|   stack[val] = true | ||||
|  | ||||
|   if rawget(val, 1) ~= nil or next(val) == nil then | ||||
|     -- Treat as array -- check keys are valid and it is not sparse | ||||
|     local n = 0 | ||||
|     for k in pairs(val) do | ||||
|       if type(k) ~= "number" then | ||||
|         error("invalid table: mixed or invalid key types") | ||||
|       end | ||||
|       n = n + 1 | ||||
|     end | ||||
|     if n ~= #val then | ||||
|       error("invalid table: sparse array") | ||||
|     end | ||||
|     -- Encode | ||||
|     for _, v in ipairs(val) do | ||||
|       state.currentIndentLevel = state.currentIndentLevel + 1 | ||||
|       table.insert(res, make_indent(state) .. encode(v, state)) | ||||
|       state.currentIndentLevel = state.currentIndentLevel - 1 | ||||
|     end | ||||
|     stack[val] = nil | ||||
|     return open_bracket .. table.concat(res, comma) .. close_bracket | ||||
|  | ||||
|   else | ||||
|     -- Treat as an object | ||||
|     for k, v in pairs(val) do | ||||
|       if type(k) ~= "string" then | ||||
|         error("invalid table: mixed or invalid key types") | ||||
|       end | ||||
|       state.currentIndentLevel = state.currentIndentLevel + 1 | ||||
|       table.insert(res, make_indent(state) .. encode(k, state) .. colon .. encode(v, state)) | ||||
|       state.currentIndentLevel = state.currentIndentLevel - 1 | ||||
|     end | ||||
|     stack[val] = nil | ||||
|     return open_brace .. table.concat(res, comma) .. close_brace | ||||
|   end | ||||
| end | ||||
|  | ||||
|  | ||||
| local function encode_string(val) | ||||
|   return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' | ||||
| end | ||||
|  | ||||
|  | ||||
| local function encode_number(val) | ||||
|   -- Check for NaN, -inf and inf | ||||
|   if val ~= val or val <= -math.huge or val >= math.huge then | ||||
|     error("unexpected number value '" .. tostring(val) .. "'") | ||||
|   end | ||||
|   return string.format("%.14g", val) | ||||
| end | ||||
|  | ||||
|  | ||||
| local type_func_map = { | ||||
|   [ "nil"     ] = encode_nil, | ||||
|   [ "table"   ] = encode_table, | ||||
|   [ "string"  ] = encode_string, | ||||
|   [ "number"  ] = encode_number, | ||||
|   [ "boolean" ] = tostring, | ||||
| } | ||||
|  | ||||
|  | ||||
| encode = function(val, state) | ||||
|   local t = type(val) | ||||
|   local f = type_func_map[t] | ||||
|   if f then | ||||
|     return f(val, state) | ||||
|   end | ||||
|   error("unexpected type '" .. t .. "'") | ||||
| end | ||||
|  | ||||
| function json.encode(val, indent) | ||||
|   local state = { | ||||
|     indent = indent or 0, | ||||
|     currentIndentLevel = 0, | ||||
|     stack = {} | ||||
|   } | ||||
|   return encode(val, state) | ||||
| end | ||||
|  | ||||
|  | ||||
| ------------------------------------------------------------------------------- | ||||
| -- Decode | ||||
| ------------------------------------------------------------------------------- | ||||
|  | ||||
| local parse | ||||
|  | ||||
| local function create_set(...) | ||||
|   local res = {} | ||||
|   for i = 1, select("#", ...) do | ||||
|     res[ select(i, ...) ] = true | ||||
|   end | ||||
|   return res | ||||
| end | ||||
|  | ||||
| local space_chars   = create_set(" ", "\t", "\r", "\n") | ||||
| local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",") | ||||
| local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") | ||||
| local literals      = create_set("true", "false", "null") | ||||
|  | ||||
| local literal_map = { | ||||
|   [ "true"  ] = true, | ||||
|   [ "false" ] = false, | ||||
|   [ "null"  ] = nil, | ||||
| } | ||||
|  | ||||
|  | ||||
| local function next_char(str, idx, set, negate) | ||||
|   for i = idx, #str do | ||||
|     if set[str:sub(i, i)] ~= negate then | ||||
|       return i | ||||
|     end | ||||
|   end | ||||
|   return #str + 1 | ||||
| end | ||||
|  | ||||
|  | ||||
| local function decode_error(str, idx, msg) | ||||
|   local line_count = 1 | ||||
|   local col_count = 1 | ||||
|   for i = 1, idx - 1 do | ||||
|     col_count = col_count + 1 | ||||
|     if str:sub(i, i) == "\n" then | ||||
|       line_count = line_count + 1 | ||||
|       col_count = 1 | ||||
|     end | ||||
|   end | ||||
|   error( string.format("%s at line %d col %d", msg, line_count, col_count) ) | ||||
| end | ||||
|  | ||||
|  | ||||
| local function codepoint_to_utf8(n) | ||||
|   -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa | ||||
|   local f = math.floor | ||||
|   if n <= 0x7f then | ||||
|     return string.char(n) | ||||
|   elseif n <= 0x7ff then | ||||
|     return string.char(f(n / 64) + 192, n % 64 + 128) | ||||
|   elseif n <= 0xffff then | ||||
|     return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) | ||||
|   elseif n <= 0x10ffff then | ||||
|     return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, | ||||
|                        f(n % 4096 / 64) + 128, n % 64 + 128) | ||||
|   end | ||||
|   error( string.format("invalid unicode codepoint '%x'", n) ) | ||||
| end | ||||
|  | ||||
|  | ||||
| local function parse_unicode_escape(s) | ||||
|   local n1 = tonumber( s:sub(3, 6),  16 ) | ||||
|   local n2 = tonumber( s:sub(9, 12), 16 ) | ||||
|   -- Surrogate pair? | ||||
|   if n2 then | ||||
|     return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) | ||||
|   else | ||||
|     return codepoint_to_utf8(n1) | ||||
|   end | ||||
| end | ||||
|  | ||||
|  | ||||
| local function parse_string(str, i) | ||||
|   local has_unicode_escape = false | ||||
|   local has_surrogate_escape = false | ||||
|   local has_escape = false | ||||
|   local last | ||||
|   for j = i + 1, #str do | ||||
|     local x = str:byte(j) | ||||
|  | ||||
|     if x < 32 then | ||||
|       decode_error(str, j, "control character in string") | ||||
|     end | ||||
|  | ||||
|     if last == 92 then -- "\\" (escape char) | ||||
|       if x == 117 then -- "u" (unicode escape sequence) | ||||
|         local hex = str:sub(j + 1, j + 5) | ||||
|         if not hex:find("%x%x%x%x") then | ||||
|           decode_error(str, j, "invalid unicode escape in string") | ||||
|         end | ||||
|         if hex:find("^[dD][89aAbB]") then | ||||
|           has_surrogate_escape = true | ||||
|         else | ||||
|           has_unicode_escape = true | ||||
|         end | ||||
|       else | ||||
|         local c = string.char(x) | ||||
|         if not escape_chars[c] then | ||||
|           decode_error(str, j, "invalid escape char '" .. c .. "' in string") | ||||
|         end | ||||
|         has_escape = true | ||||
|       end | ||||
|       last = nil | ||||
|  | ||||
|     elseif x == 34 then -- '"' (end of string) | ||||
|       local s = str:sub(i + 1, j - 1) | ||||
|       if has_surrogate_escape then | ||||
|         s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) | ||||
|       end | ||||
|       if has_unicode_escape then | ||||
|         s = s:gsub("\\u....", parse_unicode_escape) | ||||
|       end | ||||
|       if has_escape then | ||||
|         s = s:gsub("\\.", escape_char_map_inv) | ||||
|       end | ||||
|       return s, j + 1 | ||||
|  | ||||
|     else | ||||
|       last = x | ||||
|     end | ||||
|   end | ||||
|   decode_error(str, i, "expected closing quote for string") | ||||
| end | ||||
|  | ||||
|  | ||||
| local function parse_number(str, i) | ||||
|   local x = next_char(str, i, delim_chars) | ||||
|   local s = str:sub(i, x - 1) | ||||
|   local n = tonumber(s) | ||||
|   if not n then | ||||
|     decode_error(str, i, "invalid number '" .. s .. "'") | ||||
|   end | ||||
|   return n, x | ||||
| end | ||||
|  | ||||
|  | ||||
| local function parse_literal(str, i) | ||||
|   local x = next_char(str, i, delim_chars) | ||||
|   local word = str:sub(i, x - 1) | ||||
|   if not literals[word] then | ||||
|     decode_error(str, i, "invalid literal '" .. word .. "'") | ||||
|   end | ||||
|   return literal_map[word], x | ||||
| end | ||||
|  | ||||
|  | ||||
| local function parse_array(str, i) | ||||
|   local res = {} | ||||
|   local n = 1 | ||||
|   i = i + 1 | ||||
|   while 1 do | ||||
|     local x | ||||
|     i = next_char(str, i, space_chars, true) | ||||
|     -- Empty / end of array? | ||||
|     if str:sub(i, i) == "]" then | ||||
|       i = i + 1 | ||||
|       break | ||||
|     end | ||||
|     -- Read token | ||||
|     x, i = parse(str, i) | ||||
|     res[n] = x | ||||
|     n = n + 1 | ||||
|     -- Next token | ||||
|     i = next_char(str, i, space_chars, true) | ||||
|     local chr = str:sub(i, i) | ||||
|     i = i + 1 | ||||
|     if chr == "]" then break end | ||||
|     if chr ~= "," then decode_error(str, i, "expected ']' or ','") end | ||||
|   end | ||||
|   return res, i | ||||
| end | ||||
|  | ||||
|  | ||||
| local function parse_object(str, i) | ||||
|   local res = {} | ||||
|   i = i + 1 | ||||
|   while 1 do | ||||
|     local key, val | ||||
|     i = next_char(str, i, space_chars, true) | ||||
|     -- Empty / end of object? | ||||
|     if str:sub(i, i) == "}" then | ||||
|       i = i + 1 | ||||
|       break | ||||
|     end | ||||
|     -- Read key | ||||
|     if str:sub(i, i) ~= '"' then | ||||
|       decode_error(str, i, "expected string for key") | ||||
|     end | ||||
|     key, i = parse(str, i) | ||||
|     -- Read ':' delimiter | ||||
|     i = next_char(str, i, space_chars, true) | ||||
|     if str:sub(i, i) ~= ":" then | ||||
|       decode_error(str, i, "expected ':' after key") | ||||
|     end | ||||
|     i = next_char(str, i + 1, space_chars, true) | ||||
|     -- Read value | ||||
|     val, i = parse(str, i) | ||||
|     -- Set | ||||
|     res[key] = val | ||||
|     -- Next token | ||||
|     i = next_char(str, i, space_chars, true) | ||||
|     local chr = str:sub(i, i) | ||||
|     i = i + 1 | ||||
|     if chr == "}" then break end | ||||
|     if chr ~= "," then decode_error(str, i, "expected '}' or ','") end | ||||
|   end | ||||
|   return res, i | ||||
| end | ||||
|  | ||||
|  | ||||
| local char_func_map = { | ||||
|   [ '"' ] = parse_string, | ||||
|   [ "0" ] = parse_number, | ||||
|   [ "1" ] = parse_number, | ||||
|   [ "2" ] = parse_number, | ||||
|   [ "3" ] = parse_number, | ||||
|   [ "4" ] = parse_number, | ||||
|   [ "5" ] = parse_number, | ||||
|   [ "6" ] = parse_number, | ||||
|   [ "7" ] = parse_number, | ||||
|   [ "8" ] = parse_number, | ||||
|   [ "9" ] = parse_number, | ||||
|   [ "-" ] = parse_number, | ||||
|   [ "t" ] = parse_literal, | ||||
|   [ "f" ] = parse_literal, | ||||
|   [ "n" ] = parse_literal, | ||||
|   [ "[" ] = parse_array, | ||||
|   [ "{" ] = parse_object, | ||||
| } | ||||
|  | ||||
|  | ||||
| parse = function(str, idx) | ||||
|   local chr = str:sub(idx, idx) | ||||
|   local f = char_func_map[chr] | ||||
|   if f then | ||||
|     return f(str, idx) | ||||
|   end | ||||
|   decode_error(str, idx, "unexpected character '" .. chr .. "'") | ||||
| end | ||||
|  | ||||
|  | ||||
| function json.decode(str) | ||||
|   if type(str) ~= "string" then | ||||
|     error("expected argument of type string, got " .. type(str)) | ||||
|   end | ||||
|   local res, idx = parse(str, next_char(str, 1, space_chars, true)) | ||||
|   idx = next_char(str, idx, space_chars, true) | ||||
|   if idx <= #str then | ||||
|     decode_error(str, idx, "trailing garbage") | ||||
|   end | ||||
|   return res | ||||
| end | ||||
							
								
								
									
										251
									
								
								SabrehavenOTClient/modules/corelib/keyboard.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								SabrehavenOTClient/modules/corelib/keyboard.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,251 @@ | ||||
| -- @docclass | ||||
| g_keyboard = {} | ||||
|  | ||||
| -- private functions | ||||
| function translateKeyCombo(keyCombo) | ||||
|   if not keyCombo or #keyCombo == 0 then return nil end | ||||
|   local keyComboDesc = '' | ||||
|   for k,v in pairs(keyCombo) do | ||||
|     local keyDesc = KeyCodeDescs[v] | ||||
|     if keyDesc == nil then return nil end | ||||
|     keyComboDesc = keyComboDesc .. '+' .. keyDesc | ||||
|   end | ||||
|   keyComboDesc = keyComboDesc:sub(2) | ||||
|   return keyComboDesc | ||||
| end | ||||
|  | ||||
| local function getKeyCode(key) | ||||
|   for keyCode, keyDesc in pairs(KeyCodeDescs) do | ||||
|     if keyDesc:lower() == key:trim():lower() then | ||||
|       return keyCode | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function retranslateKeyComboDesc(keyComboDesc) | ||||
|   if keyComboDesc == nil then | ||||
|     error('Unable to translate key combo \'' .. keyComboDesc .. '\'') | ||||
|   end | ||||
|  | ||||
|   if type(keyComboDesc) == 'number' then | ||||
|     keyComboDesc = tostring(keyComboDesc) | ||||
|   end | ||||
|  | ||||
|   local keyCombo = {} | ||||
|   for i,currentKeyDesc in ipairs(keyComboDesc:split('+')) do | ||||
|     for keyCode, keyDesc in pairs(KeyCodeDescs) do | ||||
|       if keyDesc:lower() == currentKeyDesc:trim():lower() then | ||||
|         table.insert(keyCombo, keyCode) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   return translateKeyCombo(keyCombo) | ||||
| end | ||||
|  | ||||
| function determineKeyComboDesc(keyCode, keyboardModifiers) | ||||
|   local keyCombo = {} | ||||
|   if keyCode == KeyCtrl or keyCode == KeyShift or keyCode == KeyAlt then | ||||
|     table.insert(keyCombo, keyCode) | ||||
|   elseif KeyCodeDescs[keyCode] ~= nil then | ||||
|     if keyboardModifiers == KeyboardCtrlModifier then | ||||
|       table.insert(keyCombo, KeyCtrl) | ||||
|     elseif keyboardModifiers == KeyboardAltModifier then | ||||
|       table.insert(keyCombo, KeyAlt) | ||||
|     elseif keyboardModifiers == KeyboardCtrlAltModifier then | ||||
|       table.insert(keyCombo, KeyCtrl) | ||||
|       table.insert(keyCombo, KeyAlt) | ||||
|     elseif keyboardModifiers == KeyboardShiftModifier then | ||||
|       table.insert(keyCombo, KeyShift) | ||||
|     elseif keyboardModifiers == KeyboardCtrlShiftModifier then | ||||
|       table.insert(keyCombo, KeyCtrl) | ||||
|       table.insert(keyCombo, KeyShift) | ||||
|     elseif keyboardModifiers == KeyboardAltShiftModifier then | ||||
|       table.insert(keyCombo, KeyAlt) | ||||
|       table.insert(keyCombo, KeyShift) | ||||
|     elseif keyboardModifiers == KeyboardCtrlAltShiftModifier then | ||||
|       table.insert(keyCombo, KeyCtrl) | ||||
|       table.insert(keyCombo, KeyAlt) | ||||
|       table.insert(keyCombo, KeyShift) | ||||
|     end | ||||
|     table.insert(keyCombo, keyCode) | ||||
|   end | ||||
|   return translateKeyCombo(keyCombo) | ||||
| end | ||||
|  | ||||
| local function onWidgetKeyDown(widget, keyCode, keyboardModifiers) | ||||
|   if keyCode == KeyUnknown then return false end | ||||
|   local callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)] | ||||
|   signalcall(callback, widget, keyCode) | ||||
|   callback = widget.boundKeyDownCombos[determineKeyComboDesc(keyCode, keyboardModifiers)] | ||||
|   return signalcall(callback, widget, keyCode) | ||||
| end | ||||
|  | ||||
| local function onWidgetKeyUp(widget, keyCode, keyboardModifiers) | ||||
|   if keyCode == KeyUnknown then return false end | ||||
|   local callback = widget.boundAloneKeyUpCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)] | ||||
|   signalcall(callback, widget, keyCode) | ||||
|   callback = widget.boundKeyUpCombos[determineKeyComboDesc(keyCode, keyboardModifiers)] | ||||
|   return signalcall(callback, widget, keyCode) | ||||
| end | ||||
|  | ||||
| local function onWidgetKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks) | ||||
|   if keyCode == KeyUnknown then return false end | ||||
|   local callback = widget.boundKeyPressCombos[determineKeyComboDesc(keyCode, keyboardModifiers)] | ||||
|   return signalcall(callback, widget, keyCode, autoRepeatTicks) | ||||
| end | ||||
|  | ||||
| local function connectKeyDownEvent(widget) | ||||
|   if widget.boundKeyDownCombos then return end | ||||
|   connect(widget, { onKeyDown = onWidgetKeyDown }) | ||||
|   widget.boundKeyDownCombos = {} | ||||
|   widget.boundAloneKeyDownCombos = {} | ||||
| end | ||||
|  | ||||
| local function connectKeyUpEvent(widget) | ||||
|   if widget.boundKeyUpCombos then return end | ||||
|   connect(widget, { onKeyUp = onWidgetKeyUp }) | ||||
|   widget.boundKeyUpCombos = {} | ||||
|   widget.boundAloneKeyUpCombos = {} | ||||
| end | ||||
|  | ||||
| local function connectKeyPressEvent(widget) | ||||
|   if widget.boundKeyPressCombos then return end | ||||
|   connect(widget, { onKeyPress = onWidgetKeyPress }) | ||||
|   widget.boundKeyPressCombos = {} | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function g_keyboard.bindKeyDown(keyComboDesc, callback, widget, alone) | ||||
|   widget = widget or rootWidget | ||||
|   connectKeyDownEvent(widget) | ||||
|   local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) | ||||
|   if alone then | ||||
|     connect(widget.boundAloneKeyDownCombos, keyComboDesc, callback) | ||||
|   else | ||||
|     connect(widget.boundKeyDownCombos, keyComboDesc, callback) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function g_keyboard.bindKeyUp(keyComboDesc, callback, widget, alone) | ||||
|   widget = widget or rootWidget | ||||
|   connectKeyUpEvent(widget) | ||||
|   local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) | ||||
|   if alone then | ||||
|     connect(widget.boundAloneKeyUpCombos, keyComboDesc, callback) | ||||
|   else | ||||
|     connect(widget.boundKeyUpCombos, keyComboDesc, callback) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function g_keyboard.bindKeyPress(keyComboDesc, callback, widget) | ||||
|   widget = widget or rootWidget | ||||
|   connectKeyPressEvent(widget) | ||||
|   local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) | ||||
|   connect(widget.boundKeyPressCombos, keyComboDesc, callback) | ||||
| end | ||||
|  | ||||
| local function getUnbindArgs(arg1, arg2) | ||||
|   local callback | ||||
|   local widget | ||||
|   if type(arg1) == 'function' then callback = arg1 | ||||
|   elseif type(arg2) == 'function' then callback = arg2 end | ||||
|   if type(arg1) == 'userdata' then widget = arg1 | ||||
|   elseif type(arg2) == 'userdata' then widget = arg2 end | ||||
|   widget = widget or rootWidget | ||||
|   return callback, widget | ||||
| end | ||||
|  | ||||
| function g_keyboard.unbindKeyDown(keyComboDesc, arg1, arg2) | ||||
|   local callback, widget = getUnbindArgs(arg1, arg2) | ||||
|   if widget.boundKeyDownCombos == nil then return end | ||||
|   local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) | ||||
|   disconnect(widget.boundKeyDownCombos, keyComboDesc, callback) | ||||
| end | ||||
|  | ||||
| function g_keyboard.unbindKeyUp(keyComboDesc, arg1, arg2) | ||||
|   local callback, widget = getUnbindArgs(arg1, arg2) | ||||
|   if widget.boundKeyUpCombos == nil then return end | ||||
|   local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) | ||||
|   disconnect(widget.boundKeyUpCombos, keyComboDesc, callback) | ||||
| end | ||||
|  | ||||
| function g_keyboard.unbindKeyPress(keyComboDesc, arg1, arg2) | ||||
|   local callback, widget = getUnbindArgs(arg1, arg2) | ||||
|   if widget.boundKeyPressCombos == nil then return end | ||||
|   local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) | ||||
|   disconnect(widget.boundKeyPressCombos, keyComboDesc, callback) | ||||
| end | ||||
|  | ||||
| function g_keyboard.getModifiers() | ||||
|   return g_window.getKeyboardModifiers() | ||||
| end | ||||
|  | ||||
| function g_keyboard.isKeyPressed(key) | ||||
|   if type(key) == 'string' then | ||||
|     key = getKeyCode(key) | ||||
|   end | ||||
|   return g_window.isKeyPressed(key) | ||||
| end | ||||
|  | ||||
| function g_keyboard.areKeysPressed(keyComboDesc) | ||||
|   for i,currentKeyDesc in ipairs(keyComboDesc:split('+')) do | ||||
|     for keyCode, keyDesc in pairs(KeyCodeDescs) do | ||||
|       if keyDesc:lower() == currentKeyDesc:trim():lower() then | ||||
|         if keyDesc:lower() == "ctrl" then  | ||||
|           if not g_keyboard.isCtrlPressed() then | ||||
|             return false | ||||
|           end | ||||
|         elseif keyDesc:lower() == "shift" then  | ||||
|           if not g_keyboard.isShiftPressed() then | ||||
|             return false | ||||
|           end               | ||||
|         elseif keyDesc:lower() == "alt" then  | ||||
|           if not g_keyboard.isAltPressed() then | ||||
|             return false | ||||
|           end               | ||||
|         elseif not g_window.isKeyPressed(keyCode) then | ||||
|           return false | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function g_keyboard.isKeySetPressed(keys, all) | ||||
|   all = all or false | ||||
|   local result = {} | ||||
|   for k,v in pairs(keys) do | ||||
|     if type(v) == 'string' then | ||||
|       v = getKeyCode(v) | ||||
|     end | ||||
|     if g_window.isKeyPressed(v) then | ||||
|       if not all then | ||||
|         return true | ||||
|       end | ||||
|       table.insert(result, true) | ||||
|     end | ||||
|   end | ||||
|   return #result == #keys | ||||
| end | ||||
|  | ||||
| function g_keyboard.isInUse() | ||||
|   for i = FirstKey, LastKey do | ||||
|     if g_window.isKeyPressed(key) then | ||||
|       return true | ||||
|     end | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function g_keyboard.isCtrlPressed() | ||||
|   return bit32.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0 | ||||
| end | ||||
|  | ||||
| function g_keyboard.isAltPressed() | ||||
|   return bit32.band(g_window.getKeyboardModifiers(), KeyboardAltModifier) ~= 0 | ||||
| end | ||||
|  | ||||
| function g_keyboard.isShiftPressed() | ||||
|   return bit32.band(g_window.getKeyboardModifiers(), KeyboardShiftModifier) ~= 0 | ||||
| end | ||||
							
								
								
									
										35
									
								
								SabrehavenOTClient/modules/corelib/math.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								SabrehavenOTClient/modules/corelib/math.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| -- @docclass math | ||||
|  | ||||
| local U8  = 2^8 | ||||
| local U16 = 2^16 | ||||
| local U32 = 2^32 | ||||
| local U64 = 2^64 | ||||
|  | ||||
| function math.round(num, idp) | ||||
|   local mult = 10^(idp or 0) | ||||
|   if num >= 0 then | ||||
|     return math.floor(num * mult + 0.5) / mult | ||||
|   else | ||||
|     return math.ceil(num * mult - 0.5) / mult | ||||
|   end | ||||
| end | ||||
|  | ||||
| function math.isu8(num) | ||||
|   return math.isinteger(num) and num >= 0 and num < U8 | ||||
| end | ||||
|  | ||||
| function math.isu16(num) | ||||
|   return math.isinteger(num) and num >= U8 and num < U16 | ||||
| end | ||||
|  | ||||
| function math.isu32(num) | ||||
|   return math.isinteger(num) and num >= U16 and num < U32 | ||||
| end | ||||
|  | ||||
| function math.isu64(num) | ||||
|   return math.isinteger(num) and num >= U32 and num < U64 | ||||
| end | ||||
|  | ||||
| function math.isinteger(num) | ||||
|   return ((type(num) == 'number') and (num == math.floor(num))) | ||||
| end | ||||
							
								
								
									
										36
									
								
								SabrehavenOTClient/modules/corelib/mouse.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								SabrehavenOTClient/modules/corelib/mouse.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| -- @docclass | ||||
| function g_mouse.bindAutoPress(widget, callback, delay, button) | ||||
|   local button = button or MouseLeftButton | ||||
|   connect(widget, { onMousePress = function(widget, mousePos, mouseButton) | ||||
|     if mouseButton ~= button then | ||||
|       return false | ||||
|     end | ||||
|     local startTime = g_clock.millis() | ||||
|     callback(widget, mousePos, mouseButton, 0) | ||||
|     periodicalEvent(function() | ||||
|       callback(widget, g_window.getMousePosition(), mouseButton, g_clock.millis() - startTime) | ||||
|     end, function() | ||||
|       return g_mouse.isPressed(mouseButton) | ||||
|     end, 30, delay) | ||||
|     return true | ||||
|   end }) | ||||
| end | ||||
|  | ||||
| function g_mouse.bindPressMove(widget, callback) | ||||
|   connect(widget, { onMouseMove = function(widget, mousePos, mouseMoved) | ||||
|     if widget:isPressed() then | ||||
|       callback(mousePos, mouseMoved) | ||||
|       return true | ||||
|     end | ||||
|   end }) | ||||
| end | ||||
|  | ||||
| function g_mouse.bindPress(widget, callback, button) | ||||
|   connect(widget, { onMousePress = function(widget, mousePos, mouseButton) | ||||
|     if not button or button == mouseButton then | ||||
|       callback(mousePos, mouseButton) | ||||
|       return true | ||||
|     end | ||||
|     return false | ||||
|   end }) | ||||
| end | ||||
							
								
								
									
										16
									
								
								SabrehavenOTClient/modules/corelib/net.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								SabrehavenOTClient/modules/corelib/net.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| function translateNetworkError(errcode, connecting, errdesc) | ||||
|   local text | ||||
|   if errcode == 111 then | ||||
|     text = tr('Connection refused, the server might be offline or restarting.\nPlease try again later.') | ||||
|   elseif errcode == 110 then | ||||
|     text = tr('Connection timed out. Either your network is failing or the server is offline.') | ||||
|   elseif errcode == 1 then | ||||
|     text = tr('Connection failed, the server address does not exist.') | ||||
|   elseif connecting then | ||||
|     text = tr('Connection failed.') | ||||
|   else | ||||
|     text = tr('Your connection has been lost.\nEither your network or the server went down.') | ||||
|   end | ||||
|   text = text .. ' ' .. tr('(ERROR %d)', errcode) | ||||
|   return text | ||||
| end | ||||
							
								
								
									
										43
									
								
								SabrehavenOTClient/modules/corelib/orderedtable.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								SabrehavenOTClient/modules/corelib/orderedtable.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| function __genOrderedIndex( t ) | ||||
|     local orderedIndex = {} | ||||
|     for key in pairs(t) do | ||||
|         table.insert( orderedIndex, key ) | ||||
|     end | ||||
|     table.sort( orderedIndex ) | ||||
|     return orderedIndex | ||||
| end | ||||
|  | ||||
| function orderedNext(t, state) | ||||
|     -- Equivalent of the next function, but returns the keys in the alphabetic | ||||
|     -- order. We use a temporary ordered key table that is stored in the | ||||
|     -- table being iterated. | ||||
|  | ||||
|     local key = nil | ||||
|     --print("orderedNext: state = "..tostring(state) ) | ||||
|     if state == nil then | ||||
|         -- the first time, generate the index | ||||
|         t.__orderedIndex = __genOrderedIndex( t ) | ||||
|         key = t.__orderedIndex[1] | ||||
|     else | ||||
|         -- fetch the next value | ||||
|         for i = 1,table.getn(t.__orderedIndex) do | ||||
|             if t.__orderedIndex[i] == state then | ||||
|                 key = t.__orderedIndex[i+1] | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     if key then | ||||
|         return key, t[key] | ||||
|     end | ||||
|  | ||||
|     -- no more value to return, cleanup | ||||
|     t.__orderedIndex = nil | ||||
|     return | ||||
| end | ||||
|  | ||||
| function orderedPairs(t) | ||||
|     -- Equivalent of the pairs() function on tables. Allows to iterate | ||||
|     -- in order | ||||
|     return orderedNext, t, nil | ||||
| end | ||||
							
								
								
									
										69
									
								
								SabrehavenOTClient/modules/corelib/outputmessage.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								SabrehavenOTClient/modules/corelib/outputmessage.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| function OutputMessage:addData(data) | ||||
|   if type(data) == 'boolean' then | ||||
|     self:addU8(NetworkMessageTypes.Boolean) | ||||
|     self:addU8(booleantonumber(data)) | ||||
|   elseif type(data) == 'number' then | ||||
|     if math.isu8(data) then | ||||
|       self:addU8(NetworkMessageTypes.U8) | ||||
|       self:addU8(data) | ||||
|     elseif math.isu16(data) then | ||||
|       self:addU8(NetworkMessageTypes.U16) | ||||
|       self:addU16(data) | ||||
|     elseif math.isu32(data) then | ||||
|       self:addU8(NetworkMessageTypes.U32) | ||||
|       self:addU32(data) | ||||
|     elseif math.isu64(data) then | ||||
|       self:addU8(NetworkMessageTypes.U64) | ||||
|       self:addU64(data) | ||||
|     else -- negative or non integer numbers | ||||
|       self:addU8(NetworkMessageTypes.NumberString) | ||||
|       self:addString(tostring(data)) | ||||
|     end | ||||
|   elseif type(data) == 'string' then | ||||
|     self:addU8(NetworkMessageTypes.String) | ||||
|     self:addString(data) | ||||
|   elseif type(data) == 'table' then | ||||
|     self:addU8(NetworkMessageTypes.Table) | ||||
|     self:addTable(data) | ||||
|   else | ||||
|     perror('Invalid data type '  .. type(data)) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function OutputMessage:addTable(data) | ||||
|   local size = 0 | ||||
|  | ||||
|   -- reserve for size (should be addData, find a way to use it further) | ||||
|   local sizePos = self:getWritePos() | ||||
|   self:addU16(size) | ||||
|   local sizeSize = self:getWritePos() - sizePos | ||||
|  | ||||
|   -- add values | ||||
|   for key,value in pairs(data) do | ||||
|     self:addData(key) | ||||
|     self:addData(value) | ||||
|     size = size + 1 | ||||
|   end | ||||
|  | ||||
|   -- write size | ||||
|   local currentPos = self:getWritePos() | ||||
|   self:setWritePos(sizePos) | ||||
|   self:addU16(size) | ||||
|  | ||||
|   -- fix msg size and go back to end | ||||
|   self:setMessageSize(self:getMessageSize() - sizeSize) | ||||
|   self:setWritePos(currentPos) | ||||
| end | ||||
|  | ||||
| function OutputMessage:addColor(color) | ||||
|   self:addU8(color.r) | ||||
|   self:addU8(color.g) | ||||
|   self:addU8(color.b) | ||||
|   self:addU8(color.a) | ||||
| end | ||||
|  | ||||
| function OutputMessage:addPosition(position) | ||||
|   self:addU16(position.x) | ||||
|   self:addU16(position.y) | ||||
|   self:addU8(position.z) | ||||
| end | ||||
							
								
								
									
										3
									
								
								SabrehavenOTClient/modules/corelib/settings.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								SabrehavenOTClient/modules/corelib/settings.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| g_settings = makesingleton(g_configs.getSettings()) | ||||
|  | ||||
| -- Reserved for future functionality | ||||
							
								
								
									
										59
									
								
								SabrehavenOTClient/modules/corelib/string.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								SabrehavenOTClient/modules/corelib/string.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| -- @docclass string | ||||
|  | ||||
| function string:split(delim) | ||||
|   local start = 1 | ||||
|   local results = {} | ||||
|   while true do | ||||
|     local pos = string.find(self, delim, start, true) | ||||
|     if not pos then | ||||
|       break | ||||
|     end | ||||
|     table.insert(results, string.sub(self, start, pos-1)) | ||||
|     start = pos + string.len(delim) | ||||
|   end | ||||
|   table.insert(results, string.sub(self, start)) | ||||
|   table.removevalue(results, '') | ||||
|   return results | ||||
| end | ||||
|  | ||||
| function string:starts(start) | ||||
|   return string.sub(self, 1, #start) == start | ||||
| end | ||||
|  | ||||
| function string:ends(test) | ||||
|    return test =='' or string.sub(self,-string.len(test)) == test | ||||
| end | ||||
|  | ||||
| function string:trim() | ||||
|   return string.match(self, '^%s*(.*%S)') or '' | ||||
| end | ||||
|  | ||||
| function string:explode(sep, limit) | ||||
|   if type(sep) ~= 'string' or tostring(self):len() == 0 or sep:len() == 0 then | ||||
|     return {} | ||||
|   end | ||||
|  | ||||
|   local i, pos, tmp, t = 0, 1, "", {} | ||||
|   for s, e in function() return string.find(self, sep, pos) end do | ||||
|     tmp = self:sub(pos, s - 1):trim() | ||||
|     table.insert(t, tmp) | ||||
|     pos = e + 1 | ||||
|  | ||||
|     i = i + 1 | ||||
|     if limit ~= nil and i == limit then | ||||
|       break | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   tmp = self:sub(pos):trim() | ||||
|   table.insert(t, tmp) | ||||
|   return t | ||||
| end | ||||
|  | ||||
| function string:contains(str, checkCase, start, plain) | ||||
|   if(not checkCase) then | ||||
|     self = self:lower() | ||||
|     str = str:lower() | ||||
|   end | ||||
|   return string.find(self, str, start and start or 1, plain == nil and true or false) | ||||
| end | ||||
							
								
								
									
										173
									
								
								SabrehavenOTClient/modules/corelib/struct.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								SabrehavenOTClient/modules/corelib/struct.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| Struct = {} | ||||
|  | ||||
| function Struct.pack(format, ...) | ||||
|   local stream = {} | ||||
|   local vars = {...} | ||||
|   local endianness = true | ||||
|  | ||||
|   for i = 1, format:len() do | ||||
|     local opt = format:sub(i, i) | ||||
|  | ||||
|     if opt == '>' then | ||||
|       endianness = false | ||||
|     elseif opt:find('[bBhHiIlL]') then | ||||
|       local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 | ||||
|       local val = tonumber(table.remove(vars, 1)) | ||||
|  | ||||
|       if val < 0 then | ||||
|         val = val + 2 ^ (n * 8 - 1) | ||||
|       end | ||||
|  | ||||
|       local bytes = {} | ||||
|       for j = 1, n do | ||||
|         table.insert(bytes, string.char(val % (2 ^ 8))) | ||||
|         val = math.floor(val / (2 ^ 8)) | ||||
|       end | ||||
|  | ||||
|       if not endianness then | ||||
|         table.insert(stream, string.reverse(table.concat(bytes))) | ||||
|       else | ||||
|         table.insert(stream, table.concat(bytes)) | ||||
|       end | ||||
|     elseif opt:find('[fd]') then | ||||
|       local val = tonumber(table.remove(vars, 1)) | ||||
|       local sign = 0 | ||||
|  | ||||
|       if val < 0 then | ||||
|         sign = 1  | ||||
|         val = -val  | ||||
|       end | ||||
|  | ||||
|       local mantissa, exponent = math.frexp(val) | ||||
|       if val == 0 then | ||||
|         mantissa = 0 | ||||
|         exponent = 0 | ||||
|       else | ||||
|         mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == 'd') and 53 or 24) | ||||
|         exponent = exponent + ((opt == 'd') and 1022 or 126) | ||||
|       end | ||||
|  | ||||
|       local bytes = {} | ||||
|       if opt == 'd' then | ||||
|         val = mantissa | ||||
|         for i = 1, 6 do | ||||
|           table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) | ||||
|           val = math.floor(val / (2 ^ 8)) | ||||
|         end | ||||
|       else | ||||
|         table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8))) | ||||
|         val = math.floor(mantissa / (2 ^ 8)) | ||||
|         table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) | ||||
|         val = math.floor(val / (2 ^ 8)) | ||||
|       end | ||||
|  | ||||
|       table.insert(bytes, string.char(math.floor(exponent * ((opt == 'd') and 16 or 128) + val) % (2 ^ 8))) | ||||
|       val = math.floor((exponent * ((opt == 'd') and 16 or 128) + val) / (2 ^ 8)) | ||||
|       table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8))) | ||||
|       val = math.floor((sign * 128 + val) / (2 ^ 8)) | ||||
|  | ||||
|       if not endianness then | ||||
|         table.insert(stream, string.reverse(table.concat(bytes))) | ||||
|       else | ||||
|         table.insert(stream, table.concat(bytes)) | ||||
|       end | ||||
|     elseif opt == 's' then | ||||
|       table.insert(stream, tostring(table.remove(vars, 1))) | ||||
|       table.insert(stream, string.char(0)) | ||||
|     elseif opt == 'c' then | ||||
|       local n = format:sub(i + 1):match('%d+') | ||||
|       local length = tonumber(n) | ||||
|  | ||||
|       if length > 0 then | ||||
|         local str = tostring(table.remove(vars, 1)) | ||||
|         if length - str:len() > 0 then | ||||
|           str = str .. string.rep(' ', length - str:len()) | ||||
|         end | ||||
|         table.insert(stream, str:sub(1, length)) | ||||
|       end | ||||
|       i = i + n:len() | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   return table.concat(stream) | ||||
| end | ||||
|  | ||||
| function Struct.unpack(format, stream) | ||||
|   local vars = {} | ||||
|   local iterator = 1 | ||||
|   local endianness = true | ||||
|  | ||||
|   for i = 1, format:len() do | ||||
|     local opt = format:sub(i, i) | ||||
|  | ||||
|     if opt == '>' then | ||||
|       endianness = false | ||||
|     elseif opt:find('[bBhHiIlL]') then | ||||
|       local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 | ||||
|       local signed = opt:lower() == opt | ||||
|  | ||||
|       local val = 0 | ||||
|       for j = 1, n do | ||||
|         local byte = string.byte(stream:sub(iterator, iterator)) | ||||
|         if endianness then | ||||
|           val = val + byte * (2 ^ ((j - 1) * 8)) | ||||
|         else | ||||
|           val = val + byte * (2 ^ ((n - j) * 8)) | ||||
|         end | ||||
|         iterator = iterator + 1 | ||||
|       end | ||||
|  | ||||
|       if signed then | ||||
|         val = val - 2 ^ (n * 8 - 1) | ||||
|       end | ||||
|  | ||||
|       table.insert(vars, val) | ||||
|     elseif opt:find('[fd]') then | ||||
|       local n = (opt == 'd') and 8 or 4 | ||||
|       local x = stream:sub(iterator, iterator + n - 1) | ||||
|       iterator = iterator + n | ||||
|  | ||||
|       if not endianness then | ||||
|         x = string.reverse(x) | ||||
|       end | ||||
|  | ||||
|       local sign = 1 | ||||
|       local mantissa = string.byte(x, (opt == 'd') and 7 or 3) % ((opt == 'd') and 16 or 128) | ||||
|       for i = n - 2, 1, -1 do | ||||
|         mantissa = mantissa * (2 ^ 8) + string.byte(x, i) | ||||
|       end | ||||
|  | ||||
|       if string.byte(x, n) > 127 then | ||||
|         sign = -1 | ||||
|       end | ||||
|  | ||||
|       local exponent = (string.byte(x, n) % 128) * ((opt == 'd') and 16 or 2) + math.floor(string.byte(x, n - 1) / ((opt == 'd') and 16 or 128)) | ||||
|       if exponent == 0 then | ||||
|         table.insert(vars, 0.0) | ||||
|       else | ||||
|         mantissa = (math.ldexp(mantissa, (opt == 'd') and -52 or -23) + 1) * sign | ||||
|         table.insert(vars, math.ldexp(mantissa, exponent - ((opt == 'd') and 1023 or 127))) | ||||
|       end | ||||
|     elseif opt == 's' then | ||||
|       local bytes = {} | ||||
|       for j = iterator, stream:len() do | ||||
|         if stream:sub(j, j) == string.char(0) then | ||||
|           break | ||||
|         end | ||||
|  | ||||
|         table.insert(bytes, stream:sub(j, j)) | ||||
|       end | ||||
|  | ||||
|       local str = table.concat(bytes) | ||||
|       iterator = iterator + str:len() + 1 | ||||
|       table.insert(vars, str) | ||||
|     elseif opt == 'c' then | ||||
|       local n = format:sub(i + 1):match('%d+') | ||||
|       table.insert(vars, stream:sub(iterator, iterator + tonumber(n))) | ||||
|       iterator = iterator + tonumber(n) | ||||
|       i = i + n:len() | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   return unpack(vars) | ||||
| end | ||||
							
								
								
									
										287
									
								
								SabrehavenOTClient/modules/corelib/table.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								SabrehavenOTClient/modules/corelib/table.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
| -- @docclass table | ||||
|  | ||||
| function table.dump(t, depth) | ||||
|   if not depth then depth = 0 end | ||||
|   for k,v in pairs(t) do | ||||
|     str = (' '):rep(depth * 2) .. k .. ': ' | ||||
|     if type(v) ~= "table" then | ||||
|       print(str .. tostring(v)) | ||||
|     else | ||||
|       print(str) | ||||
|       table.dump(v, depth+1) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function table.clear(t) | ||||
|   for k,v in pairs(t) do | ||||
|     t[k] = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| function table.copy(t) | ||||
|   local res = {} | ||||
|   for k,v in pairs(t) do | ||||
|     res[k] = v | ||||
|   end | ||||
|   return res | ||||
| end | ||||
|  | ||||
| function table.recursivecopy(t) | ||||
|   local res = {} | ||||
|   for k,v in pairs(t) do | ||||
|     if type(v) == "table" then | ||||
|       res[k] = table.recursivecopy(v) | ||||
|     else | ||||
|       res[k] = v | ||||
|     end | ||||
|   end | ||||
|   return res | ||||
| end | ||||
|  | ||||
| function table.selectivecopy(t, keys) | ||||
|   local res = { } | ||||
|   for i,v in ipairs(keys) do | ||||
|     res[v] = t[v] | ||||
|   end | ||||
|   return res | ||||
| end | ||||
|  | ||||
| function table.merge(t, src) | ||||
|   for k,v in pairs(src) do | ||||
|     t[k] = v | ||||
|   end | ||||
| end | ||||
|  | ||||
| function table.find(t, value, lowercase) | ||||
|   for k,v in pairs(t) do | ||||
|     if lowercase and type(value) == 'string' and type(v) == 'string' then | ||||
|       if v:lower() == value:lower() then return k end | ||||
|     end | ||||
|     if v == value then return k end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function table.findbykey(t, key, lowercase) | ||||
|   for k,v in pairs(t) do | ||||
|     if lowercase and type(key) == 'string' and type(k) == 'string' then | ||||
|       if k:lower() == key:lower() then return v end | ||||
|     end | ||||
|     if k == key then return v end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function table.contains(t, value, lowercase) | ||||
|   return table.find(t, value, lowercase) ~= nil | ||||
| end | ||||
|  | ||||
| function table.findkey(t, key) | ||||
|   if t and type(t) == 'table' then | ||||
|     for k,v in pairs(t) do | ||||
|       if k == key then return k end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function table.haskey(t, key) | ||||
|   return table.findkey(t, key) ~= nil | ||||
| end | ||||
|  | ||||
| function table.removevalue(t, value) | ||||
|   for k,v in pairs(t) do | ||||
|     if v == value then | ||||
|       table.remove(t, k) | ||||
|       return true | ||||
|     end | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function table.popvalue(value) | ||||
|   local index = nil | ||||
|   for k,v in pairs(t) do | ||||
|     if v == value or not value then | ||||
|       index = k | ||||
|     end | ||||
|   end | ||||
|   if index then | ||||
|     table.remove(t, index) | ||||
|     return true | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function table.compare(t, other) | ||||
|   if #t ~= #other then return false end | ||||
|   for k,v in pairs(t) do | ||||
|     if v ~= other[k] then return false end | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function table.empty(t) | ||||
|   if t and type(t) == 'table' then | ||||
|     return next(t) == nil | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function table.permute(t, n, count) | ||||
|   n = n or #t | ||||
|   for i=1,count or n do | ||||
|     local j = math.random(i, n) | ||||
|     t[i], t[j] = t[j], t[i] | ||||
|   end | ||||
|   return t | ||||
| end | ||||
|  | ||||
| function table.findbyfield(t, fieldname, fieldvalue) | ||||
|   for _i,subt in pairs(t) do | ||||
|     if subt[fieldname] == fieldvalue then | ||||
|       return subt | ||||
|     end | ||||
|   end | ||||
|   return nil | ||||
| end | ||||
|  | ||||
| function table.size(t) | ||||
|   local size = 0 | ||||
|   for i, n in pairs(t) do | ||||
|     size = size + 1 | ||||
|   end | ||||
|  | ||||
|   return size | ||||
| end | ||||
|  | ||||
| function table.tostring(t) | ||||
|   local maxn = #t | ||||
|   local str = "" | ||||
|   for k,v in pairs(t) do | ||||
|     v = tostring(v) | ||||
|     if k == maxn and k ~= 1 then | ||||
|       str = str .. " and " .. v | ||||
|     elseif maxn > 1 and k ~= 1 then | ||||
|       str = str .. ", " .. v | ||||
|     else | ||||
|       str = str .. " " .. v | ||||
|     end | ||||
|   end | ||||
|   return str | ||||
| end | ||||
|  | ||||
| function table.collect(t, func) | ||||
|   local res = {} | ||||
|   for k,v in pairs(t) do | ||||
|     local a,b = func(k,v) | ||||
|     if a and b then | ||||
|       res[a] = b | ||||
|     elseif a ~= nil then | ||||
|       table.insert(res,a) | ||||
|     end | ||||
|   end | ||||
|   return res | ||||
| end | ||||
|  | ||||
| function table.equals(t, comp) | ||||
|   if type(t) == "table" and type(comp) == "table" then | ||||
|     for k,v in pairs(t) do | ||||
|       if v ~= comp[k] then return false end | ||||
|     end | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function table.equal(t1,t2,ignore_mt) | ||||
|    local ty1 = type(t1) | ||||
|    local ty2 = type(t2) | ||||
|    if ty1 ~= ty2 then return false end | ||||
|    -- non-table types can be directly compared | ||||
|    if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end | ||||
|    -- as well as tables which have the metamethod __eq | ||||
|    local mt = getmetatable(t1) | ||||
|    if not ignore_mt and mt and mt.__eq then return t1 == t2 end | ||||
|    for k1,v1 in pairs(t1) do | ||||
|       local v2 = t2[k1] | ||||
|       if v2 == nil or not table.equal(v1,v2) then return false end | ||||
|    end | ||||
|    for k2,v2 in pairs(t2) do | ||||
|       local v1 = t1[k2] | ||||
|       if v1 == nil or not table.equal(v1,v2) then return false end | ||||
|    end | ||||
|    return true | ||||
| end | ||||
|  | ||||
| function table.isList(t) | ||||
|   local size = #t | ||||
|   return table.size(t) == size and size > 0 | ||||
| end | ||||
|  | ||||
| function table.isStringList(t) | ||||
|   if not table.isList(t) then return false end | ||||
|   for k,v in ipairs(t) do | ||||
|     if type(v) ~= 'string' then | ||||
|       return false | ||||
|     end | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function table.isStringPairList(t) | ||||
|   if not table.isList(t) then return false end | ||||
|   for k,v in ipairs(t) do | ||||
|     if type(v) ~= 'table' or #v ~= 2 or type(v[1]) ~= 'string' or type(v[2]) ~= 'string' then | ||||
|       return false | ||||
|     end | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function table.encodeStringPairList(t) | ||||
|   local ret = "" | ||||
|   for k,v in ipairs(t) do | ||||
|     if v[2]:find("\n") then | ||||
|       ret = ret .. v[1] .. ":[[\n" .. v[2] .. "\n]]\n" | ||||
|     else | ||||
|       ret = ret .. v[1] .. ":" .. v[2] .. "\n" | ||||
|     end | ||||
|   end | ||||
|   return ret | ||||
| end | ||||
|  | ||||
| function table.decodeStringPairList(l) | ||||
|   local ret = {} | ||||
|   local r = regexMatch(l, "(?:^|\\n)([^:^\n]{1,20}):?(.*)(?:$|\\n)") | ||||
|   local multiline = "" | ||||
|   local multilineKey = "" | ||||
|   local multilineActive = false | ||||
|   for k,v in ipairs(r) do | ||||
|     if multilineActive then | ||||
|       local endPos = v[1]:find("%]%]") | ||||
|       if endPos then | ||||
|         if endPos > 1 then | ||||
|           table.insert(ret, {multilineKey, multiline .. "\n" .. v[1]:sub(1, endPos - 1)})        | ||||
|         else | ||||
|           table.insert(ret, {multilineKey, multiline})        | ||||
|         end | ||||
|         multilineActive = false | ||||
|         multiline = "" | ||||
|         multilineKey = "" | ||||
|       else | ||||
|         if multiline:len() == 0 then | ||||
|           multiline = v[1] | ||||
|         else | ||||
|           multiline = multiline .. "\n" .. v[1]         | ||||
|         end | ||||
|       end | ||||
|     else | ||||
|       local bracketPos = v[3]:find("%[%[") | ||||
|       if bracketPos == 1 then -- multiline begin | ||||
|         multiline = v[3]:sub(bracketPos + 2) | ||||
|         multilineActive = true | ||||
|         multilineKey = v[2] | ||||
|       elseif v[2]:len() > 0 and v[3]:len() > 0 then | ||||
|         table.insert(ret, {v[2], v[3]}) | ||||
|       end | ||||
|     end     | ||||
|   end | ||||
|   return ret | ||||
							
								
								
									
										62
									
								
								SabrehavenOTClient/modules/corelib/test.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								SabrehavenOTClient/modules/corelib/test.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| Test = { | ||||
|     tests = {}, | ||||
|     activeTest = 0, | ||||
|     screenShot = 1     | ||||
| } | ||||
|  | ||||
| Test.Test = function(name, func) | ||||
|     local testId = #Test.tests + 1 | ||||
|     Test.tests[testId] = { | ||||
|         name = name, | ||||
|         actions = {}, | ||||
|         delay = 0, | ||||
|         start = 0 | ||||
|     } | ||||
|     local test = function(testFunc) | ||||
|         table.insert(Test.tests[testId].actions, {type = "test", value = testFunc}) | ||||
|     end | ||||
|     local wait = function(millis) | ||||
|         Test.tests[testId].delay = Test.tests[testId].delay + millis | ||||
|         table.insert(Test.tests[testId].actions, {type = "wait", value = Test.tests[testId].delay}) | ||||
|     end | ||||
|     local ss = function() | ||||
|         table.insert(Test.tests[testId].actions, {type = "screenshot"}) | ||||
|     end | ||||
|     local fail = function(message) | ||||
|         g_logger.fatal("Test " .. name .. " failed: " .. message) | ||||
|     end | ||||
|     func(test, wait, ss, fail) | ||||
| end | ||||
|  | ||||
| Test.run = function() | ||||
|     if Test.activeTest > #Test.tests then | ||||
|         g_logger.info("[TEST] Finished tests. Exiting...") | ||||
|         return g_app.exit() | ||||
|     end | ||||
|     local test = Test.tests[Test.activeTest] | ||||
|     if not test or #test.actions == 0 then | ||||
|         Test.activeTest = Test.activeTest + 1 | ||||
|         local nextTest = Test.tests[Test.activeTest] | ||||
|         if nextTest then | ||||
|             nextTest.start = g_clock.millis() | ||||
|             g_logger.info("[TEST] Starting test: " .. nextTest.name) | ||||
|         end | ||||
|         return scheduleEvent(Test.run, 500) | ||||
|     end | ||||
|  | ||||
|     local action = test.actions[1] | ||||
|     if action.type == "test" then | ||||
|         table.remove(test.actions, 1)         | ||||
|         action.value() | ||||
|     elseif action.type == "screenshot" then | ||||
|         table.remove(test.actions, 1)         | ||||
|         g_app.doScreenshot(Test.screenShot .. ".png") | ||||
|         Test.screenShot = Test.screenShot + 1 | ||||
|     elseif action.type == "wait" then | ||||
|         if action.value + test.start < g_clock.millis() then | ||||
|             table.remove(test.actions, 1)         | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     scheduleEvent(Test.run, 100) | ||||
| end | ||||
							
								
								
									
										67
									
								
								SabrehavenOTClient/modules/corelib/ui/effects.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								SabrehavenOTClient/modules/corelib/ui/effects.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| -- @docclass | ||||
| g_effects = {} | ||||
|  | ||||
| function g_effects.fadeIn(widget, time, elapsed) | ||||
|   if not elapsed then elapsed = 0 end | ||||
|   if not time then time = 300 end | ||||
|   widget:setOpacity(math.min(elapsed/time, 1)) | ||||
|   removeEvent(widget.fadeEvent) | ||||
|   if elapsed < time then | ||||
|     removeEvent(widget.fadeEvent) | ||||
|     widget.fadeEvent = scheduleEvent(function() | ||||
|       g_effects.fadeIn(widget, time, elapsed + 30) | ||||
|     end, 30) | ||||
|   else | ||||
|     widget.fadeEvent = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| function g_effects.fadeOut(widget, time, elapsed) | ||||
|   if not elapsed then elapsed = 0 end | ||||
|   if not time then time = 300 end | ||||
|   elapsed = math.max((1 - widget:getOpacity()) * time, elapsed) | ||||
|   removeEvent(widget.fadeEvent) | ||||
|   widget:setOpacity(math.max((time - elapsed)/time, 0)) | ||||
|   if elapsed < time then | ||||
|     widget.fadeEvent = scheduleEvent(function() | ||||
|       g_effects.fadeOut(widget, time, elapsed + 30) | ||||
|     end, 30) | ||||
|   else | ||||
|     widget.fadeEvent = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| function g_effects.cancelFade(widget) | ||||
|   removeEvent(widget.fadeEvent) | ||||
|   widget.fadeEvent = nil | ||||
| end | ||||
|  | ||||
| function g_effects.startBlink(widget, duration, interval, clickCancel) | ||||
|   duration = duration or 0 -- until stop is called | ||||
|   interval = interval or 500 | ||||
|   clickCancel = clickCancel or true | ||||
|  | ||||
|   removeEvent(widget.blinkEvent) | ||||
|   removeEvent(widget.blinkStopEvent) | ||||
|  | ||||
|   widget.blinkEvent = cycleEvent(function() | ||||
|     widget:setOn(not widget:isOn()) | ||||
|   end, interval) | ||||
|  | ||||
|   if duration > 0 then | ||||
|     widget.blinkStopEvent = scheduleEvent(function() | ||||
|       g_effects.stopBlink(widget) | ||||
|     end, duration) | ||||
|   end | ||||
|  | ||||
|   connect(widget, { onClick = g_effects.stopBlink }) | ||||
| end | ||||
|  | ||||
| function g_effects.stopBlink(widget) | ||||
|   disconnect(widget, { onClick = g_effects.stopBlink }) | ||||
|   removeEvent(widget.blinkEvent) | ||||
|   removeEvent(widget.blinkStopEvent) | ||||
|   widget.blinkEvent = nil | ||||
|   widget.blinkStopEvent = nil | ||||
|   widget:setOn(false) | ||||
| end | ||||
							
								
								
									
										124
									
								
								SabrehavenOTClient/modules/corelib/ui/tooltip.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								SabrehavenOTClient/modules/corelib/ui/tooltip.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| -- @docclass | ||||
| g_tooltip = {} | ||||
|  | ||||
| -- private variables | ||||
| local toolTipLabel | ||||
| local currentHoveredWidget | ||||
|  | ||||
| -- private functions | ||||
| local function moveToolTip(first) | ||||
|   if not first and (not toolTipLabel:isVisible() or toolTipLabel:getOpacity() < 0.1) then return end | ||||
|  | ||||
|   local pos = g_window.getMousePosition() | ||||
|   local windowSize = g_window.getSize() | ||||
|   local labelSize = toolTipLabel:getSize() | ||||
|  | ||||
|   pos.x = pos.x + 1 | ||||
|   pos.y = pos.y + 1 | ||||
|  | ||||
|   if windowSize.width - (pos.x + labelSize.width) < 10 then | ||||
|     pos.x = pos.x - labelSize.width - 3 | ||||
|   else | ||||
|     pos.x = pos.x + 10 | ||||
|   end | ||||
|  | ||||
|   if windowSize.height - (pos.y + labelSize.height) < 10 then | ||||
|     pos.y = pos.y - labelSize.height - 3 | ||||
|   else | ||||
|     pos.y = pos.y + 10 | ||||
|   end | ||||
|  | ||||
|   toolTipLabel:setPosition(pos) | ||||
| end | ||||
|  | ||||
| local function onWidgetHoverChange(widget, hovered) | ||||
|   if hovered then | ||||
|     if widget.tooltip and not g_mouse.isPressed() then | ||||
|       g_tooltip.display(widget.tooltip) | ||||
|       currentHoveredWidget = widget | ||||
|     end | ||||
|   else | ||||
|     if widget == currentHoveredWidget then | ||||
|       g_tooltip.hide() | ||||
|       currentHoveredWidget = nil | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function onWidgetStyleApply(widget, styleName, styleNode) | ||||
|   if styleNode.tooltip then | ||||
|     widget.tooltip = styleNode.tooltip | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function g_tooltip.init() | ||||
|   connect(UIWidget, {  onStyleApply = onWidgetStyleApply, | ||||
|                        onHoverChange = onWidgetHoverChange}) | ||||
|  | ||||
|   addEvent(function() | ||||
|     toolTipLabel = g_ui.createWidget('UILabel', rootWidget) | ||||
|     toolTipLabel:setId('toolTip') | ||||
|     toolTipLabel:setBackgroundColor('#111111cc') | ||||
|     toolTipLabel:setTextAlign(AlignCenter) | ||||
|     toolTipLabel:hide() | ||||
|   end) | ||||
| end | ||||
|  | ||||
| function g_tooltip.terminate() | ||||
|   disconnect(UIWidget, { onStyleApply = onWidgetStyleApply, | ||||
|                          onHoverChange = onWidgetHoverChange }) | ||||
|  | ||||
|   currentHoveredWidget = nil | ||||
|   toolTipLabel:destroy() | ||||
|   toolTipLabel = nil | ||||
|  | ||||
|   g_tooltip = nil | ||||
| end | ||||
|  | ||||
| function g_tooltip.display(text) | ||||
|   if text == nil or text:len() == 0 then return end | ||||
|   if not toolTipLabel then return end | ||||
|  | ||||
|   toolTipLabel:setText(text) | ||||
|   toolTipLabel:resizeToText() | ||||
|   toolTipLabel:resize(toolTipLabel:getWidth() + 4, toolTipLabel:getHeight() + 4) | ||||
|   toolTipLabel:show() | ||||
|   toolTipLabel:raise() | ||||
|   toolTipLabel:enable() | ||||
|   g_effects.fadeIn(toolTipLabel, 100) | ||||
|   moveToolTip(true) | ||||
|    | ||||
|   connect(rootWidget, { | ||||
|     onMouseMove = moveToolTip, | ||||
|   })   | ||||
| end | ||||
|  | ||||
| function g_tooltip.hide() | ||||
|   g_effects.fadeOut(toolTipLabel, 100) | ||||
|    | ||||
|   disconnect(rootWidget, { | ||||
|     onMouseMove = moveToolTip, | ||||
|   })   | ||||
| end | ||||
|  | ||||
|  | ||||
| -- @docclass UIWidget @{ | ||||
|  | ||||
| -- UIWidget extensions | ||||
| function UIWidget:setTooltip(text) | ||||
|   self.tooltip = text | ||||
| end | ||||
|  | ||||
| function UIWidget:removeTooltip() | ||||
|   self.tooltip = nil | ||||
| end | ||||
|  | ||||
| function UIWidget:getTooltip() | ||||
|   return self.tooltip | ||||
| end | ||||
|  | ||||
| -- @} | ||||
|  | ||||
| g_tooltip.init() | ||||
| connect(g_app, { onTerminate = g_tooltip.terminate }) | ||||
							
								
								
									
										12
									
								
								SabrehavenOTClient/modules/corelib/ui/uibutton.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								SabrehavenOTClient/modules/corelib/ui/uibutton.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| -- @docclass | ||||
| UIButton = extends(UIWidget, "UIButton") | ||||
|  | ||||
| function UIButton.create() | ||||
|   local button = UIButton.internalCreate() | ||||
|   button:setFocusable(false) | ||||
|   return button | ||||
| end | ||||
|  | ||||
| function UIButton:onMouseRelease(pos, button) | ||||
|   return self:isPressed() | ||||
| end | ||||
							
								
								
									
										13
									
								
								SabrehavenOTClient/modules/corelib/ui/uicheckbox.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								SabrehavenOTClient/modules/corelib/ui/uicheckbox.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| -- @docclass | ||||
| UICheckBox = extends(UIWidget, "UICheckBox") | ||||
|  | ||||
| function UICheckBox.create() | ||||
|   local checkbox = UICheckBox.internalCreate() | ||||
|   checkbox:setFocusable(false) | ||||
|   checkbox:setTextAlign(AlignLeft) | ||||
|   return checkbox | ||||
| end | ||||
|  | ||||
| function UICheckBox:onClick() | ||||
|   self:setChecked(not self:isChecked()) | ||||
| end | ||||
							
								
								
									
										184
									
								
								SabrehavenOTClient/modules/corelib/ui/uicombobox.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								SabrehavenOTClient/modules/corelib/ui/uicombobox.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| -- @docclass | ||||
| UIComboBox = extends(UIWidget, "UIComboBox") | ||||
|  | ||||
| function UIComboBox.create() | ||||
|   local combobox = UIComboBox.internalCreate() | ||||
|   combobox:setFocusable(false) | ||||
|   combobox.options = {} | ||||
|   combobox.currentIndex = -1 | ||||
|   combobox.mouseScroll = true | ||||
|   combobox.menuScroll = false | ||||
|   combobox.menuHeight = 100 | ||||
|   combobox.menuScrollStep = 0 | ||||
|   return combobox | ||||
| end | ||||
|  | ||||
| function UIComboBox:clearOptions() | ||||
|   self.options = {} | ||||
|   self.currentIndex = -1 | ||||
|   self:clearText() | ||||
| end | ||||
|  | ||||
| function UIComboBox:clear() | ||||
|   return self:clearOptions() | ||||
| end | ||||
|  | ||||
| function UIComboBox:getOptionsCount() | ||||
|   return #self.options | ||||
| end | ||||
|  | ||||
| function UIComboBox:isOption(text) | ||||
|   if not self.options then return false end | ||||
|   for i,v in ipairs(self.options) do | ||||
|     if v.text == text then | ||||
|       return true | ||||
|     end | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function UIComboBox:setOption(text, dontSignal) | ||||
|   self:setCurrentOption(text, dontSignal) | ||||
| end | ||||
|  | ||||
| function UIComboBox:setCurrentOption(text, dontSignal) | ||||
|   if not self.options then return end | ||||
|   for i,v in ipairs(self.options) do | ||||
|     if v.text == text and self.currentIndex ~= i then | ||||
|       self.currentIndex = i | ||||
|       self:setText(text) | ||||
|       if not dontSignal then | ||||
|         signalcall(self.onOptionChange, self, text, v.data) | ||||
|       end | ||||
|       return | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIComboBox:updateCurrentOption(newText) | ||||
|   self.options[self.currentIndex].text = newText | ||||
|   self:setText(newText) | ||||
| end | ||||
|  | ||||
| function UIComboBox:setCurrentOptionByData(data, dontSignal) | ||||
|   if not self.options then return end | ||||
|   for i,v in ipairs(self.options) do | ||||
|     if v.data == data and self.currentIndex ~= i then | ||||
|       self.currentIndex = i | ||||
|       self:setText(v.text) | ||||
|       if not dontSignal then | ||||
|         signalcall(self.onOptionChange, self, v.text, v.data) | ||||
|       end | ||||
|       return | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIComboBox:setCurrentIndex(index, dontSignal) | ||||
|   if index >= 1 and index <= #self.options then | ||||
|     local v = self.options[index] | ||||
|     self.currentIndex = index | ||||
|     self:setText(v.text) | ||||
|     if not dontSignal then | ||||
|       signalcall(self.onOptionChange, self, v.text, v.data) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIComboBox:getCurrentOption() | ||||
|   if table.haskey(self.options, self.currentIndex) then | ||||
|     return self.options[self.currentIndex] | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIComboBox:addOption(text, data) | ||||
|   table.insert(self.options, { text = text, data = data }) | ||||
|   local index = #self.options | ||||
|   if index == 1 then self:setCurrentOption(text) end | ||||
|   return index | ||||
| end | ||||
|  | ||||
| function UIComboBox:removeOption(text) | ||||
|   for i,v in ipairs(self.options) do | ||||
|     if v.text == text then | ||||
|       table.remove(self.options, i) | ||||
|       if self.currentIndex == i then | ||||
|         self:setCurrentIndex(1) | ||||
|       elseif self.currentIndex > i then | ||||
|         self.currentIndex = self.currentIndex - 1 | ||||
|       end | ||||
|       return | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIComboBox:onMousePress(mousePos, mouseButton) | ||||
|   local menu | ||||
|   if self.menuScroll then | ||||
|     menu = g_ui.createWidget(self:getStyleName() .. 'PopupScrollMenu') | ||||
|     menu:setHeight(self.menuHeight) | ||||
|     if self.menuScrollStep > 0 then | ||||
|       menu:setScrollbarStep(self.menuScrollStep) | ||||
|     end | ||||
|   else | ||||
|     menu = g_ui.createWidget(self:getStyleName() .. 'PopupMenu') | ||||
|   end | ||||
|   menu:setId(self:getId() .. 'PopupMenu') | ||||
|   for i,v in ipairs(self.options) do | ||||
|     menu:addOption(v.text, function() self:setCurrentOption(v.text) end) | ||||
|   end | ||||
|   menu:setWidth(self:getWidth()) | ||||
|   menu:display({ x = self:getX(), y = self:getY() + self:getHeight() }) | ||||
|   connect(menu, { onDestroy = function() self:setOn(false) end }) | ||||
|   self:setOn(true) | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIComboBox:onMouseWheel(mousePos, direction) | ||||
|   if not self.mouseScroll or self.disableScroll then | ||||
|     return false | ||||
|   end | ||||
|   if direction == MouseWheelUp and self.currentIndex > 1 then | ||||
|     self:setCurrentIndex(self.currentIndex - 1) | ||||
|   elseif direction == MouseWheelDown and self.currentIndex < #self.options then | ||||
|     self:setCurrentIndex(self.currentIndex + 1) | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIComboBox:onStyleApply(styleName, styleNode) | ||||
|   if styleNode.options then | ||||
|     for k,option in pairs(styleNode.options) do | ||||
|       self:addOption(option) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if styleNode.data then | ||||
|     for k,data in pairs(styleNode.data) do | ||||
|       local option = self.options[k] | ||||
|       if option then | ||||
|         option.data = data | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   for name,value in pairs(styleNode) do | ||||
|     if name == 'mouse-scroll' then | ||||
|       self.mouseScroll = value | ||||
|     elseif name == 'menu-scroll' then | ||||
|       self.menuScroll = value | ||||
|     elseif name == 'menu-height' then | ||||
|       self.menuHeight = value | ||||
|     elseif name == 'menu-scroll-step' then | ||||
|       self.menuScrollStep = value | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIComboBox:setMouseScroll(scroll) | ||||
|   self.mouseScroll = scroll | ||||
| end | ||||
|  | ||||
| function UIComboBox:canMouseScroll() | ||||
|   return self.mouseScroll | ||||
| end | ||||
							
								
								
									
										99
									
								
								SabrehavenOTClient/modules/corelib/ui/uiimageview.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								SabrehavenOTClient/modules/corelib/ui/uiimageview.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| -- @docclass | ||||
| UIImageView = extends(UIWidget, "UIImageView") | ||||
|  | ||||
| function UIImageView.create() | ||||
|   local imageView = UIImageView.internalCreate() | ||||
|   imageView.zoom = 1 | ||||
|   imageView.minZoom = math.pow(10, -2) | ||||
|   imageView.maxZoom = math.pow(10,  2) | ||||
|   imageView:setClipping(true) | ||||
|   return imageView | ||||
| end | ||||
|  | ||||
| function UIImageView:getDefaultZoom() | ||||
|   local width = self:getWidth() | ||||
|   local height = self:getHeight() | ||||
|   local textureWidth = self:getImageTextureWidth() | ||||
|   local textureHeight = self:getImageTextureHeight() | ||||
|   local zoomX = width / textureWidth | ||||
|   local zoomY = height / textureHeight | ||||
|   return math.min(zoomX, zoomY) | ||||
| end | ||||
|  | ||||
| function UIImageView:getImagePosition(x, y) | ||||
|   x = x or self:getWidth() / 2 | ||||
|   y = y or self:getHeight() / 2 | ||||
|   local offsetX = self:getImageOffsetX() | ||||
|   local offsetY = self:getImageOffsetY() | ||||
|   local posX = (x - offsetX) / self.zoom | ||||
|   local posY = (y - offsetY) / self.zoom | ||||
|   return posX, posY | ||||
| end | ||||
|  | ||||
| function UIImageView:setImage(image) | ||||
|   self:setImageSource(image) | ||||
|   local zoom = self:getDefaultZoom() | ||||
|   self:setZoom(zoom) | ||||
|   self:center() | ||||
| end | ||||
|  | ||||
| function UIImageView:setZoom(zoom, x, y) | ||||
|   zoom = math.max(math.min(zoom, self.maxZoom), self.minZoom) | ||||
|   local posX, posY = self:getImagePosition(x, y) | ||||
|   local textureWidth = self:getImageTextureWidth() | ||||
|   local textureHeight = self:getImageTextureHeight() | ||||
|   local imageWidth = textureWidth * zoom | ||||
|   local imageHeight = textureHeight * zoom | ||||
|   self:setImageWidth(imageWidth) | ||||
|   self:setImageHeight(imageHeight) | ||||
|   self.zoom = zoom | ||||
|   self:move(posX, posY, x, y) | ||||
| end | ||||
|  | ||||
| function UIImageView:zoomIn(x, y) | ||||
|   local zoom = self.zoom * 1.1 | ||||
|   self:setZoom(zoom, x, y) | ||||
| end | ||||
|  | ||||
| function UIImageView:zoomOut(x, y) | ||||
|   local zoom = self.zoom / 1.1 | ||||
|   self:setZoom(zoom, x, y) | ||||
| end | ||||
|  | ||||
| function UIImageView:center() | ||||
|   self:move(self:getImageTextureWidth() / 2, self:getImageTextureHeight() / 2) | ||||
| end | ||||
|  | ||||
| function UIImageView:move(x, y, centerX, centerY) | ||||
|   x = math.max(math.min(x, self:getImageTextureWidth()), 0) | ||||
|   y = math.max(math.min(y, self:getImageTextureHeight()), 0) | ||||
|   local centerX = centerX or self:getWidth() / 2 | ||||
|   local centerY = centerY or self:getHeight() / 2 | ||||
|   local offsetX = centerX - x * self.zoom | ||||
|   local offsetY = centerY - y * self.zoom | ||||
|   self:setImageOffset({x=offsetX, y=offsetY}) | ||||
| end | ||||
|  | ||||
| function UIImageView:onDragEnter(pos) | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIImageView:onDragMove(pos, moved) | ||||
|   local posX, posY = self:getImagePosition() | ||||
|   self:move(posX - moved.x / self.zoom, posY - moved.y / self.zoom) | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIImageView:onDragLeave(widget, pos) | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIImageView:onMouseWheel(mousePos, direction) | ||||
|   local x = mousePos.x - self:getX() | ||||
|   local y = mousePos.y - self:getY() | ||||
|   if direction == MouseWheelUp then | ||||
|     self:zoomIn(x, y) | ||||
|   elseif direction == MouseWheelDown then | ||||
|     self:zoomOut(x, y) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										114
									
								
								SabrehavenOTClient/modules/corelib/ui/uiinputbox.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								SabrehavenOTClient/modules/corelib/ui/uiinputbox.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| if not UIWindow then dofile 'uiwindow' end | ||||
|  | ||||
| -- @docclass | ||||
| UIInputBox = extends(UIWindow, "UIInputBox") | ||||
|  | ||||
| function UIInputBox.create(title, okCallback, cancelCallback) | ||||
|   local inputBox = UIInputBox.internalCreate() | ||||
|  | ||||
|   inputBox:setText(title) | ||||
|   inputBox.inputs = {} | ||||
|   inputBox.onEnter = function() | ||||
|     local results = {} | ||||
|     for _,func in pairs(inputBox.inputs) do | ||||
|       table.insert(results, func()) | ||||
|     end | ||||
|     okCallback(unpack(results)) | ||||
|     inputBox:destroy() | ||||
|   end | ||||
|   inputBox.onEscape = function() | ||||
|     if cancelCallback then | ||||
|       cancelCallback() | ||||
|     end | ||||
|     inputBox:destroy() | ||||
|   end | ||||
|  | ||||
|   return inputBox | ||||
| end | ||||
|  | ||||
| function UIInputBox:addLabel(text) | ||||
|   local label = g_ui.createWidget('InputBoxLabel', self) | ||||
|   label:setText(text) | ||||
|   return label | ||||
| end | ||||
|  | ||||
| function UIInputBox:addLineEdit(labelText, defaultText, maxLength) | ||||
|   if labelText then self:addLabel(labelText) end | ||||
|   local lineEdit = g_ui.createWidget('InputBoxLineEdit', self) | ||||
|   if defaultText then lineEdit:setText(defaultText) end | ||||
|   if maxLength then lineEdit:setMaxLength(maxLength) end | ||||
|   table.insert(self.inputs, function() return lineEdit:getText() end) | ||||
|   return lineEdit | ||||
| end | ||||
|  | ||||
| function UIInputBox:addTextEdit(labelText, defaultText, maxLength, visibleLines) | ||||
|   if labelText then self:addLabel(labelText) end | ||||
|   local textEdit = g_ui.createWidget('InputBoxTextEdit', self) | ||||
|   if defaultText then textEdit:setText(defaultText) end | ||||
|   if maxLength then textEdit:setMaxLength(maxLength) end | ||||
|   visibleLines = visibleLines or 1 | ||||
|   textEdit:setHeight(textEdit:getHeight() * visibleLines) | ||||
|   table.insert(self.inputs, function() return textEdit:getText() end) | ||||
|   return textEdit | ||||
| end | ||||
|  | ||||
| function UIInputBox:addCheckBox(text, checked) | ||||
|   local checkBox = g_ui.createWidget('InputBoxCheckBox', self) | ||||
|   checkBox:setText(text) | ||||
|   checkBox:setChecked(checked) | ||||
|   table.insert(self.inputs, function() return checkBox:isChecked() end) | ||||
|   return checkBox | ||||
| end | ||||
|  | ||||
| function UIInputBox:addComboBox(labelText, ...) | ||||
|   if labelText then self:addLabel(labelText) end | ||||
|   local comboBox = g_ui.createWidget('InputBoxComboBox', self) | ||||
|   local options = {...} | ||||
|   for i=1,#options do | ||||
|     comboBox:addOption(options[i]) | ||||
|   end | ||||
|   table.insert(self.inputs, function() return comboBox:getCurrentOption() end) | ||||
|   return comboBox | ||||
| end | ||||
|  | ||||
| function UIInputBox:addSpinBox(labelText, minimum, maximum, value, step) | ||||
|   if labelText then self:addLabel(labelText) end | ||||
|   local spinBox = g_ui.createWidget('InputBoxSpinBox', self) | ||||
|   spinBox:setMinimum(minimum) | ||||
|   spinBox:setMaximum(maximum) | ||||
|   spinBox:setValue(value) | ||||
|   spinBox:setStep(step) | ||||
|   table.insert(self.inputs, function() return spinBox:getValue() end) | ||||
|   return spinBox | ||||
| end | ||||
|  | ||||
| function UIInputBox:display(okButtonText, cancelButtonText) | ||||
|   okButtonText = okButtonText or tr('Ok') | ||||
|   cancelButtonText = cancelButtonText or tr('Cancel') | ||||
|  | ||||
|   local buttonsWidget = g_ui.createWidget('InputBoxButtonsPanel', self) | ||||
|   local okButton = g_ui.createWidget('InputBoxButton', buttonsWidget) | ||||
|   okButton:setText(okButtonText) | ||||
|   okButton.onClick = self.onEnter | ||||
|  | ||||
|   local cancelButton = g_ui.createWidget('InputBoxButton', buttonsWidget) | ||||
|   cancelButton:setText(cancelButtonText) | ||||
|   cancelButton.onClick = self.onEscape | ||||
|  | ||||
|   buttonsWidget:setHeight(okButton:getHeight()) | ||||
|  | ||||
|   rootWidget:addChild(self) | ||||
|   self:setStyle('InputBoxWindow') | ||||
| end | ||||
|  | ||||
| function displayTextInputBox(title, label, okCallback, cancelCallback) | ||||
|   local inputBox = UIInputBox.create(title, okCallback, cancelCallback) | ||||
|   inputBox:addLineEdit(label) | ||||
|   inputBox:display() | ||||
| end | ||||
|  | ||||
| function displayNumberInputBox(title, label, okCallback, cancelCallback, min, max, value, step) | ||||
|   local inputBox = UIInputBox.create(title, okCallback, cancelCallback) | ||||
|   inputBox:addSpinBox(label, min, max, value, step) | ||||
|   inputBox:display() | ||||
| end | ||||
							
								
								
									
										10
									
								
								SabrehavenOTClient/modules/corelib/ui/uilabel.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								SabrehavenOTClient/modules/corelib/ui/uilabel.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| -- @docclass | ||||
| UILabel = extends(UIWidget, "UILabel") | ||||
|  | ||||
| function UILabel.create() | ||||
|   local label = UILabel.internalCreate() | ||||
|   label:setPhantom(true) | ||||
|   label:setFocusable(false) | ||||
|   label:setTextAlign(AlignLeft) | ||||
|   return label | ||||
| end | ||||
							
								
								
									
										96
									
								
								SabrehavenOTClient/modules/corelib/ui/uimessagebox.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								SabrehavenOTClient/modules/corelib/ui/uimessagebox.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| if not UIWindow then dofile 'uiwindow' end | ||||
|  | ||||
| -- @docclass | ||||
| UIMessageBox = extends(UIWindow, "UIMessageBox") | ||||
|  | ||||
| -- messagebox cannot be created from otui files | ||||
| UIMessageBox.create = nil | ||||
|  | ||||
| function UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback) | ||||
|   local messageBox = UIMessageBox.internalCreate() | ||||
|   rootWidget:addChild(messageBox) | ||||
|  | ||||
|   messageBox:setStyle('MainWindow') | ||||
|   messageBox:setText(title) | ||||
|  | ||||
|   local messageLabel = g_ui.createWidget('MessageBoxLabel', messageBox) | ||||
|   messageLabel:setText(message) | ||||
|  | ||||
|   local buttonsWidth = 0 | ||||
|   local buttonsHeight = 0 | ||||
|  | ||||
|   local anchor = AnchorRight | ||||
|   if buttons.anchor then anchor = buttons.anchor end | ||||
|  | ||||
|   local buttonHolder = g_ui.createWidget('MessageBoxButtonHolder', messageBox) | ||||
|   buttonHolder:addAnchor(anchor, 'parent', anchor) | ||||
|  | ||||
|   for i=1,#buttons do | ||||
|     local button = messageBox:addButton(buttons[i].text, buttons[i].callback) | ||||
|     if i == 1 then | ||||
|       button:setMarginLeft(0) | ||||
|       button:addAnchor(AnchorBottom, 'parent', AnchorBottom) | ||||
|       button:addAnchor(AnchorLeft, 'parent', AnchorLeft) | ||||
|       buttonsHeight = button:getHeight() | ||||
|     else | ||||
|       button:addAnchor(AnchorBottom, 'prev', AnchorBottom) | ||||
|       button:addAnchor(AnchorLeft, 'prev', AnchorRight) | ||||
|     end | ||||
|     buttonsWidth = buttonsWidth + button:getWidth() + button:getMarginLeft() | ||||
|   end | ||||
|  | ||||
|   buttonHolder:setWidth(buttonsWidth) | ||||
|   buttonHolder:setHeight(buttonsHeight) | ||||
|  | ||||
|   if onEnterCallback then connect(messageBox, { onEnter = onEnterCallback }) end | ||||
|   if onEscapeCallback then connect(messageBox, { onEscape = onEscapeCallback }) end | ||||
|  | ||||
|   messageBox:setWidth(math.max(messageLabel:getWidth(), messageBox:getTextSize().width, buttonHolder:getWidth()) + messageBox:getPaddingLeft() + messageBox:getPaddingRight()) | ||||
|   messageBox:setHeight(messageLabel:getHeight() + messageBox:getPaddingTop() + messageBox:getPaddingBottom() + buttonHolder:getHeight() + buttonHolder:getMarginTop()) | ||||
|   return messageBox | ||||
| end | ||||
|  | ||||
| function displayInfoBox(title, message) | ||||
|   local messageBox | ||||
|   local defaultCallback = function() messageBox:ok() end | ||||
|   messageBox = UIMessageBox.display(title, message, {{text='Ok', callback=defaultCallback}}, defaultCallback, defaultCallback) | ||||
|   return messageBox | ||||
| end | ||||
|  | ||||
| function displayErrorBox(title, message) | ||||
|   local messageBox | ||||
|   local defaultCallback = function() messageBox:ok() end | ||||
|   messageBox = UIMessageBox.display(title, message, {{text='Ok', callback=defaultCallback}}, defaultCallback, defaultCallback) | ||||
|   return messageBox | ||||
| end | ||||
|  | ||||
| function displayCancelBox(title, message) | ||||
|   local messageBox | ||||
|   local defaultCallback = function() messageBox:cancel() end | ||||
|   messageBox = UIMessageBox.display(title, message, {{text='Cancel', callback=defaultCallback}}, defaultCallback, defaultCallback) | ||||
|   return messageBox | ||||
| end | ||||
|  | ||||
| function displayGeneralBox(title, message, buttons, onEnterCallback, onEscapeCallback) | ||||
|   return UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback) | ||||
| end | ||||
|  | ||||
| function UIMessageBox:addButton(text, callback) | ||||
|   local buttonHolder = self:getChildById('buttonHolder') | ||||
|   local button = g_ui.createWidget('MessageBoxButton', buttonHolder) | ||||
|   button:setText(text) | ||||
|   connect(button, { onClick = callback }) | ||||
|   return button | ||||
| end | ||||
|  | ||||
| function UIMessageBox:ok() | ||||
|   signalcall(self.onOk, self) | ||||
|   self.onOk = nil | ||||
|   self:destroy() | ||||
| end | ||||
|  | ||||
| function UIMessageBox:cancel() | ||||
|   signalcall(self.onCancel, self) | ||||
|   self.onCancel = nil | ||||
|   self:destroy() | ||||
| end | ||||
							
								
								
									
										516
									
								
								SabrehavenOTClient/modules/corelib/ui/uiminiwindow.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										516
									
								
								SabrehavenOTClient/modules/corelib/ui/uiminiwindow.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,516 @@ | ||||
| -- @docclass | ||||
| UIMiniWindow = extends(UIWindow, "UIMiniWindow") | ||||
|  | ||||
| function UIMiniWindow.create() | ||||
|   local miniwindow = UIMiniWindow.internalCreate() | ||||
|   miniwindow.UIMiniWindowContainer = true | ||||
|   return miniwindow | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:open(dontSave) | ||||
|   self:setVisible(true) | ||||
|  | ||||
|   if not dontSave then | ||||
|     self:setSettings({closed = false}) | ||||
|   end | ||||
|  | ||||
|   signalcall(self.onOpen, self) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:close(dontSave) | ||||
|   if not self:isExplicitlyVisible() then return end | ||||
|   if self.forceOpen then return end | ||||
|   self:setVisible(false) | ||||
|  | ||||
|   if not dontSave then | ||||
|     self:setSettings({closed = true}) | ||||
|   end | ||||
|  | ||||
|   signalcall(self.onClose, self) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:minimize(dontSave) | ||||
|   self:setOn(true) | ||||
|   self:getChildById('contentsPanel'):hide() | ||||
|   self:getChildById('miniwindowScrollBar'):hide() | ||||
|   self:getChildById('bottomResizeBorder'):hide() | ||||
|   if self.minimizeButton then | ||||
|     self.minimizeButton:setOn(true) | ||||
|   end | ||||
|   self.maximizedHeight = self:getHeight() | ||||
|   self:setHeight(self.minimizedHeight) | ||||
|  | ||||
|   if not dontSave then | ||||
|     self:setSettings({minimized = true}) | ||||
|   end | ||||
|  | ||||
|   signalcall(self.onMinimize, self) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:maximize(dontSave) | ||||
|   self:setOn(false) | ||||
|   self:getChildById('contentsPanel'):show() | ||||
|   self:getChildById('miniwindowScrollBar'):show() | ||||
|   self:getChildById('bottomResizeBorder'):show() | ||||
|   if self.minimizeButton then | ||||
|     self.minimizeButton:setOn(false) | ||||
|   end | ||||
|   self:setHeight(self:getSettings('height') or self.maximizedHeight) | ||||
|  | ||||
|   if not dontSave then | ||||
|     self:setSettings({minimized = false}) | ||||
|   end | ||||
|  | ||||
|   local parent = self:getParent() | ||||
|   if parent and parent:getClassName() == 'UIMiniWindowContainer' then | ||||
|     parent:fitAll(self) | ||||
|   end | ||||
|  | ||||
|   signalcall(self.onMaximize, self) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:lock(dontSave) | ||||
|   local lockButton = self:getChildById('lockButton') | ||||
|   if lockButton then | ||||
|     lockButton:setOn(true) | ||||
|   end | ||||
|   self:setDraggable(false) | ||||
|   if not dontsave then | ||||
|     self:setSettings({locked = true}) | ||||
|   end | ||||
|  | ||||
|   signalcall(self.onLockChange, self) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:unlock(dontSave) | ||||
|   local lockButton = self:getChildById('lockButton') | ||||
|   if lockButton then | ||||
|     lockButton:setOn(false) | ||||
|   end | ||||
|   self:setDraggable(true) | ||||
|   if not dontsave then | ||||
|     self:setSettings({locked = false}) | ||||
|   end | ||||
|   signalcall(self.onLockChange, self) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:setup() | ||||
|   self:getChildById('closeButton').onClick = | ||||
|     function() | ||||
|       self:close() | ||||
|     end | ||||
|   if self.forceOpen then | ||||
|       if self.closeButton then | ||||
|         self.closeButton:hide() | ||||
|       end | ||||
|   end | ||||
|  | ||||
|   if(self.minimizeButton) then | ||||
|     self.minimizeButton.onClick = | ||||
|       function() | ||||
|         if self:isOn() then | ||||
|           self:maximize() | ||||
|         else | ||||
|           self:minimize() | ||||
|         end | ||||
|       end | ||||
|   end | ||||
|    | ||||
|   local lockButton = self:getChildById('lockButton') | ||||
|   if lockButton then | ||||
|     lockButton.onClick =  | ||||
|       function () | ||||
|         if self:isDraggable() then | ||||
|           self:lock() | ||||
|         else | ||||
|           self:unlock() | ||||
|         end | ||||
|       end | ||||
|   end | ||||
|  | ||||
|   self:getChildById('miniwindowTopBar').onDoubleClick = | ||||
|     function() | ||||
|       if self:isOn() then | ||||
|         self:maximize() | ||||
|       else | ||||
|         self:minimize() | ||||
|       end | ||||
|     end | ||||
|  | ||||
|   local oldParent = self:getParent() | ||||
|  | ||||
|  | ||||
|   local settings = {} | ||||
|   if g_settings.getNodeSize('MiniWindows') < 100 then | ||||
|     settings = g_settings.getNode('MiniWindows') | ||||
|   end | ||||
|  | ||||
|   if settings then | ||||
|     local selfSettings = settings[self:getId()] | ||||
|     if selfSettings then | ||||
|       if selfSettings.parentId then | ||||
|         local parent = rootWidget:recursiveGetChildById(selfSettings.parentId) | ||||
|         if parent then | ||||
|           if parent:getClassName() == 'UIMiniWindowContainer' and selfSettings.index and parent:isOn() then | ||||
|             self.miniIndex = selfSettings.index | ||||
|             parent:scheduleInsert(self, selfSettings.index) | ||||
|           elseif selfSettings.position then | ||||
|             self:setParent(parent, true) | ||||
|             self:setPosition(topoint(selfSettings.position)) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       if selfSettings.minimized then | ||||
|         self:minimize(true) | ||||
|       else | ||||
|         if selfSettings.height and self:isResizeable() then | ||||
|           self:setHeight(selfSettings.height) | ||||
|         elseif selfSettings.height and not self:isResizeable() then | ||||
|           self:eraseSettings({height = true}) | ||||
|         end | ||||
|       end | ||||
|       if selfSettings.closed and not self.forceOpen and not self.containerWindow then | ||||
|         self:close(true) | ||||
|       end | ||||
|  | ||||
|       if selfSettings.locked then | ||||
|         self:lock(true) | ||||
|       end | ||||
|     else  | ||||
|       if not self.forceOpen and self.autoOpen ~= nil and (self.autoOpen == 0 or self.autoOpen == false) and not self.containerWindow then | ||||
|         self:close(true) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   local newParent = self:getParent() | ||||
|  | ||||
|   self.miniLoaded = true | ||||
|  | ||||
|   if self.save then | ||||
|     if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' and not self.containerWindow then | ||||
|       addEvent(function() oldParent:order() end) | ||||
|     end | ||||
|     if newParent and newParent:getClassName() == 'UIMiniWindowContainer' and newParent ~= oldParent then | ||||
|       addEvent(function() newParent:order() end) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   self:fitOnParent() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:onVisibilityChange(visible) | ||||
|   self:fitOnParent() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:onDragEnter(mousePos) | ||||
|   local parent = self:getParent() | ||||
|   if not parent then return false end | ||||
|  | ||||
|   if parent:getClassName() == 'UIMiniWindowContainer' then | ||||
|     local containerParent = parent:getParent():getParent() | ||||
|     parent:removeChild(self) | ||||
|     containerParent:addChild(self) | ||||
|     parent:saveChildren() | ||||
|   end | ||||
|  | ||||
|   local oldPos = self:getPosition() | ||||
|   self.movingReference = { x = mousePos.x - oldPos.x, y = mousePos.y - oldPos.y } | ||||
|   self:setPosition(oldPos) | ||||
|   self.free = true | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:onDragLeave(droppedWidget, mousePos) | ||||
|   local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos) | ||||
|   local dropInPanel = 0 | ||||
|   for i=1,#children do | ||||
|     local child = children[i] | ||||
|     if child:getId():contains('gameLeftPanel') or child:getId():contains('gameRightPanel') then | ||||
|       dropInPanel = 1 | ||||
| 	    if self.movedWidget then | ||||
|     self.setMovedChildMargin(self.movedOldMargin or 0) | ||||
|     self.movedWidget = nil | ||||
|     self.setMovedChildMargin = nil | ||||
|     self.movedOldMargin = nil | ||||
|     self.movedIndex = nil | ||||
|   end | ||||
|  | ||||
|   UIWindow:onDragLeave(self, droppedWidget, mousePos) | ||||
|   self:saveParent(self:getParent()) | ||||
| 	  break | ||||
|     end | ||||
|   end | ||||
|   if dropInPanel == 0 then | ||||
|     tmpp = self | ||||
|     if(modules.game_interface.getLeftPanel():isVisible()) then | ||||
|      if modules.game_interface.getRootPanel():getWidth() / 2 < mousePos.x then | ||||
|        addEvent(function() tmpp:setParent(modules.game_interface.getRightPanel()) | ||||
|   if tmpp.movedWidget then | ||||
|     tmpp.setMovedChildMargin(tmpp.movedOldMargin or 0) | ||||
|     tmpp.movedWidget = nil | ||||
|     tmpp.setMovedChildMargin = nil | ||||
|     tmpp.movedOldMargin = nil | ||||
|     tmpp.movedIndex = nil | ||||
|   end | ||||
|  | ||||
|   UIWindow:onDragLeave(tmpp, droppedWidget, mousePos) | ||||
|   tmpp:saveParent(tmpp:getParent()) | ||||
| 	   end) | ||||
|      else | ||||
|        addEvent(function() tmpp:setParent(modules.game_interface.getLeftPanel())  | ||||
| 	   if tmpp.movedWidget then | ||||
|     tmpp.setMovedChildMargin(tmpp.movedOldMargin or 0) | ||||
|     tmpp.movedWidget = nil | ||||
|     tmpp.setMovedChildMargin = nil | ||||
|     tmpp.movedOldMargin = nil | ||||
|     tmpp.movedIndex = nil | ||||
|   end | ||||
|  | ||||
|   UIWindow:onDragLeave(tmpp, droppedWidget, mousePos) | ||||
|   tmpp:saveParent(tmpp:getParent()) | ||||
| 	   end) | ||||
|      end | ||||
|     else | ||||
|       addEvent(function() tmpp:setParent(modules.game_interface.getRightPanel()) | ||||
| if tmpp.movedWidget then | ||||
|     tmpp.setMovedChildMargin(tmpp.movedOldMargin or 0) | ||||
|     tmpp.movedWidget = nil | ||||
|     tmpp.setMovedChildMargin = nil | ||||
|     tmpp.movedOldMargin = nil | ||||
|     tmpp.movedIndex = nil | ||||
|   end | ||||
|  | ||||
|   UIWindow:onDragLeave(tmpp, droppedWidget, mousePos) | ||||
|   tmpp:saveParent(tmpp:getParent()) | ||||
| 	  end) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:onDragMove(mousePos, mouseMoved) | ||||
|   local oldMousePosY = mousePos.y - mouseMoved.y | ||||
|   local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos) | ||||
|   local overAnyWidget = false | ||||
|   for i=1,#children do | ||||
|     local child = children[i] | ||||
|     if child:getParent():getClassName() == 'UIMiniWindowContainer' then | ||||
|       overAnyWidget = true | ||||
|  | ||||
|       local childCenterY = child:getY() + child:getHeight() / 2 | ||||
|       if child == self.movedWidget and mousePos.y < childCenterY and oldMousePosY < childCenterY then | ||||
|         break | ||||
|       end | ||||
|  | ||||
|       if self.movedWidget then | ||||
|         self.setMovedChildMargin(self.movedOldMargin or 0) | ||||
|         self.setMovedChildMargin = nil | ||||
|       end | ||||
|  | ||||
|       if mousePos.y < childCenterY then | ||||
|         self.movedOldMargin = child:getMarginTop() | ||||
|         self.setMovedChildMargin = function(v) child:setMarginTop(v) end | ||||
|         self.movedIndex = 0 | ||||
|       else | ||||
|         self.movedOldMargin = child:getMarginBottom() | ||||
|         self.setMovedChildMargin = function(v) child:setMarginBottom(v) end | ||||
|         self.movedIndex = 1 | ||||
|       end | ||||
|  | ||||
|       self.movedWidget = child | ||||
|       self.setMovedChildMargin(self:getHeight()) | ||||
|       break | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if not overAnyWidget and self.movedWidget then | ||||
|     self.setMovedChildMargin(self.movedOldMargin or 0) | ||||
|     self.movedWidget = nil | ||||
|   end | ||||
|  | ||||
|   return UIWindow.onDragMove(self, mousePos, mouseMoved) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:onMousePress() | ||||
|   local parent = self:getParent() | ||||
|   if not parent then return false end | ||||
|   if parent:getClassName() ~= 'UIMiniWindowContainer' then | ||||
|     self:raise() | ||||
|     return true | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:onFocusChange(focused) | ||||
|   if not focused then return end | ||||
|   local parent = self:getParent() | ||||
|   if parent and parent:getClassName() ~= 'UIMiniWindowContainer' then | ||||
|     self:raise() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:onHeightChange(height) | ||||
|   if not self:isOn() then | ||||
|     self:setSettings({height = height}) | ||||
|   end | ||||
|   self:fitOnParent() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:getSettings(name) | ||||
|   if not self.save then return nil end | ||||
|   local settings = g_settings.getNode('MiniWindows') | ||||
|   if settings then | ||||
|     local selfSettings = settings[self:getId()] | ||||
|     if selfSettings then | ||||
|       return selfSettings[name] | ||||
|     end | ||||
|   end | ||||
|   return nil | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:setSettings(data) | ||||
|   if not self.save then return end | ||||
|  | ||||
|   local settings = g_settings.getNode('MiniWindows') | ||||
|   if not settings then | ||||
|     settings = {} | ||||
|   end | ||||
|  | ||||
|   local id = self:getId() | ||||
|   if not settings[id] then | ||||
|     settings[id] = {} | ||||
|   end | ||||
|  | ||||
|   for key,value in pairs(data) do | ||||
|     settings[id][key] = value | ||||
|   end | ||||
|  | ||||
|   g_settings.setNode('MiniWindows', settings) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:eraseSettings(data) | ||||
|   if not self.save then return end | ||||
|  | ||||
|   local settings = g_settings.getNode('MiniWindows') | ||||
|   if not settings then | ||||
|     settings = {} | ||||
|   end | ||||
|  | ||||
|   local id = self:getId() | ||||
|   if not settings[id] then | ||||
|     settings[id] = {} | ||||
|   end | ||||
|  | ||||
|   for key,value in pairs(data) do | ||||
|     settings[id][key] = nil | ||||
|   end | ||||
|  | ||||
|   g_settings.setNode('MiniWindows', settings) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:clearSettings() | ||||
|   if not self.save then return end | ||||
|  | ||||
|   local settings = g_settings.getNode('MiniWindows') | ||||
|   if not settings then | ||||
|     settings = {} | ||||
|   end | ||||
|  | ||||
|   local id = self:getId() | ||||
|   settings[id] = {} | ||||
|  | ||||
|   g_settings.setNode('MiniWindows', settings) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:saveParent(parent) | ||||
|   local parent = self:getParent() | ||||
|   if parent then | ||||
|     if parent:getClassName() == 'UIMiniWindowContainer' then | ||||
|       parent:saveChildren() | ||||
|     else | ||||
|       self:saveParentPosition(parent:getId(), self:getPosition()) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:saveParentPosition(parentId, position) | ||||
|   local selfSettings = {} | ||||
|   selfSettings.parentId = parentId | ||||
|   selfSettings.position = pointtostring(position) | ||||
|   self:setSettings(selfSettings) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:saveParentIndex(parentId, index) | ||||
|   local selfSettings = {} | ||||
|   selfSettings.parentId = parentId | ||||
|   selfSettings.index = index | ||||
|   self:setSettings(selfSettings) | ||||
|   self.miniIndex = index | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:disableResize() | ||||
|   self:getChildById('bottomResizeBorder'):disable() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:enableResize() | ||||
|   self:getChildById('bottomResizeBorder'):enable() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:fitOnParent() | ||||
|   local parent = self:getParent() | ||||
|   if self:isVisible() and parent and parent:getClassName() == 'UIMiniWindowContainer' then | ||||
|     parent:fitAll(self) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:setParent(parent, dontsave) | ||||
|   UIWidget.setParent(self, parent) | ||||
|   if not dontsave then | ||||
|     self:saveParent(parent) | ||||
|   end | ||||
|   self:fitOnParent() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:setHeight(height) | ||||
|   UIWidget.setHeight(self, height) | ||||
|   signalcall(self.onHeightChange, self, height) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:setContentHeight(height) | ||||
|   local contentsPanel = self:getChildById('contentsPanel') | ||||
|   local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom() | ||||
|  | ||||
|   local resizeBorder = self:getChildById('bottomResizeBorder') | ||||
|   resizeBorder:setParentSize(minHeight + height) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:setContentMinimumHeight(height) | ||||
|   local contentsPanel = self:getChildById('contentsPanel') | ||||
|   local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom() | ||||
|  | ||||
|   local resizeBorder = self:getChildById('bottomResizeBorder') | ||||
|   resizeBorder:setMinimum(minHeight + height) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:setContentMaximumHeight(height) | ||||
|   local contentsPanel = self:getChildById('contentsPanel') | ||||
|   local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom() | ||||
|  | ||||
|   local resizeBorder = self:getChildById('bottomResizeBorder') | ||||
|   resizeBorder:setMaximum(minHeight + height) | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:getMinimumHeight() | ||||
|   local resizeBorder = self:getChildById('bottomResizeBorder') | ||||
|   return resizeBorder:getMinimum() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:getMaximumHeight() | ||||
|   local resizeBorder = self:getChildById('bottomResizeBorder') | ||||
|   return resizeBorder:getMaximum() | ||||
| end | ||||
|  | ||||
| function UIMiniWindow:isResizeable() | ||||
|   local resizeBorder = self:getChildById('bottomResizeBorder') | ||||
|   return resizeBorder:isExplicitlyVisible() and resizeBorder:isEnabled() | ||||
| end | ||||
							
								
								
									
										228
									
								
								SabrehavenOTClient/modules/corelib/ui/uiminiwindowcontainer.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								SabrehavenOTClient/modules/corelib/ui/uiminiwindowcontainer.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,228 @@ | ||||
| -- @docclass | ||||
| UIMiniWindowContainer = extends(UIWidget, "UIMiniWindowContainer") | ||||
|  | ||||
| function UIMiniWindowContainer.create() | ||||
|   local container = UIMiniWindowContainer.internalCreate() | ||||
|   container.scheduledWidgets = {} | ||||
|   container:setFocusable(false) | ||||
|   container:setPhantom(true) | ||||
|   return container | ||||
| end | ||||
|  | ||||
| -- TODO: connect to window onResize event | ||||
| -- TODO: try to resize another widget? | ||||
| -- TODO: try to find another panel? | ||||
| function UIMiniWindowContainer:fitAll(noRemoveChild) | ||||
|   if not self:isVisible() then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   if not noRemoveChild then | ||||
|     local children = self:getChildren() | ||||
|     if #children > 0 then | ||||
|       noRemoveChild = children[#children] | ||||
|     else | ||||
|       return | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   local sumHeight = 0 | ||||
|   local children = self:getChildren() | ||||
|   for i=1,#children do | ||||
|     if children[i]:isVisible() then | ||||
|       sumHeight = sumHeight + children[i]:getHeight() | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   local selfHeight = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom()) | ||||
|   if sumHeight <= selfHeight then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local removeChildren = {} | ||||
|  | ||||
|   -- try to resize noRemoveChild | ||||
|   local maximumHeight = selfHeight - (sumHeight - noRemoveChild:getHeight()) | ||||
|   if noRemoveChild:isResizeable() and noRemoveChild:getMinimumHeight() <= maximumHeight then | ||||
|     sumHeight = sumHeight - noRemoveChild:getHeight() + maximumHeight | ||||
|     addEvent(function() noRemoveChild:setHeight(maximumHeight) end) | ||||
|   end | ||||
|  | ||||
|   -- try to remove no-save widget | ||||
|   for i=#children,1,-1 do | ||||
|     if sumHeight <= selfHeight then | ||||
|       break | ||||
|     end | ||||
|  | ||||
|     local child = children[i] | ||||
|     if child ~= noRemoveChild and not child.save then | ||||
|       local childHeight = child:getHeight() | ||||
|       sumHeight = sumHeight - childHeight | ||||
|       table.insert(removeChildren, child) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   -- try to remove save widget, not forceOpen | ||||
|   for i=#children,1,-1 do | ||||
|     if sumHeight <= selfHeight then | ||||
|       break | ||||
|     end | ||||
|  | ||||
|     local child = children[i] | ||||
|     if child ~= noRemoveChild and child:isVisible() and not child.forceOpen then | ||||
|       local childHeight = child:getHeight() | ||||
|       sumHeight = sumHeight - childHeight | ||||
|       table.insert(removeChildren, child) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   -- try to remove save widget | ||||
|   for i=#children,1,-1 do | ||||
|     if sumHeight <= selfHeight then | ||||
|       break | ||||
|     end | ||||
|  | ||||
|     local child = children[i] | ||||
|     if child ~= noRemoveChild and child:isVisible() then | ||||
|       local childHeight = child:getHeight() - 50 | ||||
|       sumHeight = sumHeight - childHeight | ||||
|       table.insert(removeChildren, child) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   -- close widgets | ||||
|   for i=1,#removeChildren do | ||||
|     if removeChildren[i].forceOpen then | ||||
|       removeChildren[i]:minimize(true) | ||||
|     else | ||||
|       removeChildren[i]:close() | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindowContainer:onDrop(widget, mousePos) | ||||
|   if widget.UIMiniWindowContainer then | ||||
|     local oldParent = widget:getParent() | ||||
|     if oldParent == self then | ||||
|       return true | ||||
|     end | ||||
|  | ||||
|     if oldParent then | ||||
|       oldParent:removeChild(widget) | ||||
|     end | ||||
|  | ||||
|     if widget.movedWidget then | ||||
|       local index = self:getChildIndex(widget.movedWidget) | ||||
|       self:insertChild(index + widget.movedIndex, widget) | ||||
|     else | ||||
|       self:addChild(widget) | ||||
|     end | ||||
|  | ||||
|     self:fitAll(widget) | ||||
|     return true | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindowContainer:moveTo(newPanel) | ||||
|   if not newPanel or newPanel == self then | ||||
|     return | ||||
|   end | ||||
|   local children = self:getChildByIndex(1) | ||||
|   while children do | ||||
|     newPanel:addChild(children) | ||||
|     children = self:getChildByIndex(1) | ||||
|   end | ||||
|   newPanel:fitAll() | ||||
| end | ||||
|  | ||||
| function UIMiniWindowContainer:swapInsert(widget, index) | ||||
|   local oldParent = widget:getParent() | ||||
|   local oldIndex = self:getChildIndex(widget) | ||||
|  | ||||
|   if oldParent == self and oldIndex ~= index then | ||||
|     local oldWidget = self:getChildByIndex(index) | ||||
|     if oldWidget then | ||||
|       self:removeChild(oldWidget) | ||||
|       self:insertChild(oldIndex, oldWidget) | ||||
|     end | ||||
|     self:removeChild(widget) | ||||
|     self:insertChild(index, widget) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindowContainer:scheduleInsert(widget, index) | ||||
|   if index - 1 > self:getChildCount() then | ||||
|     if self.scheduledWidgets[index] then | ||||
|       pdebug('replacing scheduled widget id ' .. widget:getId()) | ||||
|     end | ||||
|     self.scheduledWidgets[index] = widget | ||||
|   else | ||||
|     local oldParent = widget:getParent() | ||||
|     if oldParent ~= self then | ||||
|       if oldParent then | ||||
|         oldParent:removeChild(widget) | ||||
|       end | ||||
|       self:insertChild(index, widget) | ||||
|  | ||||
|       while true do | ||||
|         local placed = false | ||||
|         for nIndex,nWidget in pairs(self.scheduledWidgets) do | ||||
|           if nIndex - 1 <= self:getChildCount() then | ||||
|             local oldParent = nWidget:getParent() | ||||
|             if oldParent ~= self then | ||||
|               if oldParent then | ||||
|                 oldParent:removeChild(nWidget) | ||||
|               end | ||||
|               self:insertChild(nIndex, nWidget) | ||||
|             else | ||||
|               self:moveChildToIndex(nWidget, nIndex) | ||||
|             end | ||||
|             self.scheduledWidgets[nIndex] = nil | ||||
|             placed = true | ||||
|             break | ||||
|           end | ||||
|         end | ||||
|         if not placed then break end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindowContainer:order() | ||||
|   local children = self:getChildren() | ||||
|   for i=1,#children do | ||||
|     if not children[i].miniLoaded then return end | ||||
|   end | ||||
|  | ||||
|   table.sort(children, function(a, b) | ||||
|     local indexA = a.miniIndex or a.autoOpen or 999 | ||||
|     local indexB = b.miniIndex or b.autoOpen or 999 | ||||
|     return indexA < indexB | ||||
|   end) | ||||
|  | ||||
|   self:reorderChildren(children) | ||||
|   local ignoreIndex = 0 | ||||
|   for i=1,#children do | ||||
|     if children[i].save then | ||||
|       children[i].miniIndex = i - ignoreIndex | ||||
|     else | ||||
|       ignoreIndex = ignoreIndex + 1 | ||||
|     end       | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindowContainer:saveChildren() | ||||
|   local children = self:getChildren() | ||||
|   local ignoreIndex = 0 | ||||
|   for i=1,#children do | ||||
|     if children[i].save then | ||||
|       children[i]:saveParentIndex(self:getId(), i - ignoreIndex) | ||||
|     else | ||||
|       ignoreIndex = ignoreIndex + 1 | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMiniWindowContainer:onGeometryChange() | ||||
|   self:fitAll() | ||||
| end | ||||
							
								
								
									
										505
									
								
								SabrehavenOTClient/modules/corelib/ui/uimovabletabbar.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										505
									
								
								SabrehavenOTClient/modules/corelib/ui/uimovabletabbar.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,505 @@ | ||||
| -- @docclass | ||||
| UIMoveableTabBar = extends(UIWidget, "UIMoveableTabBar") | ||||
|  | ||||
| -- private functions | ||||
| local function onTabClick(tab) | ||||
|   tab.tabBar:selectTab(tab) | ||||
| end | ||||
|  | ||||
| local function updateMargins(tabBar) | ||||
|   if #tabBar.tabs == 0 then return end | ||||
|  | ||||
|   local currentMargin = 0 | ||||
|   for i = 1, #tabBar.tabs do | ||||
|     tabBar.tabs[i]:setMarginLeft(currentMargin) | ||||
|     currentMargin = currentMargin + tabBar.tabSpacing + tabBar.tabs[i]:getWidth() | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function updateNavigation(tabBar) | ||||
|   if tabBar.prevNavigation then | ||||
|     if #tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1 then | ||||
|       tabBar.prevNavigation:enable() | ||||
|     else | ||||
|       tabBar.prevNavigation:disable() | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if tabBar.nextNavigation then | ||||
|     if #tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs then | ||||
|       tabBar.nextNavigation:enable() | ||||
|     else | ||||
|       tabBar.nextNavigation:disable() | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function updateIndexes(tabBar, tab, xoff) | ||||
|   local tabs = tabBar.tabs | ||||
|   local currentMargin = 0 | ||||
|   local prevIndex = table.find(tabs, tab) | ||||
|   local newIndex = prevIndex | ||||
|   local xmid = xoff + tab:getWidth()/2 | ||||
|   for i = 1, #tabs do | ||||
|     local nextTab = tabs[i] | ||||
|     if xmid >= currentMargin + nextTab:getWidth()/2 then | ||||
|       newIndex = table.find(tabs, nextTab) | ||||
|     end | ||||
|     currentMargin = currentMargin + tabBar.tabSpacing * (i - 1) + tabBar.tabs[i]:getWidth() | ||||
|   end | ||||
|   if newIndex ~= prevIndex then | ||||
|     table.remove(tabs, table.find(tabs, tab)) | ||||
|     table.insert(tabs, newIndex, tab) | ||||
|   end | ||||
|   updateNavigation(tabBar) | ||||
| end | ||||
|  | ||||
| local function getMaxMargin(tabBar, tab) | ||||
|   if #tabBar.tabs == 0 then return 0 end | ||||
|  | ||||
|   local maxMargin = 0 | ||||
|   for i = 1, #tabBar.tabs do | ||||
|     if tabBar.tabs[i] ~= tab then | ||||
|       maxMargin = maxMargin + tabBar.tabs[i]:getWidth() | ||||
|     end | ||||
|   end | ||||
|   return maxMargin + tabBar.tabSpacing * (#tabBar.tabs - 1) | ||||
| end | ||||
|  | ||||
| local function updateTabs(tabBar) | ||||
|   if #tabBar.postTabs > 0 then | ||||
|     local i = 1 | ||||
|     while i <= #tabBar.postTabs do | ||||
|       local tab = tabBar.postTabs[i] | ||||
|       if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then | ||||
|         break | ||||
|       end | ||||
|  | ||||
|       table.remove(tabBar.postTabs, i) | ||||
|       table.insert(tabBar.tabs, tab) | ||||
|       tab:setVisible(true) | ||||
|     end | ||||
|   end | ||||
|   if #tabBar.preTabs > 0 then | ||||
|     for i = #tabBar.preTabs, 1, -1 do | ||||
|       local tab = tabBar.preTabs[i] | ||||
|       if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then | ||||
|         break | ||||
|       end | ||||
|  | ||||
|       table.remove(tabBar.preTabs, i) | ||||
|       table.insert(tabBar.tabs, 1, tab) | ||||
|       tab:setVisible(true) | ||||
|     end | ||||
|   end | ||||
|   updateNavigation(tabBar) | ||||
|   updateMargins(tabBar) | ||||
|   if not tabBar.currentTab and #tabBar.tabs > 0 then | ||||
|     tabBar:selectTab(tabBar.tabs[1]) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function hideTabs(tabBar, fromBack, toArray, width) | ||||
|   while #tabBar.tabs > 0 and getMaxMargin(tabBar) + width > tabBar:getWidth() do | ||||
|     local index = fromBack and #tabBar.tabs or 1 | ||||
|     local tab = tabBar.tabs[index] | ||||
|     table.remove(tabBar.tabs, index) | ||||
|     if fromBack then | ||||
|       table.insert(toArray, 1, tab) | ||||
|     else | ||||
|       table.insert(toArray, tab) | ||||
|     end | ||||
|     if tabBar.currentTab == tab then | ||||
|       if #tabBar.tabs > 0 then | ||||
|         tabBar:selectTab(tabBar.tabs[#tabBar.tabs]) | ||||
|       else | ||||
|         tabBar.currentTab:setChecked(false) | ||||
|         tabBar.currentTab = nil | ||||
|       end | ||||
|     end | ||||
|     tab:setVisible(false) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function showPreTab(tabBar) | ||||
|   if #tabBar.preTabs == 0 then | ||||
|     return nil | ||||
|   end | ||||
|  | ||||
|   local tmpTab = tabBar.preTabs[#tabBar.preTabs] | ||||
|   hideTabs(tabBar, true, tabBar.postTabs, tmpTab:getWidth()) | ||||
|  | ||||
|   table.remove(tabBar.preTabs, #tabBar.preTabs) | ||||
|   table.insert(tabBar.tabs, 1, tmpTab) | ||||
|   tmpTab:setVisible(true) | ||||
|   return tmpTab | ||||
| end | ||||
|  | ||||
| local function showPostTab(tabBar) | ||||
|   if #tabBar.postTabs == 0 then | ||||
|     return nil | ||||
|   end | ||||
|  | ||||
|   local tmpTab = tabBar.postTabs[1] | ||||
|   hideTabs(tabBar, false, tabBar.preTabs, tmpTab:getWidth()) | ||||
|  | ||||
|   table.remove(tabBar.postTabs, 1) | ||||
|   table.insert(tabBar.tabs, tmpTab) | ||||
|   tmpTab:setVisible(true) | ||||
|   return tmpTab | ||||
| end | ||||
|  | ||||
| local function onTabMousePress(tab, mousePos, mouseButton) | ||||
|   if mouseButton == MouseRightButton then | ||||
|     if tab.menuCallback then tab.menuCallback(tab, mousePos, mouseButton) end | ||||
|     return true | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function onTabDragEnter(tab, mousePos) | ||||
|   tab:raise() | ||||
|   tab.hotSpot = mousePos.x - tab:getMarginLeft() | ||||
|   tab.tabBar.selected = tab | ||||
|   return true | ||||
| end | ||||
|  | ||||
| local function onTabDragLeave(tab) | ||||
|   updateMargins(tab.tabBar) | ||||
|   tab.tabBar.selected = nil | ||||
|   return true | ||||
| end | ||||
|  | ||||
| local function onTabDragMove(tab, mousePos, mouseMoved) | ||||
|   if tab == tab.tabBar.selected then | ||||
|     local xoff = mousePos.x - tab.hotSpot | ||||
|  | ||||
|     -- update indexes | ||||
|     updateIndexes(tab.tabBar, tab, xoff) | ||||
|     updateIndexes(tab.tabBar, tab, xoff) | ||||
|  | ||||
|     -- update margins | ||||
|     updateMargins(tab.tabBar) | ||||
|     xoff = math.max(xoff, 0) | ||||
|     xoff = math.min(xoff, getMaxMargin(tab.tabBar, tab)) | ||||
|     tab:setMarginLeft(xoff) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function tabBlink(tab, step) | ||||
|   local step = step or 0 | ||||
|   tab:setOn(not tab:isOn()) | ||||
|  | ||||
|   removeEvent(tab.blinkEvent) | ||||
|   if step < 4 then | ||||
|     tab.blinkEvent = scheduleEvent(function() tabBlink(tab, step+1) end, 500) | ||||
|   else | ||||
|     tab:setOn(true) | ||||
|     tab.blinkEvent = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function UIMoveableTabBar.create() | ||||
|   local tabbar = UIMoveableTabBar.internalCreate() | ||||
|   tabbar:setFocusable(false) | ||||
|   tabbar.tabs = {} | ||||
|   tabbar.selected = nil  -- dragged tab | ||||
|   tabbar.tabSpacing = 0 | ||||
|   tabbar.tabsMoveable = false | ||||
|   tabbar.preTabs = {} | ||||
|   tabbar.postTabs = {} | ||||
|   tabbar.prevNavigation = nil | ||||
|   tabbar.nextNavigation = nil | ||||
|   tabbar.onGeometryChange = function() | ||||
|                               hideTabs(tabbar, true, tabbar.postTabs, 0) | ||||
|                               updateTabs(tabbar) | ||||
|                             end | ||||
|   return tabbar | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:onDestroy() | ||||
|   if self.prevNavigation then | ||||
|     self.prevNavigation:disable() | ||||
|   end | ||||
|  | ||||
|   if self.nextNavigation then | ||||
|     self.nextNavigation:disable() | ||||
|   end | ||||
|  | ||||
|   self.nextNavigation = nil | ||||
|   self.prevNavigation = nil | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:setContentWidget(widget) | ||||
|   self.contentWidget = widget | ||||
|   if #self.tabs > 0 then | ||||
|     self.contentWidget:addChild(self.tabs[1].tabPanel) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:setTabSpacing(tabSpacing) | ||||
|   self.tabSpacing = tabSpacing | ||||
|   updateMargins(self) | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:addTab(text, panel, menuCallback) | ||||
|   if panel == nil then | ||||
|     panel = g_ui.createWidget(self:getStyleName() .. 'Panel') | ||||
|     panel:setId('tabPanel') | ||||
|   end | ||||
|  | ||||
|   local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self) | ||||
|   panel.isTab = true | ||||
|   tab.tabPanel = panel | ||||
|   tab.tabBar = self | ||||
|   tab:setId('tab') | ||||
|   tab:setDraggable(self.tabsMoveable) | ||||
|   tab:setText(text) | ||||
|   tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight()) | ||||
|   tab.menuCallback = menuCallback or nil | ||||
|   tab.onClick = onTabClick | ||||
|   tab.onMousePress = onTabMousePress | ||||
|   tab.onDragEnter = onTabDragEnter | ||||
|   tab.onDragLeave = onTabDragLeave | ||||
|   tab.onDragMove = onTabDragMove | ||||
|   tab.onDestroy = function() tab.tabPanel:destroy() end | ||||
|  | ||||
|   if #self.tabs == 0 then | ||||
|     self:selectTab(tab) | ||||
|     tab:setMarginLeft(0) | ||||
|     table.insert(self.tabs, tab) | ||||
|   else | ||||
|     local newMargin = self.tabSpacing * #self.tabs | ||||
|     for i = 1, #self.tabs do | ||||
|       newMargin = newMargin + self.tabs[i]:getWidth() | ||||
|     end | ||||
|     tab:setMarginLeft(newMargin) | ||||
|  | ||||
|     hideTabs(self, true, self.postTabs, tab:getWidth()) | ||||
|     table.insert(self.tabs, tab) | ||||
|     if #self.tabs == 1 then | ||||
|       self:selectTab(tab) | ||||
|     end | ||||
|     updateMargins(self) | ||||
|   end | ||||
|  | ||||
|   updateNavigation(self) | ||||
|   return tab | ||||
| end | ||||
|  | ||||
| -- Additional function to move the tab by lua | ||||
| function UIMoveableTabBar:moveTab(tab, units) | ||||
|   local index = table.find(self.tabs, tab) | ||||
|   if index == nil then return end | ||||
|  | ||||
|   local focus = false | ||||
|   if self.currentTab == tab then | ||||
|     self:selectPrevTab() | ||||
|     focus = true | ||||
|   end | ||||
|  | ||||
|   table.remove(self.tabs, index) | ||||
|  | ||||
|   local newIndex = math.min(#self.tabs+1, math.max(index + units, 1)) | ||||
|   table.insert(self.tabs, newIndex, tab) | ||||
|   if focus then self:selectTab(tab) end | ||||
|   updateMargins(self) | ||||
|   return newIndex | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:onStyleApply(styleName, styleNode) | ||||
|   if styleNode['movable'] then | ||||
|     self.tabsMoveable = styleNode['movable'] | ||||
|   end | ||||
|   if styleNode['tab-spacing'] then | ||||
|     self:setTabSpacing(styleNode['tab-spacing']) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:clearTabs() | ||||
|   while #self.tabs > 0 do | ||||
|     self:removeTab(self.tabs[#self.tabs]) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:removeTab(tab) | ||||
|   local tabTables = {self.tabs, self.preTabs, self.postTabs} | ||||
|   local index = nil | ||||
|   local tabTable = nil | ||||
|   for i = 1, #tabTables do | ||||
|     index = table.find(tabTables[i], tab) | ||||
|     if index ~= nil then | ||||
|       tabTable = tabTables[i] | ||||
|       break | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if tabTable == nil then | ||||
|     return | ||||
|   end | ||||
|   table.remove(tabTable, index) | ||||
|   if self.currentTab == tab then | ||||
|     self:selectPrevTab() | ||||
|     if #self.tabs == 1 then | ||||
|       self.currentTab = nil | ||||
|     end | ||||
|   end | ||||
|   if tab.blinkEvent then | ||||
|     removeEvent(tab.blinkEvent) | ||||
|   end | ||||
|   updateTabs(self) | ||||
|   tab:destroy() | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:getTab(text) | ||||
|   for k,tab in pairs(self.tabs) do | ||||
|     if tab:getText():lower() == text:lower() then | ||||
|       return tab | ||||
|     end | ||||
|   end | ||||
|   for k,tab in pairs(self.preTabs) do | ||||
|     if tab:getText():lower() == text:lower() then | ||||
|       return tab | ||||
|     end | ||||
|   end | ||||
|   for k,tab in pairs(self.postTabs) do | ||||
|     if tab:getText():lower() == text:lower() then | ||||
|       return tab | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:selectTab(tab) | ||||
|   if self.currentTab == tab then return end | ||||
|   if self.contentWidget then | ||||
|     local selectedWidget = self.contentWidget:getLastChild() | ||||
|     if selectedWidget and selectedWidget.isTab then | ||||
|       self.contentWidget:removeChild(selectedWidget) | ||||
|     end | ||||
|     self.contentWidget:addChild(tab.tabPanel) | ||||
|     tab.tabPanel:fill('parent') | ||||
|   end | ||||
|  | ||||
|   if self.currentTab then | ||||
|     self.currentTab:setChecked(false) | ||||
|   end | ||||
|   signalcall(self.onTabChange, self, tab) | ||||
|   self.currentTab = tab | ||||
|   tab:setChecked(true) | ||||
|   tab:setOn(false) | ||||
|   tab.blinking = false | ||||
|  | ||||
|   if tab.blinkEvent then | ||||
|     removeEvent(tab.blinkEvent) | ||||
|     tab.blinkEvent = nil | ||||
|   end | ||||
|  | ||||
|   local parent = tab:getParent() | ||||
|   parent:focusChild(tab, MouseFocusReason) | ||||
|   updateNavigation(self) | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:selectNextTab() | ||||
|   if self.currentTab == nil then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local index = table.find(self.tabs, self.currentTab) | ||||
|   if index == nil then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local newIndex = index + 1 | ||||
|   if newIndex > #self.tabs then | ||||
|     if #self.postTabs > 0 then | ||||
|       local widget = showPostTab(self) | ||||
|       self:selectTab(widget) | ||||
|     else | ||||
|       if #self.preTabs > 0 then | ||||
|         for i = 1, #self.preTabs do | ||||
|           showPreTab(self) | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       self:selectTab(self.tabs[1]) | ||||
|     end | ||||
|     updateTabs(self) | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local nextTab = self.tabs[newIndex] | ||||
|   if not nextTab then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   self:selectTab(nextTab) | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:selectPrevTab() | ||||
|   if self.currentTab == nil then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local index = table.find(self.tabs, self.currentTab) | ||||
|   if index == nil then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local newIndex = index - 1 | ||||
|   if newIndex <= 0 then | ||||
|     if #self.preTabs > 0 then | ||||
|       local widget = showPreTab(self) | ||||
|       self:selectTab(widget) | ||||
|     else | ||||
|       if #self.postTabs > 0 then | ||||
|         for i = 1, #self.postTabs do | ||||
|           showPostTab(self) | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       self:selectTab(self.tabs[#self.tabs]) | ||||
|     end | ||||
|     updateTabs(self) | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local prevTab = self.tabs[newIndex] | ||||
|   if not prevTab then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   self:selectTab(prevTab) | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:blinkTab(tab) | ||||
|   if tab:isChecked() then return end | ||||
|   tab.blinking = true | ||||
|   tabBlink(tab) | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:getTabPanel(tab) | ||||
|   return tab.tabPanel | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:getCurrentTabPanel() | ||||
|   if self.currentTab then | ||||
|     return self.currentTab.tabPanel | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:getCurrentTab() | ||||
|   return self.currentTab | ||||
| end | ||||
|  | ||||
| function UIMoveableTabBar:setNavigation(prevButton, nextButton) | ||||
|   self.prevNavigation = prevButton | ||||
|   self.nextNavigation = nextButton | ||||
|  | ||||
|   if self.prevNavigation then | ||||
|     self.prevNavigation.onClick = function() self:selectPrevTab() end | ||||
|   end | ||||
|   if self.nextNavigation then | ||||
|     self.nextNavigation.onClick = function() self:selectNextTab() end | ||||
|   end | ||||
|   updateNavigation(self) | ||||
| end | ||||
							
								
								
									
										122
									
								
								SabrehavenOTClient/modules/corelib/ui/uipopupmenu.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								SabrehavenOTClient/modules/corelib/ui/uipopupmenu.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| -- @docclass | ||||
| UIPopupMenu = extends(UIWidget, "UIPopupMenu") | ||||
|  | ||||
| local currentMenu | ||||
|  | ||||
| function UIPopupMenu.create() | ||||
|   local menu = UIPopupMenu.internalCreate() | ||||
|   local layout = UIVerticalLayout.create(menu) | ||||
|   layout:setFitChildren(true) | ||||
|   menu:setLayout(layout) | ||||
|   menu.isGameMenu = false | ||||
|   return menu | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:display(pos) | ||||
|   -- don't display if not options was added | ||||
|   if self:getChildCount() == 0 then | ||||
|     self:destroy() | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   if g_ui.isMouseGrabbed() then | ||||
|     self:destroy() | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   if currentMenu then | ||||
|     currentMenu:destroy() | ||||
|   end | ||||
|  | ||||
|   if pos == nil then | ||||
|     pos = g_window.getMousePosition() | ||||
|   end | ||||
|  | ||||
|   rootWidget:addChild(self) | ||||
|   self:setPosition(pos) | ||||
|   self:grabMouse() | ||||
|   self:focus() | ||||
|   --self:grabKeyboard() | ||||
|   currentMenu = self | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:onGeometryChange(oldRect, newRect) | ||||
|   local parent = self:getParent() | ||||
|   if not parent then return end | ||||
|   local ymax = parent:getY() + parent:getHeight() | ||||
|   local xmax = parent:getX() + parent:getWidth() | ||||
|   if newRect.y + newRect.height > ymax then | ||||
|     local newy = ymax - newRect.height | ||||
|     if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end | ||||
|   end | ||||
|   if newRect.x + newRect.width > xmax then | ||||
|     local newx = xmax - newRect.width | ||||
|     if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end | ||||
|   end | ||||
|   self:bindRectToParent() | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:addOption(optionName, optionCallback, shortcut) | ||||
|   local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self) | ||||
|   optionWidget.onClick = function(widget) | ||||
|     self:destroy() | ||||
|     optionCallback() | ||||
|   end | ||||
|   optionWidget:setText(optionName) | ||||
|   local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15 | ||||
|  | ||||
|   if shortcut then | ||||
|     local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget) | ||||
|     shortcutLabel:setText(shortcut) | ||||
|     width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight() | ||||
|   end | ||||
|  | ||||
|   self:setWidth(math.max(self:getWidth(), width)) | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:addSeparator() | ||||
|   g_ui.createWidget(self:getStyleName() .. 'Separator', self) | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:setGameMenu(state) | ||||
|   self.isGameMenu = state | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:onDestroy() | ||||
|   if currentMenu == self then | ||||
|     currentMenu = nil | ||||
|   end | ||||
|   self:ungrabMouse() | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:onMousePress(mousePos, mouseButton) | ||||
|   -- clicks outside menu area destroys the menu | ||||
|   if not self:containsPoint(mousePos) then | ||||
|     self:destroy() | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIPopupMenu:onKeyPress(keyCode, keyboardModifiers) | ||||
|   if keyCode == KeyEscape then | ||||
|     self:destroy() | ||||
|     return true | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| -- close all menus when the window is resized | ||||
| local function onRootGeometryUpdate() | ||||
|   if currentMenu then | ||||
|     currentMenu:destroy() | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function onGameEnd() | ||||
|   if currentMenu and currentMenu.isGameMenu then | ||||
|     currentMenu:destroy() | ||||
|   end | ||||
| end | ||||
|  | ||||
| connect(rootWidget, { onGeometryChange = onRootGeometryUpdate }) | ||||
| connect(g_game, { onGameEnd = onGameEnd } ) | ||||
							
								
								
									
										129
									
								
								SabrehavenOTClient/modules/corelib/ui/uipopupscrollmenu.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								SabrehavenOTClient/modules/corelib/ui/uipopupscrollmenu.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| -- @docclass | ||||
| UIPopupScrollMenu = extends(UIWidget, "UIPopupScrollMenu") | ||||
|  | ||||
| local currentMenu | ||||
|  | ||||
| function UIPopupScrollMenu.create() | ||||
|   local menu = UIPopupScrollMenu.internalCreate() | ||||
|  | ||||
|   local scrollArea = g_ui.createWidget('UIScrollArea', menu) | ||||
|   scrollArea:setLayout(UIVerticalLayout.create(menu)) | ||||
|   scrollArea:setId('scrollArea') | ||||
|  | ||||
|   local scrollBar = g_ui.createWidget('VerticalScrollBar', menu) | ||||
|   scrollBar:setId('scrollBar') | ||||
|   scrollBar.pixelsScroll = false | ||||
|  | ||||
|   scrollBar:addAnchor(AnchorRight, 'parent', AnchorRight) | ||||
|   scrollBar:addAnchor(AnchorTop, 'parent', AnchorTop) | ||||
|   scrollBar:addAnchor(AnchorBottom, 'parent', AnchorBottom) | ||||
|  | ||||
|   scrollArea:addAnchor(AnchorLeft, 'parent', AnchorLeft) | ||||
|   scrollArea:addAnchor(AnchorTop, 'parent', AnchorTop) | ||||
|   scrollArea:addAnchor(AnchorBottom, 'parent', AnchorBottom) | ||||
|   scrollArea:addAnchor(AnchorRight, 'next', AnchorLeft) | ||||
|   scrollArea:setVerticalScrollBar(scrollBar) | ||||
|  | ||||
|   menu.scrollArea = scrollArea | ||||
|   menu.scrollBar = scrollBar | ||||
|   return menu | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:setScrollbarStep(step) | ||||
|   self.scrollBar:setStep(step) | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:display(pos) | ||||
|   -- don't display if not options was added | ||||
|   if self.scrollArea:getChildCount() == 0 then | ||||
|     self:destroy() | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   if g_ui.isMouseGrabbed() then | ||||
|     self:destroy() | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   if currentMenu then | ||||
|     currentMenu:destroy() | ||||
|   end | ||||
|  | ||||
|   if pos == nil then | ||||
|     pos = g_window.getMousePosition() | ||||
|   end | ||||
|  | ||||
|   rootWidget:addChild(self) | ||||
|   self:setPosition(pos) | ||||
|   self:grabMouse() | ||||
|   currentMenu = self | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:onGeometryChange(oldRect, newRect) | ||||
|   local parent = self:getParent() | ||||
|   if not parent then return end | ||||
|   local ymax = parent:getY() + parent:getHeight() | ||||
|   local xmax = parent:getX() + parent:getWidth() | ||||
|   if newRect.y + newRect.height > ymax then | ||||
|     local newy = newRect.y - newRect.height | ||||
|     if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end | ||||
|   end | ||||
|   if newRect.x + newRect.width > xmax then | ||||
|     local newx = newRect.x - newRect.width | ||||
|     if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end | ||||
|   end | ||||
|   self:bindRectToParent() | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:addOption(optionName, optionCallback, shortcut) | ||||
|   local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self.scrollArea) | ||||
|   optionWidget.onClick = function(widget) | ||||
|     self:destroy() | ||||
|     optionCallback() | ||||
|   end | ||||
|   optionWidget:setText(optionName) | ||||
|   local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15 | ||||
|  | ||||
|   if shortcut then | ||||
|     local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget) | ||||
|     shortcutLabel:setText(shortcut) | ||||
|     width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight() | ||||
|   end | ||||
|  | ||||
|   self:setWidth(math.max(self:getWidth(), width)) | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:addSeparator() | ||||
|   g_ui.createWidget(self:getStyleName() .. 'Separator', self.scrollArea) | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:onDestroy() | ||||
|   if currentMenu == self then | ||||
|     currentMenu = nil | ||||
|   end | ||||
|   self:ungrabMouse() | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:onMousePress(mousePos, mouseButton) | ||||
|   -- clicks outside menu area destroys the menu | ||||
|   if not self:containsPoint(mousePos) then | ||||
|     self:destroy() | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIPopupScrollMenu:onKeyPress(keyCode, keyboardModifiers) | ||||
|   if keyCode == KeyEscape then | ||||
|     self:destroy() | ||||
|     return true | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| -- close all menus when the window is resized | ||||
| local function onRootGeometryUpdate() | ||||
|   if currentMenu then | ||||
|     currentMenu:destroy() | ||||
|   end | ||||
| end | ||||
| connect(rootWidget, { onGeometryChange = onRootGeometryUpdate} ) | ||||
							
								
								
									
										99
									
								
								SabrehavenOTClient/modules/corelib/ui/uiprogressbar.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								SabrehavenOTClient/modules/corelib/ui/uiprogressbar.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| -- @docclass | ||||
| UIProgressBar = extends(UIWidget, "UIProgressBar") | ||||
|  | ||||
| function UIProgressBar.create() | ||||
|   local progressbar = UIProgressBar.internalCreate() | ||||
|   progressbar:setFocusable(false) | ||||
|   progressbar:setOn(true) | ||||
|   progressbar.min = 0 | ||||
|   progressbar.max = 100 | ||||
|   progressbar.value = 0 | ||||
|   progressbar.bgBorderLeft = 0 | ||||
|   progressbar.bgBorderRight = 0 | ||||
|   progressbar.bgBorderTop = 0 | ||||
|   progressbar.bgBorderBottom = 0 | ||||
|   return progressbar | ||||
| end | ||||
|  | ||||
| function UIProgressBar:setMinimum(minimum) | ||||
|   self.minimum = minimum | ||||
|   if self.value < minimum then | ||||
|     self:setValue(minimum) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIProgressBar:setMaximum(maximum) | ||||
|   self.maximum = maximum | ||||
|   if self.value > maximum then | ||||
|     self:setValue(maximum) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIProgressBar:setValue(value, minimum, maximum) | ||||
|   if minimum then | ||||
|     self:setMinimum(minimum) | ||||
|   end | ||||
|  | ||||
|   if maximum then | ||||
|     self:setMaximum(maximum) | ||||
|   end | ||||
|  | ||||
|   self.value = math.max(math.min(value, self.maximum), self.minimum) | ||||
|   self:updateBackground() | ||||
| end | ||||
|  | ||||
| function UIProgressBar:setPercent(percent) | ||||
|   self:setValue(percent, 0, 100) | ||||
| end | ||||
|  | ||||
| function UIProgressBar:getPercent() | ||||
|   return self.value | ||||
| end | ||||
|  | ||||
| function UIProgressBar:getPercentPixels() | ||||
|   return (self.maximum - self.minimum) / self:getWidth() | ||||
| end | ||||
|  | ||||
| function UIProgressBar:getProgress() | ||||
|   if self.minimum == self.maximum then return 1 end | ||||
|   return (self.value - self.minimum) / (self.maximum - self.minimum) | ||||
| end | ||||
|  | ||||
| function UIProgressBar:updateBackground() | ||||
|   if self:isOn() then | ||||
|     local width = math.round(math.max((self:getProgress() * (self:getWidth() - self.bgBorderLeft - self.bgBorderRight)), 1)) | ||||
|     local height = self:getHeight() - self.bgBorderTop - self.bgBorderBottom | ||||
|     local rect = { x = self.bgBorderLeft, y = self.bgBorderTop, width = width, height = height } | ||||
|     self:setBackgroundRect(rect) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIProgressBar:onSetup() | ||||
|   self:updateBackground() | ||||
| end | ||||
|  | ||||
| function UIProgressBar:onStyleApply(name, node) | ||||
|   for name,value in pairs(node) do | ||||
|     if name == 'background-border-left' then | ||||
|       self.bgBorderLeft = tonumber(value) | ||||
|     elseif name == 'background-border-right' then | ||||
|       self.bgBorderRight = tonumber(value) | ||||
|     elseif name == 'background-border-top' then | ||||
|       self.bgBorderTop = tonumber(value) | ||||
|     elseif name == 'background-border-bottom' then | ||||
|       self.bgBorderBottom = tonumber(value) | ||||
|     elseif name == 'background-border' then | ||||
|       self.bgBorderLeft = tonumber(value) | ||||
|       self.bgBorderRight = tonumber(value) | ||||
|       self.bgBorderTop = tonumber(value) | ||||
|       self.bgBorderBottom = tonumber(value) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIProgressBar:onGeometryChange(oldRect, newRect) | ||||
|   if not self:isOn() then | ||||
|     self:setHeight(0) | ||||
|   end | ||||
|   self:updateBackground() | ||||
| end | ||||
							
								
								
									
										66
									
								
								SabrehavenOTClient/modules/corelib/ui/uiradiogroup.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								SabrehavenOTClient/modules/corelib/ui/uiradiogroup.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| -- @docclass | ||||
| UIRadioGroup = newclass("UIRadioGroup") | ||||
|  | ||||
| function UIRadioGroup.create() | ||||
|   local radiogroup = UIRadioGroup.internalCreate() | ||||
|   radiogroup.widgets = {} | ||||
|   radiogroup.selectedWidget = nil | ||||
|   return radiogroup | ||||
| end | ||||
|  | ||||
| function UIRadioGroup:destroy() | ||||
|   for k,widget in pairs(self.widgets) do | ||||
|     widget.onClick = nil | ||||
|   end | ||||
|   self.widgets = {} | ||||
| end | ||||
|  | ||||
| function UIRadioGroup:addWidget(widget) | ||||
|   table.insert(self.widgets, widget) | ||||
|   widget.onClick = function(widget) self:selectWidget(widget) end | ||||
| end | ||||
|  | ||||
| function UIRadioGroup:removeWidget(widget) | ||||
|   if self.selectedWidget == widget then | ||||
|     self:selectWidget(nil) | ||||
|   end | ||||
|   widget.onClick = nil | ||||
|   table.removevalue(self.widgets, widget) | ||||
| end | ||||
|  | ||||
| function UIRadioGroup:selectWidget(selectedWidget, dontSignal) | ||||
|   if selectedWidget == self.selectedWidget then return end | ||||
|  | ||||
|   local previousSelectedWidget = self.selectedWidget | ||||
|   self.selectedWidget = selectedWidget | ||||
|  | ||||
|   if previousSelectedWidget then | ||||
|     previousSelectedWidget:setChecked(false) | ||||
|   end | ||||
|  | ||||
|   if selectedWidget then | ||||
|     selectedWidget:setChecked(true) | ||||
|   end | ||||
|  | ||||
|   if not dontSignal then | ||||
|     signalcall(self.onSelectionChange, self, selectedWidget, previousSelectedWidget) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIRadioGroup:clearSelected() | ||||
|   if not self.selectedWidget then return end | ||||
|  | ||||
|   local previousSelectedWidget = self.selectedWidget | ||||
|   self.selectedWidget:setChecked(false) | ||||
|   self.selectedWidget = nil | ||||
|  | ||||
|   signalcall(self.onSelectionChange, self, nil, previousSelectedWidget) | ||||
| end | ||||
|  | ||||
| function UIRadioGroup:getSelectedWidget() | ||||
|   return self.selectedWidget | ||||
| end | ||||
|  | ||||
| function UIRadioGroup:getFirstWidget() | ||||
|   return self.widgets[1] | ||||
| end | ||||
							
								
								
									
										132
									
								
								SabrehavenOTClient/modules/corelib/ui/uiresizeborder.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								SabrehavenOTClient/modules/corelib/ui/uiresizeborder.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| -- @docclass | ||||
| UIResizeBorder = extends(UIWidget, "UIResizeBorder") | ||||
|  | ||||
| function UIResizeBorder.create() | ||||
|   local resizeborder = UIResizeBorder.internalCreate() | ||||
|   resizeborder:setFocusable(false) | ||||
|   resizeborder.minimum = 0 | ||||
|   resizeborder.maximum = 1000 | ||||
|   return resizeborder | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:onSetup() | ||||
|   if self:getWidth() > self:getHeight() then | ||||
|     self.vertical = true | ||||
|   else | ||||
|     self.vertical = false | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:onDestroy() | ||||
|   if self.hovering then | ||||
|     g_mouse.popCursor(self.cursortype) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:onHoverChange(hovered) | ||||
|   if hovered then | ||||
|     if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end | ||||
|     if self:getWidth() > self:getHeight() then | ||||
|       self.vertical = true | ||||
|       self.cursortype = 'vertical' | ||||
|     else | ||||
|       self.vertical = false | ||||
|       self.cursortype = 'horizontal' | ||||
|     end | ||||
|     g_mouse.pushCursor(self.cursortype) | ||||
|     self.hovering = true | ||||
|     if not self:isPressed() then | ||||
|       g_effects.fadeIn(self) | ||||
|     end | ||||
|   else | ||||
|     if not self:isPressed() and self.hovering then | ||||
|       g_mouse.popCursor(self.cursortype) | ||||
|       g_effects.fadeOut(self) | ||||
|       self.hovering = false | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:onMouseMove(mousePos, mouseMoved) | ||||
|   if self:isPressed() then | ||||
|     local parent = self:getParent() | ||||
|     local newSize = 0 | ||||
|     if self.vertical then | ||||
|       local delta = mousePos.y - self:getY() - self:getHeight()/2 | ||||
|       newSize = math.min(math.max(parent:getHeight() + delta, self.minimum), self.maximum) | ||||
|       parent:setHeight(newSize) | ||||
|     else | ||||
|       local delta = mousePos.x - self:getX() - self:getWidth()/2 | ||||
|       newSize = math.min(math.max(parent:getWidth() + delta, self.minimum), self.maximum) | ||||
|       parent:setWidth(newSize) | ||||
|     end | ||||
|  | ||||
|     self:checkBoundary(newSize) | ||||
|     return true | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:onMouseRelease(mousePos, mouseButton) | ||||
|   if not self:isHovered() then | ||||
|     g_mouse.popCursor(self.cursortype) | ||||
|     g_effects.fadeOut(self) | ||||
|     self.hovering = false | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:onStyleApply(styleName, styleNode) | ||||
|   for name,value in pairs(styleNode) do | ||||
|     if name == 'maximum' then | ||||
|       self:setMaximum(tonumber(value)) | ||||
|     elseif name == 'minimum' then | ||||
|       self:setMinimum(tonumber(value)) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:onVisibilityChange(visible) | ||||
|   if visible and self.maximum == self.minimum then | ||||
|     self:hide() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:setMaximum(maximum) | ||||
|   self.maximum = maximum | ||||
|   self:checkBoundary() | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:setMinimum(minimum) | ||||
|   self.minimum = minimum | ||||
|   self:checkBoundary() | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:getMaximum() return self.maximum end | ||||
| function UIResizeBorder:getMinimum() return self.minimum end | ||||
|  | ||||
| function UIResizeBorder:setParentSize(size) | ||||
|   local parent = self:getParent() | ||||
|   if self.vertical then | ||||
|     parent:setHeight(size) | ||||
|   else | ||||
|     parent:setWidth(size) | ||||
|   end | ||||
|   self:checkBoundary(size) | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:getParentSize() | ||||
|   local parent = self:getParent() | ||||
|   if self.vertical then | ||||
|     return parent:getHeight() | ||||
|   else | ||||
|     return parent:getWidth() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIResizeBorder:checkBoundary(size) | ||||
|   size = size or self:getParentSize() | ||||
|   if self.maximum == self.minimum and size == self.maximum then | ||||
|     self:hide() | ||||
|   else | ||||
|     self:show() | ||||
|   end | ||||
| end | ||||
							
								
								
									
										190
									
								
								SabrehavenOTClient/modules/corelib/ui/uiscrollarea.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								SabrehavenOTClient/modules/corelib/ui/uiscrollarea.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| -- @docclass | ||||
| UIScrollArea = extends(UIWidget, "UIScrollArea") | ||||
|  | ||||
| -- public functions | ||||
| function UIScrollArea.create() | ||||
|   local scrollarea = UIScrollArea.internalCreate() | ||||
|   scrollarea:setClipping(true) | ||||
|   scrollarea.inverted = false | ||||
|   scrollarea.alwaysScrollMaximum = false | ||||
|   return scrollarea | ||||
| end | ||||
|  | ||||
| function UIScrollArea:onStyleApply(styleName, styleNode) | ||||
|   for name,value in pairs(styleNode) do | ||||
|     if name == 'vertical-scrollbar' then | ||||
|       addEvent(function() | ||||
|         local parent = self:getParent() | ||||
|         if parent then | ||||
|           self:setVerticalScrollBar(parent:getChildById(value)) | ||||
|         end | ||||
|       end) | ||||
|     elseif name == 'horizontal-scrollbar' then | ||||
|       addEvent(function() | ||||
|         local parent = self:getParent() | ||||
|         if parent then | ||||
|           self:setHorizontalScrollBar(self:getParent():getChildById(value)) | ||||
|         end | ||||
|       end) | ||||
|     elseif name == 'inverted-scroll' then | ||||
|       self:setInverted(value) | ||||
|     elseif name == 'always-scroll-maximum' then | ||||
|       self:setAlwaysScrollMaximum(value) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollArea:updateScrollBars() | ||||
|   local scrollWidth = math.max(self:getChildrenRect().width - self:getPaddingRect().width, 0) | ||||
|   local scrollHeight = math.max(self:getChildrenRect().height - self:getPaddingRect().height, 0) | ||||
|  | ||||
|   local scrollbar = self.verticalScrollBar | ||||
|   if scrollbar then | ||||
|     if self.inverted then | ||||
|       scrollbar:setMinimum(-scrollHeight) | ||||
|       scrollbar:setMaximum(0) | ||||
|     else | ||||
|       scrollbar:setMinimum(0) | ||||
|       scrollbar:setMaximum(scrollHeight) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   local scrollbar = self.horizontalScrollBar | ||||
|   if scrollbar then | ||||
|     if self.inverted then | ||||
|       scrollbar:setMinimum(-scrollWidth) | ||||
|       scrollbar:setMaximum(0) | ||||
|     else | ||||
|       scrollbar:setMinimum(0) | ||||
|       scrollbar:setMaximum(scrollWidth) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if self.lastScrollWidth ~= scrollWidth then | ||||
|     self:onScrollWidthChange() | ||||
|   end | ||||
|   if self.lastScrollHeight ~= scrollHeight then | ||||
|     self:onScrollHeightChange() | ||||
|   end | ||||
|  | ||||
|   self.lastScrollWidth = scrollWidth | ||||
|   self.lastScrollHeight = scrollHeight | ||||
| end | ||||
|  | ||||
| function UIScrollArea:setVerticalScrollBar(scrollbar) | ||||
|   self.verticalScrollBar = scrollbar | ||||
|   connect(self.verticalScrollBar, 'onValueChange', function(scrollbar, value) | ||||
|     local virtualOffset = self:getVirtualOffset() | ||||
|     virtualOffset.y = value | ||||
|     self:setVirtualOffset(virtualOffset) | ||||
|     signalcall(self.onScrollChange, self, virtualOffset) | ||||
|   end) | ||||
|   self:updateScrollBars() | ||||
| end | ||||
|  | ||||
| function UIScrollArea:setHorizontalScrollBar(scrollbar) | ||||
|   self.horizontalScrollBar = scrollbar | ||||
|   connect(self.horizontalScrollBar, 'onValueChange', function(scrollbar, value) | ||||
|     local virtualOffset = self:getVirtualOffset() | ||||
|     virtualOffset.x = value | ||||
|     self:setVirtualOffset(virtualOffset) | ||||
|     signalcall(self.onScrollChange, self, virtualOffset) | ||||
|   end) | ||||
|   self:updateScrollBars() | ||||
| end | ||||
|  | ||||
| function UIScrollArea:setInverted(inverted) | ||||
|   self.inverted = inverted | ||||
| end | ||||
|  | ||||
| function UIScrollArea:setAlwaysScrollMaximum(value) | ||||
|   self.alwaysScrollMaximum = value | ||||
| end | ||||
|  | ||||
| function UIScrollArea:onLayoutUpdate() | ||||
|   self:updateScrollBars() | ||||
| end | ||||
|  | ||||
| function UIScrollArea:onMouseWheel(mousePos, mouseWheel) | ||||
|   if self.verticalScrollBar then | ||||
|     if not self.verticalScrollBar:isOn() then | ||||
|       return false | ||||
|     end | ||||
|     if mouseWheel == MouseWheelUp then | ||||
|       local minimum = self.verticalScrollBar:getMinimum() | ||||
|       if self.verticalScrollBar:getValue() <= minimum then | ||||
|         return false | ||||
|       end | ||||
|       self.verticalScrollBar:decrement() | ||||
|     else | ||||
|       local maximum = self.verticalScrollBar:getMaximum() | ||||
|       if self.verticalScrollBar:getValue() >= maximum then | ||||
|         return false | ||||
|       end | ||||
|       self.verticalScrollBar:increment() | ||||
|     end | ||||
|   elseif self.horizontalScrollBar then | ||||
|     if not self.horizontalScrollBar:isOn() then | ||||
|       return false | ||||
|     end | ||||
|     if mouseWheel == MouseWheelUp then | ||||
|       local maximum = self.horizontalScrollBar:getMaximum() | ||||
|       if self.horizontalScrollBar:getValue() >= maximum then | ||||
|         return false | ||||
|       end | ||||
|       self.horizontalScrollBar:increment() | ||||
|     else | ||||
|       local minimum = self.horizontalScrollBar:getMinimum() | ||||
|       if self.horizontalScrollBar:getValue() <= minimum then | ||||
|         return false | ||||
|       end | ||||
|       self.horizontalScrollBar:decrement() | ||||
|     end | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIScrollArea:ensureChildVisible(child) | ||||
|   if child then | ||||
|     local paddingRect = self:getPaddingRect() | ||||
|     if self.verticalScrollBar then | ||||
|       local deltaY = paddingRect.y - child:getY() | ||||
|       if deltaY > 0 then | ||||
|         self.verticalScrollBar:decrement(deltaY) | ||||
|       end | ||||
|  | ||||
|       deltaY = (child:getY() + child:getHeight()) - (paddingRect.y + paddingRect.height) | ||||
|       if deltaY > 0 then | ||||
|         self.verticalScrollBar:increment(deltaY) | ||||
|       end | ||||
|     elseif self.horizontalScrollBar then | ||||
|       local deltaX = paddingRect.x - child:getX() | ||||
|       if deltaX > 0 then | ||||
|         self.horizontalScrollBar:decrement(deltaX) | ||||
|       end | ||||
|  | ||||
|       deltaX = (child:getX() + child:getWidth()) - (paddingRect.x + paddingRect.width) | ||||
|       if deltaX > 0 then | ||||
|         self.horizontalScrollBar:increment(deltaX) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollArea:onChildFocusChange(focusedChild, oldFocused, reason) | ||||
|   if focusedChild and (reason == MouseFocusReason or reason == KeyboardFocusReason) then | ||||
|     self:ensureChildVisible(focusedChild) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollArea:onScrollWidthChange() | ||||
|   if self.alwaysScrollMaximum and self.horizontalScrollBar then | ||||
|     self.horizontalScrollBar:setValue(self.horizontalScrollBar:getMaximum()) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollArea:onScrollHeightChange() | ||||
|   if self.alwaysScrollMaximum and self.verticalScrollBar then | ||||
|     self.verticalScrollBar:setValue(self.verticalScrollBar:getMaximum()) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										290
									
								
								SabrehavenOTClient/modules/corelib/ui/uiscrollbar.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								SabrehavenOTClient/modules/corelib/ui/uiscrollbar.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,290 @@ | ||||
| -- @docclass | ||||
| UIScrollBar = extends(UIWidget, "UIScrollBar") | ||||
|  | ||||
| -- private functions | ||||
| local function calcValues(self) | ||||
|   local slider = self:getChildById('sliderButton') | ||||
|   local decrementButton = self:getChildById('decrementButton') | ||||
|   local incrementButton = self:getChildById('incrementButton') | ||||
|  | ||||
|   local pxrange, center | ||||
|   if self.orientation == 'vertical' then | ||||
|     pxrange = (self:getHeight() - decrementButton:getHeight() - decrementButton:getMarginTop() - decrementButton:getMarginBottom() | ||||
|                                 - incrementButton:getHeight() - incrementButton:getMarginTop() - incrementButton:getMarginBottom()) | ||||
|     center = self:getY() + math.floor(self:getHeight() / 2) | ||||
|   else -- horizontal | ||||
|     pxrange = (self:getWidth() - decrementButton:getWidth() - decrementButton:getMarginLeft() - decrementButton:getMarginRight() | ||||
|                                - incrementButton:getWidth() - incrementButton:getMarginLeft() - incrementButton:getMarginRight()) | ||||
|     center = self:getX() + math.floor(self:getWidth() / 2) | ||||
|   end | ||||
|  | ||||
|   local range = self.maximum - self.minimum + 1 | ||||
|  | ||||
|   local proportion | ||||
|  | ||||
|   if self.pixelsScroll then | ||||
|     proportion = pxrange/(range+pxrange) | ||||
|   else | ||||
|     proportion = math.min(math.max(self.step, 1), range)/range | ||||
|   end | ||||
|  | ||||
|   local px = math.max(proportion * pxrange, 6) | ||||
|   if g_app.isMobile() then | ||||
|     px = math.max(proportion * pxrange, 24)   | ||||
|   end | ||||
|   px = px - px % 2 + 1 | ||||
|  | ||||
|   local offset = 0 | ||||
|   if range == 0 or self.value == self.minimum then | ||||
|     if self.orientation == 'vertical' then | ||||
|       offset = -math.floor((self:getHeight() - px) / 2) + decrementButton:getMarginRect().height | ||||
|     else | ||||
|       offset = -math.floor((self:getWidth() - px) / 2) + decrementButton:getMarginRect().width | ||||
|     end | ||||
|   elseif range > 1 and self.value == self.maximum then | ||||
|     if self.orientation == 'vertical' then | ||||
|       offset = math.ceil((self:getHeight() - px) / 2) - incrementButton:getMarginRect().height | ||||
|     else | ||||
|       offset = math.ceil((self:getWidth() - px) / 2) - incrementButton:getMarginRect().width | ||||
|     end | ||||
|   elseif range > 1 then | ||||
|     offset = (((self.value - self.minimum) / (range - 1)) - 0.5) * (pxrange - px) | ||||
|   end | ||||
|  | ||||
|   return range, pxrange, px, offset, center | ||||
| end | ||||
|  | ||||
| local function updateValueDisplay(widget) | ||||
|   if widget == nil then return end | ||||
|  | ||||
|   if widget:getShowValue() then | ||||
|     widget:setText(widget:getValue() .. (widget:getSymbol() or '')) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function updateSlider(self) | ||||
|   local slider = self:getChildById('sliderButton') | ||||
|   if slider == nil then return end | ||||
|  | ||||
|   local range, pxrange, px, offset, center = calcValues(self) | ||||
|   if self.orientation == 'vertical' then | ||||
|     slider:setHeight(px) | ||||
|     slider:setMarginTop(offset) | ||||
|   else -- horizontal | ||||
|     slider:setWidth(px) | ||||
|     slider:setMarginLeft(offset) | ||||
|   end | ||||
|   updateValueDisplay(self) | ||||
|  | ||||
|   local status = (self.maximum ~= self.minimum) | ||||
|  | ||||
|   self:setOn(status) | ||||
|   for _i,child in pairs(self:getChildren()) do | ||||
|     child:setEnabled(status) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function parseSliderPos(self, slider, pos, move) | ||||
|   local delta, hotDistance | ||||
|   if self.orientation == 'vertical' then | ||||
|     delta = move.y | ||||
|     hotDistance = pos.y - slider:getY() | ||||
|   else | ||||
|     delta = move.x | ||||
|     hotDistance = pos.x - slider:getX() | ||||
|   end | ||||
|  | ||||
|   if (delta > 0 and hotDistance + delta > self.hotDistance) or | ||||
|      (delta < 0 and hotDistance + delta < self.hotDistance) then | ||||
|     local range, pxrange, px, offset, center = calcValues(self) | ||||
|     local newvalue = self.value + delta * (range / (pxrange - px)) | ||||
|     self:setValue(newvalue) | ||||
|   end | ||||
| end | ||||
|  | ||||
| local function parseSliderPress(self, slider, pos, button) | ||||
|   if self.orientation == 'vertical' then | ||||
|     self.hotDistance = pos.y - slider:getY() | ||||
|   else | ||||
|     self.hotDistance = pos.x - slider:getX() | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function UIScrollBar.create() | ||||
|   local scrollbar = UIScrollBar.internalCreate() | ||||
|   scrollbar:setFocusable(false) | ||||
|   scrollbar.value = 0 | ||||
|   scrollbar.minimum = -999999 | ||||
|   scrollbar.maximum = 999999 | ||||
|   scrollbar.step = 1 | ||||
|   scrollbar.orientation = 'vertical' | ||||
|   scrollbar.pixelsScroll = false | ||||
|   scrollbar.showValue = false | ||||
|   scrollbar.symbol = nil | ||||
|   scrollbar.mouseScroll = true | ||||
|   return scrollbar | ||||
| end | ||||
|  | ||||
| function UIScrollBar:onSetup() | ||||
|   self.setupDone = true | ||||
|   local sliderButton = self:getChildById('sliderButton') | ||||
|   g_mouse.bindAutoPress(self:getChildById('decrementButton'), function() self:onDecrement() end, 300) | ||||
|   g_mouse.bindAutoPress(self:getChildById('incrementButton'), function() self:onIncrement() end, 300) | ||||
|   g_mouse.bindPressMove(sliderButton, function(mousePos, mouseMoved) parseSliderPos(self, sliderButton, mousePos, mouseMoved) end) | ||||
|   g_mouse.bindPress(sliderButton, function(mousePos, mouseButton) parseSliderPress(self, sliderButton, mousePos, mouseButton) end) | ||||
|  | ||||
|   updateSlider(self) | ||||
| end | ||||
|  | ||||
| function UIScrollBar:onStyleApply(styleName, styleNode) | ||||
|   for name,value in pairs(styleNode) do | ||||
|     if name == 'maximum' then | ||||
|       self:setMaximum(tonumber(value)) | ||||
|     elseif name == 'minimum' then | ||||
|       self:setMinimum(tonumber(value)) | ||||
|     elseif name == 'step' then | ||||
|       self:setStep(tonumber(value)) | ||||
|     elseif name == 'orientation' then | ||||
|       self:setOrientation(value) | ||||
|     elseif name == 'value' then | ||||
|       self:setValue(value) | ||||
|     elseif name == 'pixels-scroll' then | ||||
|       self.pixelsScroll = true | ||||
|     elseif name == 'show-value' then | ||||
|       self.showValue = true | ||||
|     elseif name == 'symbol' then | ||||
|       self.symbol = value | ||||
|     elseif name == 'mouse-scroll' then | ||||
|       self.mouseScroll = value | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollBar:onDecrement() | ||||
|   if g_keyboard.isCtrlPressed() then | ||||
|     self:decrement(self.value) | ||||
|   elseif g_keyboard.isShiftPressed() then | ||||
|     self:decrement(10) | ||||
|   else | ||||
|     self:decrement() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollBar:onIncrement() | ||||
|   if g_keyboard.isCtrlPressed() then | ||||
|     self:increment(self.maximum) | ||||
|   elseif g_keyboard.isShiftPressed() then | ||||
|     self:increment(10) | ||||
|   else | ||||
|     self:increment() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollBar:decrement(count) | ||||
|   count = count or self.step | ||||
|   self:setValue(self.value - count) | ||||
| end | ||||
|  | ||||
| function UIScrollBar:increment(count) | ||||
|   count = count or self.step | ||||
|   self:setValue(self.value + count) | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setMaximum(maximum) | ||||
|   if maximum == self.maximum then return end | ||||
|   self.maximum = maximum | ||||
|   if self.minimum > maximum then | ||||
|     self:setMinimum(maximum) | ||||
|   end | ||||
|   if self.value > maximum then | ||||
|     self:setValue(maximum) | ||||
|   else | ||||
|     updateSlider(self) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setMinimum(minimum) | ||||
|   if minimum == self.minimum then return end | ||||
|   self.minimum = minimum | ||||
|   if self.maximum < minimum then | ||||
|     self:setMaximum(minimum) | ||||
|   end | ||||
|   if self.value < minimum then | ||||
|     self:setValue(minimum) | ||||
|   else | ||||
|     updateSlider(self) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setRange(minimum, maximum) | ||||
|   self:setMinimum(minimum) | ||||
|   self:setMaximum(maximum) | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setValue(value) | ||||
|   value = math.max(math.min(value, self.maximum), self.minimum) | ||||
|   if self.value == value then return end | ||||
|   local delta = value - self.value | ||||
|   self.value = value | ||||
|   updateSlider(self) | ||||
|   if self.setupDone then | ||||
|     signalcall(self.onValueChange, self, math.round(value), delta) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setMouseScroll(scroll) | ||||
|   self.mouseScroll = scroll | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setStep(step) | ||||
|   self.step = step | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setOrientation(orientation) | ||||
|   self.orientation = orientation | ||||
| end | ||||
|  | ||||
| function UIScrollBar:setText(text) | ||||
|   local valueLabel = self:getChildById('valueLabel') | ||||
|   if valueLabel then | ||||
|     valueLabel:setText(text) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIScrollBar:onGeometryChange() | ||||
|   updateSlider(self) | ||||
| end | ||||
|  | ||||
| function UIScrollBar:onMouseWheel(mousePos, mouseWheel) | ||||
|   if not self.mouseScroll or not self:isOn() or self.disableScroll then | ||||
|     return false | ||||
|   end | ||||
|   if mouseWheel == MouseWheelUp then | ||||
|     if self.orientation == 'vertical' then | ||||
|       if self.value <= self.minimum then  return false end | ||||
|       self:decrement() | ||||
|     else | ||||
|       if self.value >= self.maximum then return false end | ||||
|       self:increment() | ||||
|     end | ||||
|   else | ||||
|     if self.orientation == 'vertical' then | ||||
|       if self.value >= self.maximum then return false end | ||||
|       self:increment() | ||||
|     else | ||||
|       if self.value <= self.minimum then  return false end | ||||
|       self:decrement() | ||||
|     end | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIScrollBar:getMaximum() return self.maximum end | ||||
| function UIScrollBar:getMinimum() return self.minimum end | ||||
| function UIScrollBar:getValue() return math.round(self.value) end | ||||
| function UIScrollBar:getStep() return self.step end | ||||
| function UIScrollBar:getOrientation() return self.orientation end | ||||
| function UIScrollBar:getShowValue() return self.showValue end | ||||
| function UIScrollBar:getSymbol() return self.symbol end | ||||
| function UIScrollBar:getMouseScroll() return self.mouseScroll end | ||||
							
								
								
									
										191
									
								
								SabrehavenOTClient/modules/corelib/ui/uispinbox.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								SabrehavenOTClient/modules/corelib/ui/uispinbox.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,191 @@ | ||||
| -- @docclass | ||||
| UISpinBox = extends(UITextEdit, "UISpinBox") | ||||
|  | ||||
| function UISpinBox.create() | ||||
|   local spinbox = UISpinBox.internalCreate() | ||||
|   spinbox:setFocusable(false) | ||||
|   spinbox:setValidCharacters('0123456789') | ||||
|   spinbox.displayButtons = true | ||||
|   spinbox.minimum = 0 | ||||
|   spinbox.maximum = 1 | ||||
|   spinbox.value = 0 | ||||
|   spinbox.step = 1 | ||||
|   spinbox.firstchange = true | ||||
|   spinbox.mouseScroll = true | ||||
|   spinbox:setText("1") | ||||
|   spinbox:setValue(1) | ||||
|   return spinbox | ||||
| end | ||||
|  | ||||
| function UISpinBox:onSetup() | ||||
|   g_mouse.bindAutoPress(self:getChildById('up'), function() self:up() end, 300) | ||||
|   g_mouse.bindAutoPress(self:getChildById('down'), function() self:down() end, 300) | ||||
| end | ||||
|  | ||||
| function UISpinBox:onMouseWheel(mousePos, direction) | ||||
|   if not self.mouseScroll or self.disableScroll then | ||||
|     return false | ||||
|   end | ||||
|   if direction == MouseWheelUp then | ||||
|     self:up() | ||||
|   elseif direction == MouseWheelDown then | ||||
|     self:down() | ||||
|   end | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UISpinBox:onKeyPress() | ||||
|   if self.firstchange then | ||||
|     self.firstchange = false | ||||
|     self:setText('') | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function UISpinBox:onTextChange(text, oldText) | ||||
|   if text:len() == 0 then | ||||
|     self:setValue(self.minimum) | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local number = tonumber(text) | ||||
|   if not number then | ||||
|     self:setText(number) | ||||
|     return | ||||
|   else | ||||
|     if number < self.minimum then | ||||
|       self:setText(self.minimum) | ||||
|       return | ||||
|     elseif number > self.maximum then | ||||
|       self:setText(self.maximum) | ||||
|       return | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   self:setValue(number) | ||||
| end | ||||
|  | ||||
| function UISpinBox:onValueChange(value) | ||||
|   -- nothing to do | ||||
| end | ||||
|  | ||||
| function UISpinBox:onFocusChange(focused) | ||||
|   if not focused then | ||||
|     if self:getText():len() == 0 then | ||||
|       self:setText(self.minimum) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISpinBox:onStyleApply(styleName, styleNode) | ||||
|   for name, value in pairs(styleNode) do | ||||
|     if name == 'maximum' then | ||||
|       self.maximum = value | ||||
|       addEvent(function() self:setMaximum(value) end) | ||||
|     elseif name == 'minimum' then | ||||
|       self.minimum = value | ||||
|       addEvent(function() self:setMinimum(value) end) | ||||
|     elseif name == 'mouse-scroll' then | ||||
|       addEvent(function() self:setMouseScroll(value) end) | ||||
|     elseif name == 'buttons' then | ||||
|       addEvent(function() | ||||
|         if value then | ||||
|           self:showButtons() | ||||
|         else | ||||
|           self:hideButtons() | ||||
|         end | ||||
|       end) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISpinBox:showButtons() | ||||
|   self:getChildById('up'):show() | ||||
|   self:getChildById('down'):show() | ||||
|   self.displayButtons = true | ||||
| end | ||||
|  | ||||
| function UISpinBox:hideButtons() | ||||
|   self:getChildById('up'):hide() | ||||
|   self:getChildById('down'):hide() | ||||
|   self.displayButtons = false | ||||
| end | ||||
|  | ||||
| function UISpinBox:up() | ||||
|   self:setValue(self.value + self.step) | ||||
| end | ||||
|  | ||||
| function UISpinBox:down() | ||||
|   self:setValue(self.value - self.step) | ||||
| end | ||||
|  | ||||
| function UISpinBox:setValue(value, dontSignal) | ||||
|   if type(value) == "string" then | ||||
|     value = tonumber(value) | ||||
|   end | ||||
|   value = value or 0 | ||||
|   value = math.max(math.min(self.maximum, value), self.minimum) | ||||
|  | ||||
|   if value == self.value then return end | ||||
|  | ||||
|   self.value = value | ||||
|   if self:getText():len() > 0 then | ||||
|     self:setText(value) | ||||
|   end | ||||
|  | ||||
|   local upButton = self:getChildById('up') | ||||
|   local downButton = self:getChildById('down') | ||||
|   if upButton then | ||||
|     upButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.maximum) | ||||
|   end | ||||
|   if downButton then | ||||
|     downButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.minimum) | ||||
|   end | ||||
|  | ||||
|   if not dontSignal then | ||||
|     signalcall(self.onValueChange, self, value) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISpinBox:getValue() | ||||
|   return self.value | ||||
| end | ||||
|  | ||||
| function UISpinBox:setMinimum(minimum) | ||||
|   minimum = minimum or -9223372036854775808 | ||||
|   self.minimum = minimum | ||||
|   if self.minimum > self.maximum then | ||||
|     self.maximum = self.minimum | ||||
|   end | ||||
|   if self.value < minimum then | ||||
|     self:setValue(minimum) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISpinBox:getMinimum() | ||||
|   return self.minimum | ||||
| end | ||||
|  | ||||
| function UISpinBox:setMaximum(maximum) | ||||
|   maximum = maximum or 9223372036854775807 | ||||
|   self.maximum = maximum | ||||
|   if self.value > maximum then | ||||
|     self:setValue(maximum) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISpinBox:getMaximum() | ||||
|   return self.maximum | ||||
| end | ||||
|  | ||||
| function UISpinBox:setStep(step) | ||||
|   self.step = step or 1 | ||||
| end | ||||
|  | ||||
| function UISpinBox:setMouseScroll(mouseScroll) | ||||
|   self.mouseScroll = mouseScroll | ||||
| end | ||||
|  | ||||
| function UISpinBox:getMouseScroll() | ||||
|   return self.mouseScroll | ||||
| end | ||||
							
								
								
									
										85
									
								
								SabrehavenOTClient/modules/corelib/ui/uisplitter.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								SabrehavenOTClient/modules/corelib/ui/uisplitter.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| -- @docclass | ||||
| UISplitter = extends(UIWidget, "UISplitter") | ||||
|  | ||||
| function UISplitter.create() | ||||
|   local splitter = UISplitter.internalCreate() | ||||
|   splitter:setFocusable(false) | ||||
|   splitter.relativeMargin = 'bottom' | ||||
|   return splitter | ||||
| end | ||||
|  | ||||
| function UISplitter:onHoverChange(hovered) | ||||
|   -- Check if margin can be changed | ||||
|   local margin = (self.vertical and self:getMarginBottom() or self:getMarginRight()) | ||||
|   if hovered and (self:canUpdateMargin(margin + 1) ~= margin or self:canUpdateMargin(margin - 1) ~= margin) then | ||||
|     if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end | ||||
|     if self:getWidth() > self:getHeight() then | ||||
|       self.vertical = true | ||||
|       self.cursortype = 'vertical' | ||||
|     else | ||||
|       self.vertical = false | ||||
|       self.cursortype = 'horizontal' | ||||
|     end | ||||
|     self.hovering = true | ||||
|     g_mouse.pushCursor(self.cursortype) | ||||
|     if not self:isPressed() then | ||||
|       g_effects.fadeIn(self) | ||||
|     end | ||||
|   else | ||||
|     if not self:isPressed() and self.hovering then | ||||
|       g_mouse.popCursor(self.cursortype) | ||||
|       g_effects.fadeOut(self) | ||||
|       self.hovering = false | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISplitter:onMouseMove(mousePos, mouseMoved) | ||||
|   if self:isPressed() then | ||||
|     --local currentmargin, newmargin, delta | ||||
|     if self.vertical then | ||||
|       local delta = mousePos.y - self:getY() - self:getHeight()/2 | ||||
|       local newMargin = self:canUpdateMargin(self:getMarginBottom() - delta) | ||||
|       local currentMargin = self:getMarginBottom() | ||||
|       if newMargin ~= currentMargin then | ||||
|         self.newMargin = newMargin | ||||
|         if not self.event or self.event:isExecuted() then | ||||
|           self.event = addEvent(function() | ||||
|             self:setMarginBottom(self.newMargin) | ||||
|           end) | ||||
|         end | ||||
|       end | ||||
|     else | ||||
|       local delta = mousePos.x - self:getX() - self:getWidth()/2 | ||||
|       local newMargin = self:canUpdateMargin(self:getMarginRight() - delta) | ||||
|       local currentMargin = self:getMarginRight() | ||||
|       if newMargin ~= currentMargin then | ||||
|         self.newMargin = newMargin | ||||
|         if not self.event or self.event:isExecuted() then | ||||
|           self.event = addEvent(function() | ||||
|             self:setMarginRight(self.newMargin) | ||||
|           end) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|     return true | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISplitter:onMouseRelease(mousePos, mouseButton) | ||||
|   if not self:isHovered() then | ||||
|     g_mouse.popCursor(self.cursortype) | ||||
|     g_effects.fadeOut(self) | ||||
|     self.hovering = false | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISplitter:onStyleApply(styleName, styleNode) | ||||
|   if styleNode['relative-margin'] then | ||||
|     self.relativeMargin = styleNode['relative-margin'] | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UISplitter:canUpdateMargin(newMargin) | ||||
|   return newMargin | ||||
| end | ||||
							
								
								
									
										163
									
								
								SabrehavenOTClient/modules/corelib/ui/uitabbar.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								SabrehavenOTClient/modules/corelib/ui/uitabbar.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| -- @docclass | ||||
| UITabBar = extends(UIWidget, "UITabBar") | ||||
|  | ||||
| -- private functions | ||||
| local function onTabClick(tab) | ||||
|   tab.tabBar:selectTab(tab) | ||||
| end | ||||
|  | ||||
| local function onTabMouseRelease(tab, mousePos, mouseButton) | ||||
|   if mouseButton == MouseRightButton and tab:containsPoint(mousePos) then | ||||
|     signalcall(tab.tabBar.onTabLeftClick, tab.tabBar, tab) | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- public functions | ||||
| function UITabBar.create() | ||||
|   local tabbar = UITabBar.internalCreate() | ||||
|   tabbar:setFocusable(false) | ||||
|   tabbar.tabs = {} | ||||
|   return tabbar | ||||
| end | ||||
|  | ||||
| function UITabBar:onSetup() | ||||
|   self.buttonsPanel = self:getChildById('buttonsPanel') | ||||
| end | ||||
|  | ||||
| function UITabBar:setContentWidget(widget) | ||||
|   self.contentWidget = widget | ||||
|   if #self.tabs > 0 then | ||||
|     self.contentWidget:addChild(self.tabs[1].tabPanel) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITabBar:addTab(text, panel, icon) | ||||
|   if panel == nil then | ||||
|     panel = g_ui.createWidget(self:getStyleName() .. 'Panel') | ||||
|     panel:setId('tabPanel') | ||||
|   end | ||||
|  | ||||
|   local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel) | ||||
|  | ||||
|   panel.isTab = true | ||||
|   tab.tabPanel = panel | ||||
|   tab.tabBar = self | ||||
|   tab:setId('tab') | ||||
|   tab:setText(text) | ||||
|   tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight()) | ||||
|   tab.onClick = onTabClick | ||||
|   tab.onMouseRelease = onTabMouseRelease | ||||
|   tab.onDestroy = function() tab.tabPanel:destroy() end | ||||
|  | ||||
|   table.insert(self.tabs, tab) | ||||
|   if #self.tabs == 1 then | ||||
|     self:selectTab(tab) | ||||
|   end | ||||
|  | ||||
|   local tabStyle = {} | ||||
|   tabStyle['icon-source'] = icon | ||||
|   tab:mergeStyle(tabStyle) | ||||
|  | ||||
|   return tab | ||||
| end | ||||
|  | ||||
| function UITabBar:addButton(text, func, icon) | ||||
|   local button = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel) | ||||
|   button:setText(text) | ||||
|  | ||||
|   local style = {} | ||||
|   style['icon-source'] = icon | ||||
|   button:mergeStyle(style) | ||||
|  | ||||
|   button.onClick = func | ||||
|   return button | ||||
| end | ||||
|  | ||||
| function UITabBar:removeTab(tab) | ||||
|   local index = table.find(self.tabs, tab) | ||||
|   if index == nil then return end | ||||
|   if self.currentTab == tab then | ||||
|     self:selectPrevTab() | ||||
|   end | ||||
|   table.remove(self.tabs, index) | ||||
|   tab:destroy() | ||||
| end | ||||
|  | ||||
| function UITabBar:getTab(text) | ||||
|   for k,tab in pairs(self.tabs) do | ||||
|     if tab:getText():lower() == text:lower() then | ||||
|       return tab | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITabBar:selectTab(tab) | ||||
|   if self.currentTab == tab then return end | ||||
|   if self.contentWidget then | ||||
|     local selectedWidget = self.contentWidget:getLastChild() | ||||
|     if selectedWidget and selectedWidget.isTab then | ||||
|       self.contentWidget:removeChild(selectedWidget) | ||||
|     end | ||||
|     self.contentWidget:addChild(tab.tabPanel) | ||||
|     tab.tabPanel:fill('parent') | ||||
|   end | ||||
|  | ||||
|   if self.currentTab then | ||||
|     self.currentTab:setChecked(false) | ||||
|   end | ||||
|   signalcall(self.onTabChange, self, tab) | ||||
|   self.currentTab = tab | ||||
|   tab:setChecked(true) | ||||
|   tab:setOn(false) | ||||
|  | ||||
|   local parent = tab:getParent() | ||||
|   if parent then | ||||
|     parent:focusChild(tab, MouseFocusReason) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITabBar:selectNextTab() | ||||
|   if self.currentTab == nil then return end | ||||
|   local index = table.find(self.tabs, self.currentTab) | ||||
|   if index == nil then return end | ||||
|   local nextTab = self.tabs[index + 1] or self.tabs[1] | ||||
|   if not nextTab then return end | ||||
|   self:selectTab(nextTab) | ||||
| end | ||||
|  | ||||
| function UITabBar:selectPrevTab() | ||||
|   if self.currentTab == nil then return end | ||||
|   local index = table.find(self.tabs, self.currentTab) | ||||
|   if index == nil then return end | ||||
|   local prevTab = self.tabs[index - 1] or self.tabs[#self.tabs] | ||||
|   if not prevTab then return end | ||||
|   self:selectTab(prevTab) | ||||
| end | ||||
|  | ||||
| function UITabBar:getTabPanel(tab) | ||||
|   return tab.tabPanel | ||||
| end | ||||
|  | ||||
| function UITabBar:getCurrentTabPanel() | ||||
|   if self.currentTab then | ||||
|     return self.currentTab.tabPanel | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITabBar:getCurrentTab() | ||||
|   return self.currentTab | ||||
| end | ||||
|  | ||||
| function UITabBar:getTabs() | ||||
|   return self.tabs | ||||
| end | ||||
|  | ||||
| function UITabBar:getTabsPanel() | ||||
|   return table.collect(self.tabs, function(_,tab) return tab.tabPanel end) | ||||
| end | ||||
|  | ||||
| function UITabBar:clearTabs() | ||||
|   while #self.tabs > 0 do | ||||
|     self:removeTab(self.tabs[#self.tabs]) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										432
									
								
								SabrehavenOTClient/modules/corelib/ui/uitable.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										432
									
								
								SabrehavenOTClient/modules/corelib/ui/uitable.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,432 @@ | ||||
| -- @docclass | ||||
| --[[ | ||||
|   TODO: | ||||
|     * Make table headers more robust. | ||||
|     * Get dynamic row heights working with text wrapping. | ||||
| ]] | ||||
|  | ||||
| TABLE_SORTING_ASC = 0 | ||||
| TABLE_SORTING_DESC = 1 | ||||
|  | ||||
| UITable = extends(UIWidget, "UITable") | ||||
|  | ||||
| -- Initialize default values | ||||
| function UITable.create() | ||||
|   local table = UITable.internalCreate() | ||||
|   table.headerRow = nil | ||||
|   table.headerColumns = {} | ||||
|   table.dataSpace = nil | ||||
|   table.rows = {} | ||||
|   table.rowBaseStyle = nil | ||||
|   table.columns = {} | ||||
|   table.columnWidth = {} | ||||
|   table.columBaseStyle = nil | ||||
|   table.headerRowBaseStyle = nil | ||||
|   table.headerColumnBaseStyle = nil | ||||
|   table.selectedRow = nil | ||||
|   table.defaultColumnWidth = 80 | ||||
|   table.sortColumn = -1 | ||||
|   table.sortType = TABLE_SORTING_ASC | ||||
|   table.autoSort = false | ||||
|  | ||||
|   return table | ||||
| end | ||||
|  | ||||
| -- Clear table values | ||||
| function UITable:onDestroy() | ||||
|   for _,row in pairs(self.rows) do | ||||
|     row.onClick = nil | ||||
|   end | ||||
|   self.rows = {} | ||||
|   self.columns = {} | ||||
|   self.headerRow = nil | ||||
|   self.headerColumns = {} | ||||
|   self.columnWidth = {} | ||||
|   self.selectedRow = nil | ||||
|  | ||||
|   if self.dataSpace then | ||||
|     self.dataSpace:destroyChildren() | ||||
|     self.dataSpace = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- Detect if a header is already defined | ||||
| function UITable:onSetup() | ||||
|   local header = self:getChildById('header') | ||||
|   if header then | ||||
|     self:setHeader(header) | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- Parse table related styles | ||||
| function UITable:onStyleApply(styleName, styleNode) | ||||
|   for name, value in pairs(styleNode) do | ||||
|     if value ~= false then | ||||
|       if name == 'table-data' then | ||||
|         addEvent(function() | ||||
|           self:setTableData(self:getParent():getChildById(value)) | ||||
|         end) | ||||
|       elseif name == 'column-style' then | ||||
|         addEvent(function() | ||||
|           self:setColumnStyle(value) | ||||
|         end) | ||||
|       elseif name == 'row-style' then | ||||
|         addEvent(function() | ||||
|           self:setRowStyle(value) | ||||
|         end) | ||||
|       elseif name == 'header-column-style' then | ||||
|         addEvent(function() | ||||
|           self:setHeaderColumnStyle(value) | ||||
|         end) | ||||
|       elseif name == 'header-row-style' then | ||||
|         addEvent(function() | ||||
|           self:setHeaderRowStyle(value) | ||||
|         end) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITable:setColumnWidth(width) | ||||
|   if self:hasHeader() then return end | ||||
|   self.columnWidth = width | ||||
| end | ||||
|  | ||||
| function UITable:setDefaultColumnWidth(width) | ||||
|   self.defaultColumnWidth = width | ||||
| end | ||||
|  | ||||
| -- Check if the table has a header | ||||
| function UITable:hasHeader() | ||||
|   return self.headerRow ~= nil | ||||
| end | ||||
|  | ||||
| -- Clear all rows | ||||
| function UITable:clearData() | ||||
|   if not self.dataSpace then | ||||
|     return | ||||
|   end | ||||
|   self.dataSpace:destroyChildren() | ||||
|   self.selectedRow = nil | ||||
|   self.columns = {} | ||||
|   self.rows = {} | ||||
| end | ||||
|  | ||||
| -- Set existing child as header | ||||
| function UITable:setHeader(headerWidget) | ||||
|   self:removeHeader() | ||||
|  | ||||
|   if self.dataSpace then | ||||
|     local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop() | ||||
|     self.dataSpace:applyStyle({ height = newHeight }) | ||||
|   end | ||||
|  | ||||
|   self.headerColumns = {} | ||||
|   self.columnWidth = {} | ||||
|   for colId, column in pairs(headerWidget:getChildren()) do | ||||
|     column.colId = colId | ||||
|     column.table = self | ||||
|     table.insert(self.columnWidth, column:getWidth()) | ||||
|     table.insert(self.headerColumns, column) | ||||
|   end | ||||
|  | ||||
|   self.headerRow = headerWidget | ||||
| end | ||||
|  | ||||
| -- Create and add header from table data | ||||
| function UITable:addHeader(data) | ||||
|   if not data or type(data) ~= 'table' then | ||||
|     g_logger.error('UITable:addHeaderRow - table columns must be provided in a table') | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   self:removeHeader() | ||||
|  | ||||
|   -- build header columns | ||||
|   local columns = {} | ||||
|   for colId, column in pairs(data) do | ||||
|     local col = g_ui.createWidget(self.headerColumnBaseStyle) | ||||
|     col.colId = colId | ||||
|     col.table = self | ||||
|     for type, value in pairs(column) do | ||||
|       if type == 'width' then | ||||
|         col:setWidth(value) | ||||
|       elseif type == 'height' then | ||||
|         col:setHeight(value) | ||||
|       elseif type == 'text' then | ||||
|         col:setText(value) | ||||
|       elseif type == 'onClick' then | ||||
|         col.onClick = value | ||||
|       end | ||||
|     end | ||||
|     table.insert(columns, col) | ||||
|   end | ||||
|  | ||||
|   -- create a new header | ||||
|   local headerRow = g_ui.createWidget(self.headerRowBaseStyle, self) | ||||
|   local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop() | ||||
|   self.dataSpace:applyStyle({ height = newHeight }) | ||||
|  | ||||
|   headerRow:setId('header') | ||||
|   self.headerColumns = {} | ||||
|   self.columnWidth = {} | ||||
|   for _, column in pairs(columns) do | ||||
|     headerRow:addChild(column) | ||||
|     table.insert(self.columnWidth, column:getWidth()) | ||||
|     table.insert(self.headerColumns, column) | ||||
|   end | ||||
|  | ||||
|   self.headerRow = headerRow | ||||
|   return headerRow | ||||
| end | ||||
|  | ||||
| -- Remove header | ||||
| function UITable:removeHeader() | ||||
|   if self:hasHeader() then | ||||
|     if self.dataSpace then | ||||
|       local newHeight = self.dataSpace:getHeight()+self.headerRow:getHeight()+self.dataSpace:getMarginTop() | ||||
|       self.dataSpace:applyStyle({ height = newHeight }) | ||||
|     end | ||||
|     self.headerColumns = {} | ||||
|     self.columnWidth = {} | ||||
|     self.headerRow:destroy() | ||||
|     self.headerRow = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITable:addRow(data, height) | ||||
|   if not self.dataSpace then | ||||
|     g_logger.error('UITable:addRow - table data space has not been set, cannot add rows.') | ||||
|     return | ||||
|   end | ||||
|   if not data or type(data) ~= 'table' then | ||||
|     g_logger.error('UITable:addRow - table columns must be provided in a table.') | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local row = g_ui.createWidget(self.rowBaseStyle) | ||||
|   row.table = self | ||||
|   if height then row:setHeight(height) end | ||||
|  | ||||
|   local rowId = #self.rows + 1 | ||||
|   row.rowId = rowId | ||||
|   row:setId('row'..rowId) | ||||
|   row:updateBackgroundColor() | ||||
|  | ||||
|   self.columns[rowId] = {} | ||||
|   for colId, column in pairs(data) do | ||||
|     local col = g_ui.createWidget(self.columBaseStyle, row) | ||||
|     if column.width then | ||||
|       col:setWidth(column.width) | ||||
|     else | ||||
|       col:setWidth(self.columnWidth[colId] or self.defaultColumnWidth) | ||||
|     end | ||||
|     if column.height then | ||||
|       col:setHeight(column.height) | ||||
|     end | ||||
|     if column.text then | ||||
|       col:setText(column.text) | ||||
|     end | ||||
|     if column.sortvalue then | ||||
|       col.sortvalue = column.sortvalue | ||||
|     else | ||||
|       col.sortvalue = column.text or 0 | ||||
|     end | ||||
|     table.insert(self.columns[rowId], col) | ||||
|   end | ||||
|  | ||||
|   self.dataSpace:addChild(row) | ||||
|   table.insert(self.rows, row) | ||||
|  | ||||
|   if self.autoSort then | ||||
|     self:sort() | ||||
|   end | ||||
|  | ||||
|   return row | ||||
| end | ||||
|  | ||||
| -- Update row indices and background color | ||||
| function UITable:updateRows() | ||||
|   for rowId = 1, #self.rows do | ||||
|     local row = self.rows[rowId] | ||||
|     row.rowId = rowId | ||||
|     row:setId('row'..rowId) | ||||
|     row:updateBackgroundColor() | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- Removes the given row widget from the table | ||||
| function UITable:removeRow(row) | ||||
|   if self.selectedRow == row then | ||||
|     self:selectRow(nil) | ||||
|   end | ||||
|   row.onClick = nil | ||||
|   row.table = nil | ||||
|   table.remove(self.columns, row.rowId) | ||||
|   table.remove(self.rows, row.rowId) | ||||
|   self.dataSpace:removeChild(row) | ||||
|   self:updateRows() | ||||
| end | ||||
|  | ||||
| function UITable:toggleSorting(enabled) | ||||
|   self.autoSort = enabled | ||||
| end | ||||
|  | ||||
| function UITable:setSorting(colId, sortType) | ||||
|   self.headerColumns[colId]:focus() | ||||
|  | ||||
|   if sortType then | ||||
|     self.sortType = sortType | ||||
|   elseif self.sortColumn == colId then | ||||
|     if self.sortType == TABLE_SORTING_ASC then | ||||
|       self.sortType = TABLE_SORTING_DESC | ||||
|     else | ||||
|       self.sortType = TABLE_SORTING_ASC | ||||
|     end | ||||
|   else | ||||
|     self.sortType = TABLE_SORTING_ASC | ||||
|   end | ||||
|   self.sortColumn = colId | ||||
| end | ||||
|  | ||||
| function UITable:sort() | ||||
|   if self.sortColumn <= 0 then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   if self.sortType == TABLE_SORTING_ASC then | ||||
|     table.sort(self.rows, function(rowA, b) | ||||
|       return rowA:getChildByIndex(self.sortColumn).sortvalue < b:getChildByIndex(self.sortColumn).sortvalue | ||||
|     end) | ||||
|   else | ||||
|     table.sort(self.rows, function(rowA, b) | ||||
|       return rowA:getChildByIndex(self.sortColumn).sortvalue > b:getChildByIndex(self.sortColumn).sortvalue | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   if self.dataSpace then | ||||
|     for _, child in pairs(self.dataSpace:getChildren()) do | ||||
|       self.dataSpace:removeChild(child) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   self:updateRows() | ||||
|   self.columns = {} | ||||
|   for _, row in pairs(self.rows) do | ||||
|     if self.dataSpace then | ||||
|       self.dataSpace:addChild(row) | ||||
|     end | ||||
|  | ||||
|     self.columns[row.rowId] = {} | ||||
|     for _, column in pairs(row:getChildren()) do | ||||
|       table.insert(self.columns[row.rowId], column) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITable:selectRow(selectedRow) | ||||
|   if selectedRow == self.selectedRow then return end | ||||
|  | ||||
|   local previousSelectedRow = self.selectedRow | ||||
|   self.selectedRow = selectedRow | ||||
|  | ||||
|   if previousSelectedRow then | ||||
|     previousSelectedRow:setChecked(false) | ||||
|   end | ||||
|  | ||||
|   if selectedRow then | ||||
|     selectedRow:setChecked(true) | ||||
|   end | ||||
|  | ||||
|   signalcall(self.onSelectionChange, self, selectedRow, previousSelectedRow) | ||||
| end | ||||
|  | ||||
| function UITable:setTableData(tableData) | ||||
|   local headerHeight = 0 | ||||
|   if self.headerRow then | ||||
|     headerHeight = self.headerRow:getHeight() | ||||
|   end | ||||
|  | ||||
|   self.dataSpace = tableData | ||||
|   self.dataSpace:applyStyle({ height = self:getHeight()-headerHeight-self:getMarginTop() }) | ||||
| end | ||||
|  | ||||
| function UITable:setRowStyle(style, dontUpdate) | ||||
|   self.rowBaseStyle = style | ||||
|  | ||||
|   if not dontUpdate then | ||||
|     for _, row in pairs(self.rows) do | ||||
|       row:setStyle(style) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITable:setColumnStyle(style, dontUpdate) | ||||
|   self.columBaseStyle = style | ||||
|  | ||||
|   if not dontUpdate then | ||||
|     for _, columns in pairs(self.columns) do | ||||
|       for _, col in pairs(columns) do | ||||
|         col:setStyle(style) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITable:setHeaderRowStyle(style) | ||||
|   self.headerRowBaseStyle = style | ||||
|   if self.headerRow then | ||||
|     self.headerRow:setStyle(style) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITable:setHeaderColumnStyle(style) | ||||
|   self.headerColumnBaseStyle = style | ||||
|   for _, col in pairs(self.headerColumns) do | ||||
|     col:setStyle(style) | ||||
|   end | ||||
| end | ||||
|  | ||||
|  | ||||
| UITableRow = extends(UIWidget, "UITableRow") | ||||
|  | ||||
| function UITableRow:onFocusChange(focused) | ||||
|   if focused then | ||||
|     if self.table then self.table:selectRow(self) end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITableRow:onStyleApply(styleName, styleNode) | ||||
|   for name,value in pairs(styleNode) do | ||||
|     if name == 'even-background-color' then | ||||
|       self.evenBackgroundColor = value | ||||
|     elseif name == 'odd-background-color' then | ||||
|       self.oddBackgroundColor = value | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITableRow:updateBackgroundColor() | ||||
|   self.backgroundColor = nil | ||||
|  | ||||
|   local isEven = (self.rowId % 2 == 0) | ||||
|   if isEven and self.evenBackgroundColor then | ||||
|     self.backgroundColor = self.evenBackgroundColor | ||||
|   elseif not isEven and self.oddBackgroundColor then | ||||
|     self.backgroundColor = self.oddBackgroundColor | ||||
|   end | ||||
|  | ||||
|   if self.backgroundColor then | ||||
|     self:mergeStyle({ ['background-color'] = self.backgroundColor }) | ||||
|   end | ||||
| end | ||||
|  | ||||
|  | ||||
| UITableHeaderColumn = extends(UIButton, "UITableHeaderColumn") | ||||
|  | ||||
| function UITableHeaderColumn:onClick() | ||||
|   if self.table then | ||||
|     self.table:setSorting(self.colId) | ||||
|     self.table:sort() | ||||
|   end | ||||
| end | ||||
							
								
								
									
										78
									
								
								SabrehavenOTClient/modules/corelib/ui/uitextedit.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								SabrehavenOTClient/modules/corelib/ui/uitextedit.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| function UITextEdit:onStyleApply(styleName, styleNode) | ||||
|   for name,value in pairs(styleNode) do | ||||
|     if name == 'vertical-scrollbar' then | ||||
|       addEvent(function() | ||||
|         self:setVerticalScrollBar(self:getParent():getChildById(value)) | ||||
|       end) | ||||
|     elseif name == 'horizontal-scrollbar' then | ||||
|       addEvent(function() | ||||
|         self:setHorizontalScrollBar(self:getParent():getChildById(value)) | ||||
|       end) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITextEdit:onMouseWheel(mousePos, mouseWheel) | ||||
|   if self.verticalScrollBar and self:isMultiline() then | ||||
|     if mouseWheel == MouseWheelUp then | ||||
|       self.verticalScrollBar:decrement() | ||||
|     else | ||||
|       self.verticalScrollBar:increment() | ||||
|     end | ||||
|     return true | ||||
|   elseif self.horizontalScrollBar then | ||||
|     if mouseWheel == MouseWheelUp then | ||||
|       self.horizontalScrollBar:increment() | ||||
|     else | ||||
|       self.horizontalScrollBar:decrement() | ||||
|     end | ||||
|     return true | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UITextEdit:onTextAreaUpdate(virtualOffset, virtualSize, totalSize) | ||||
|   self:updateScrollBars() | ||||
| end | ||||
|  | ||||
| function UITextEdit:setVerticalScrollBar(scrollbar) | ||||
|   self.verticalScrollBar = scrollbar | ||||
|   self.verticalScrollBar.onValueChange = function(scrollbar, value) | ||||
|     local virtualOffset = self:getTextVirtualOffset() | ||||
|     virtualOffset.y = value | ||||
|     self:setTextVirtualOffset(virtualOffset) | ||||
|   end | ||||
|   self:updateScrollBars() | ||||
| end | ||||
|  | ||||
| function UITextEdit:setHorizontalScrollBar(scrollbar) | ||||
|   self.horizontalScrollBar = scrollbar | ||||
|   self.horizontalScrollBar.onValueChange = function(scrollbar, value) | ||||
|     local virtualOffset = self:getTextVirtualOffset() | ||||
|     virtualOffset.x = value | ||||
|     self:setTextVirtualOffset(virtualOffset) | ||||
|   end | ||||
|   self:updateScrollBars() | ||||
| end | ||||
|  | ||||
| function UITextEdit:updateScrollBars() | ||||
|   local scrollSize = self:getTextTotalSize() | ||||
|   local scrollWidth = math.max(scrollSize.width - self:getTextVirtualSize().width, 0) | ||||
|   local scrollHeight = math.max(scrollSize.height - self:getTextVirtualSize().height, 0) | ||||
|  | ||||
|   local scrollbar = self.verticalScrollBar | ||||
|   if scrollbar then | ||||
|     scrollbar:setMinimum(0) | ||||
|     scrollbar:setMaximum(scrollHeight) | ||||
|     scrollbar:setValue(self:getTextVirtualOffset().y) | ||||
|   end | ||||
|  | ||||
|   local scrollbar = self.horizontalScrollBar | ||||
|   if scrollbar then | ||||
|     scrollbar:setMinimum(0) | ||||
|     scrollbar:setMaximum(scrollWidth) | ||||
|     scrollbar:setValue(self:getTextVirtualOffset().x) | ||||
|   end | ||||
|  | ||||
| end | ||||
|  | ||||
| -- todo: ontext change, focus to cursor | ||||
							
								
								
									
										21
									
								
								SabrehavenOTClient/modules/corelib/ui/uiwidget.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SabrehavenOTClient/modules/corelib/ui/uiwidget.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| -- @docclass UIWidget | ||||
|  | ||||
| function UIWidget:setMargin(...) | ||||
|   local params = {...} | ||||
|   if #params == 1 then | ||||
|     self:setMarginTop(params[1]) | ||||
|     self:setMarginRight(params[1]) | ||||
|     self:setMarginBottom(params[1]) | ||||
|     self:setMarginLeft(params[1]) | ||||
|   elseif #params == 2 then | ||||
|     self:setMarginTop(params[1]) | ||||
|     self:setMarginRight(params[2]) | ||||
|     self:setMarginBottom(params[1]) | ||||
|     self:setMarginLeft(params[2]) | ||||
|   elseif #params == 4 then | ||||
|     self:setMarginTop(params[1]) | ||||
|     self:setMarginRight(params[2]) | ||||
|     self:setMarginBottom(params[3]) | ||||
|     self:setMarginLeft(params[4]) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										46
									
								
								SabrehavenOTClient/modules/corelib/ui/uiwindow.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								SabrehavenOTClient/modules/corelib/ui/uiwindow.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| -- @docclass | ||||
| UIWindow = extends(UIWidget, "UIWindow") | ||||
|  | ||||
| function UIWindow.create() | ||||
|   local window = UIWindow.internalCreate() | ||||
|   window:setTextAlign(AlignTopCenter) | ||||
|   window:setDraggable(true)   | ||||
|   window:setAutoFocusPolicy(AutoFocusFirst) | ||||
|   return window | ||||
| end | ||||
|  | ||||
| function UIWindow:onKeyDown(keyCode, keyboardModifiers) | ||||
|   if keyboardModifiers == KeyboardNoModifier then | ||||
|     if keyCode == KeyEnter then | ||||
|       signalcall(self.onEnter, self) | ||||
|     elseif keyCode == KeyEscape then | ||||
|       signalcall(self.onEscape, self) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function UIWindow:onFocusChange(focused) | ||||
|   if focused then self:raise() end | ||||
| end | ||||
|  | ||||
| function UIWindow:onDragEnter(mousePos) | ||||
|   if self.static then | ||||
|     return false | ||||
|   end | ||||
|   self:breakAnchors() | ||||
|   self.movingReference = { x = mousePos.x - self:getX(), y = mousePos.y - self:getY() } | ||||
|   return true | ||||
| end | ||||
|  | ||||
| function UIWindow:onDragLeave(droppedWidget, mousePos) | ||||
|   -- TODO: auto detect and reconnect anchors | ||||
| end | ||||
|  | ||||
| function UIWindow:onDragMove(mousePos, mouseMoved) | ||||
|   if self.static then | ||||
|     return | ||||
|   end | ||||
|   local pos = { x = mousePos.x - self.movingReference.x, y = mousePos.y - self.movingReference.y } | ||||
|   self:setPosition(pos) | ||||
|   self:bindRectToParent() | ||||
| end | ||||
							
								
								
									
										376
									
								
								SabrehavenOTClient/modules/corelib/util.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								SabrehavenOTClient/modules/corelib/util.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,376 @@ | ||||
| -- @docfuncs @{ | ||||
|  | ||||
| function print(...) | ||||
|   local msg = "" | ||||
|   local args = {...} | ||||
|   local appendSpace = #args > 1 | ||||
|   for i,v in ipairs(args) do | ||||
|     msg = msg .. tostring(v) | ||||
|     if appendSpace and i < #args then | ||||
|       msg = msg .. '    ' | ||||
|     end | ||||
|   end | ||||
|   g_logger.log(LogInfo, msg) | ||||
| end | ||||
|  | ||||
| function pinfo(msg) | ||||
|   g_logger.log(LogInfo, msg) | ||||
| end | ||||
|  | ||||
| function perror(msg) | ||||
|   g_logger.log(LogError, msg) | ||||
| end | ||||
|  | ||||
| function pwarning(msg) | ||||
|   g_logger.log(LogWarning, msg) | ||||
| end | ||||
|  | ||||
| function pdebug(msg) | ||||
|   g_logger.log(LogDebug, msg) | ||||
| end | ||||
|  | ||||
| function fatal(msg) | ||||
|   g_logger.log(LogFatal, msg) | ||||
| end | ||||
|  | ||||
| function exit() | ||||
|   g_app.exit() | ||||
| end | ||||
|  | ||||
| function quit() | ||||
|   g_app.exit() | ||||
| end | ||||
|  | ||||
| function connect(object, arg1, arg2, arg3) | ||||
|   local signalsAndSlots | ||||
|   local pushFront | ||||
|   if type(arg1) == 'string' then | ||||
|     signalsAndSlots = { [arg1] = arg2 } | ||||
|     pushFront = arg3 | ||||
|   else | ||||
|     signalsAndSlots = arg1 | ||||
|     pushFront = arg2 | ||||
|   end | ||||
|  | ||||
|   for signal,slot in pairs(signalsAndSlots) do | ||||
|     if not object[signal] then | ||||
|       local mt = getmetatable(object) | ||||
|       if mt and type(object) == 'userdata' then | ||||
|         object[signal] = function(...) | ||||
|           return signalcall(mt[signal], ...) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     if not object[signal] then | ||||
|       object[signal] = slot | ||||
|     elseif type(object[signal]) == 'function' then | ||||
|       object[signal] = { object[signal] } | ||||
|     end | ||||
|  | ||||
|     if type(slot) ~= 'function' then | ||||
|       perror(debug.traceback('unable to connect a non function value')) | ||||
|     end | ||||
|  | ||||
|     if type(object[signal]) == 'table' then | ||||
|       if pushFront then | ||||
|         table.insert(object[signal], 1, slot) | ||||
|       else | ||||
|         table.insert(object[signal], #object[signal]+1, slot) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function disconnect(object, arg1, arg2) | ||||
|   local signalsAndSlots | ||||
|   if type(arg1) == 'string' then | ||||
|     if arg2 == nil then | ||||
|       object[arg1] = nil | ||||
|       return | ||||
|     end | ||||
|     signalsAndSlots = { [arg1] = arg2 } | ||||
|   elseif type(arg1) == 'table' then | ||||
|     signalsAndSlots = arg1 | ||||
|   else | ||||
|     perror(debug.traceback('unable to disconnect')) | ||||
|   end | ||||
|  | ||||
|   for signal,slot in pairs(signalsAndSlots) do | ||||
|     if not object[signal] then | ||||
|     elseif type(object[signal]) == 'function' then | ||||
|       if object[signal] == slot then | ||||
|         object[signal] = nil | ||||
|       end | ||||
|     elseif type(object[signal]) == 'table' then | ||||
|       for k,func in pairs(object[signal]) do | ||||
|         if func == slot then | ||||
|           table.remove(object[signal], k) | ||||
|  | ||||
|           if #object[signal] == 1 then | ||||
|             object[signal] = object[signal][1] | ||||
|           end | ||||
|           break | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function newclass(name) | ||||
|   if not name then | ||||
|     perror(debug.traceback('new class has no name.')) | ||||
|   end | ||||
|  | ||||
|   local class = {} | ||||
|   function class.internalCreate() | ||||
|     local instance = {} | ||||
|     for k,v in pairs(class) do | ||||
|       instance[k] = v | ||||
|     end | ||||
|     return instance | ||||
|   end | ||||
|   class.create = class.internalCreate | ||||
|   class.__class = name | ||||
|   class.getClassName = function() return name end | ||||
|   return class | ||||
| end | ||||
|  | ||||
| function extends(base, name) | ||||
|   if not name then | ||||
|     perror(debug.traceback('extended class has no name.')) | ||||
|   end | ||||
|  | ||||
|   local derived = {} | ||||
|   function derived.internalCreate() | ||||
|     local instance = base.create() | ||||
|     for k,v in pairs(derived) do | ||||
|       instance[k] = v | ||||
|     end | ||||
|     return instance | ||||
|   end | ||||
|   derived.create = derived.internalCreate | ||||
|   derived.__class = name | ||||
|   derived.getClassName = function() return name end | ||||
|   return derived | ||||
| end | ||||
|  | ||||
| function runinsandbox(func, ...) | ||||
|   if type(func) == 'string' then | ||||
|     func, err = loadfile(resolvepath(func, 2)) | ||||
|     if not func then | ||||
|       error(err) | ||||
|     end | ||||
|   end | ||||
|   local env = { } | ||||
|   local oldenv = getfenv(0) | ||||
|   setmetatable(env, { __index = oldenv } ) | ||||
|   setfenv(0, env) | ||||
|   func(...) | ||||
|   setfenv(0, oldenv) | ||||
|   return env | ||||
| end | ||||
|  | ||||
| function loadasmodule(name, file) | ||||
|   file = file or resolvepath(name, 2) | ||||
|   if package.loaded[name] then | ||||
|     return package.loaded[name] | ||||
|   end | ||||
|   local env = runinsandbox(file) | ||||
|   package.loaded[name] = env | ||||
|   return env | ||||
| end | ||||
|  | ||||
| local function module_loader(modname) | ||||
|   local module = g_modules.getModule(modname) | ||||
|   if not module then | ||||
|     return '\n\tno module \'' .. modname .. '\'' | ||||
|   end | ||||
|   return function() | ||||
|     if not module:load() then | ||||
|       error('unable to load required module ' .. modname) | ||||
|     end | ||||
|     return module:getSandbox() | ||||
|   end | ||||
| end | ||||
| table.insert(package.loaders, 1, module_loader) | ||||
|  | ||||
| function import(table) | ||||
|   assert(type(table) == 'table') | ||||
|   local env = getfenv(2) | ||||
|   for k,v in pairs(table) do | ||||
|     env[k] = v | ||||
|   end | ||||
| end | ||||
|  | ||||
| function export(what, key) | ||||
|   if key ~= nil then | ||||
|     _G[key] = what | ||||
|   else | ||||
|     for k,v in pairs(what) do | ||||
|       _G[k] = v | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function unexport(key) | ||||
|   if type(key) == 'table' then | ||||
|     for _k,v in pairs(key) do | ||||
|       _G[v] = nil | ||||
|     end | ||||
|   else | ||||
|     _G[key] = nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| function getfsrcpath(depth) | ||||
|   depth = depth or 2 | ||||
|   local info = debug.getinfo(1+depth, "Sn") | ||||
|   local path | ||||
|   if info.short_src then | ||||
|     path = info.short_src:match("(.*)/.*") | ||||
|   end | ||||
|   if not path then | ||||
|     path = '/' | ||||
|   elseif path:sub(0, 1) ~= '/' then | ||||
|     path = '/' .. path | ||||
|   end | ||||
|   return path | ||||
| end | ||||
|  | ||||
| function resolvepath(filePath, depth) | ||||
|   if not filePath then return nil end | ||||
|   depth = depth or 1 | ||||
|   if filePath then | ||||
|     if filePath:sub(0, 1) ~= '/' then | ||||
|       local basepath = getfsrcpath(depth+1) | ||||
|       if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end | ||||
|       return  basepath .. filePath | ||||
|     else | ||||
|       return filePath | ||||
|     end | ||||
|   else | ||||
|     local basepath = getfsrcpath(depth+1) | ||||
|     if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end | ||||
|     return basepath | ||||
|   end | ||||
| end | ||||
|  | ||||
| function toboolean(v) | ||||
|   if type(v) == 'string' then | ||||
|     v = v:trim():lower() | ||||
|     if v == '1' or v == 'true' then | ||||
|       return true | ||||
|     end | ||||
|   elseif type(v) == 'number' then | ||||
|     if v == 1 then | ||||
|       return true | ||||
|     end | ||||
|   elseif type(v) == 'boolean' then | ||||
|     return v | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function fromboolean(boolean) | ||||
|   if boolean then | ||||
|     return 'true' | ||||
|   else | ||||
|     return 'false' | ||||
|   end | ||||
| end | ||||
|  | ||||
| function booleantonumber(boolean) | ||||
|   if boolean then | ||||
|     return 1 | ||||
|   else | ||||
|     return 0 | ||||
|   end | ||||
| end | ||||
|  | ||||
| function numbertoboolean(number) | ||||
|   if number ~= 0 then | ||||
|     return true | ||||
|   else | ||||
|     return false | ||||
|   end | ||||
| end | ||||
|  | ||||
| function protectedcall(func, ...) | ||||
|   local status, ret = pcall(func, ...) | ||||
|   if status then | ||||
|     return ret | ||||
|   end | ||||
|  | ||||
|   perror(ret) | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function signalcall(param, ...) | ||||
|   if type(param) == 'function' then | ||||
|     local status, ret = pcall(param, ...) | ||||
|     if status then | ||||
|       return ret | ||||
|     else | ||||
|       perror(ret) | ||||
|     end | ||||
|   elseif type(param) == 'table' then | ||||
|     for k,v in pairs(param) do | ||||
|       local status, ret = pcall(v, ...) | ||||
|       if status then | ||||
|         if ret then return true end | ||||
|       else | ||||
|         perror(ret) | ||||
|       end | ||||
|     end | ||||
|   elseif param ~= nil then | ||||
|     error('attempt to call a non function value') | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function tr(s, ...) | ||||
|   return string.format(s, ...) | ||||
| end | ||||
|  | ||||
| function getOppositeAnchor(anchor) | ||||
|   if anchor == AnchorLeft then | ||||
|     return AnchorRight | ||||
|   elseif anchor == AnchorRight then | ||||
|     return AnchorLeft | ||||
|   elseif anchor == AnchorTop then | ||||
|     return AnchorBottom | ||||
|   elseif anchor == AnchorBottom then | ||||
|     return AnchorTop | ||||
|   elseif anchor == AnchorVerticalCenter then | ||||
|     return AnchorHorizontalCenter | ||||
|   elseif anchor == AnchorHorizontalCenter then | ||||
|     return AnchorVerticalCenter | ||||
|   end | ||||
|   return anchor | ||||
| end | ||||
|  | ||||
| function makesingleton(obj) | ||||
|   local singleton = {} | ||||
|   if obj.getClassName then | ||||
|     for key,value in pairs(_G[obj:getClassName()]) do | ||||
|       if type(value) == 'function' then | ||||
|         singleton[key] = function(...) return value(obj, ...) end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   return singleton | ||||
| end | ||||
|  | ||||
| function comma_value(amount) | ||||
|   local formatted = amount | ||||
|   while true do   | ||||
|     formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2') | ||||
|     if (k==0) then | ||||
|       break | ||||
|     end | ||||
|   end | ||||
|   return formatted | ||||
| end | ||||
|  | ||||
| -- @} | ||||
							
								
								
									
										22
									
								
								SabrehavenOTClient/modules/crash_reporter/crash_reporter.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								SabrehavenOTClient/modules/crash_reporter/crash_reporter.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| local CRASH_FILE = "exception.dmp" | ||||
|  | ||||
| function init() | ||||
|   if g_resources.fileExists(CRASH_FILE) then | ||||
|     local crashLog = g_resources.readFileContents(CRASH_FILE) | ||||
|     local clientLog = g_logger.getLastLog() | ||||
|     HTTP.post(Services.crash, { | ||||
|       version = APP_VERSION, | ||||
|       build = g_app.getVersion(), | ||||
|       os = g_app.getOs(), | ||||
|       platform = g_window.getPlatformType(), | ||||
|       crash = base64.encode(crashLog), | ||||
|       log = base64.encode(clientLog) | ||||
|     }, function(data, err) | ||||
|       if err then  | ||||
|         return g_logger.error("Error while reporting crash report: " .. err) | ||||
|       end | ||||
|       g_resources.deleteFile(CRASH_FILE) | ||||
|     end)       | ||||
|   end | ||||
| end | ||||
|  | ||||
| @@ -0,0 +1,8 @@ | ||||
| Module | ||||
|   name: crash_reporter | ||||
|   description: Sends crash log to remote server | ||||
|   author: otclient@otclient.ovh | ||||
|   website: otclient.ovh | ||||
|   reloadable: false | ||||
|   scripts: [ crash_reporter ] | ||||
|   @onLoad: init() | ||||
							
								
								
									
										391
									
								
								SabrehavenOTClient/modules/game_actionbar/actionbar.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								SabrehavenOTClient/modules/game_actionbar/actionbar.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,391 @@ | ||||
| actionPanel1 = nil | ||||
| actionPanel2 = nil | ||||
|  | ||||
| local actionConfig | ||||
| local hotkeyAssignWindow | ||||
| local actionButtonsInPanel = 50 | ||||
|  | ||||
| ActionTypes = { | ||||
|   USE = 0, | ||||
|   USE_SELF = 1, | ||||
|   USE_TARGET = 2, | ||||
|   USE_WITH = 3, | ||||
|   EQUIP = 4 | ||||
| } | ||||
|  | ||||
| ActionColors = { | ||||
|   empty = '#00000033', | ||||
|   text = '#00000033', | ||||
|   itemUse = '#8888FF88', | ||||
|   itemUseSelf = '#00FF0088', | ||||
|   itemUseTarget = '#FF000088', | ||||
|   itemUseWith = '#F5B32588', | ||||
|   itemEquip = '#FFFFFF88' | ||||
| } | ||||
|  | ||||
| function init() | ||||
|   local bottomPanel = modules.game_interface.getActionPanel() | ||||
|   actionPanel1 = g_ui.loadUI('actionbar', bottomPanel) | ||||
|   bottomPanel:moveChildToIndex(actionPanel1, 1) | ||||
|   actionPanel2 = g_ui.loadUI('actionbar', bottomPanel) | ||||
|   bottomPanel:moveChildToIndex(actionPanel2, 1) | ||||
|    | ||||
|   actionConfig = g_configs.create("/actionbar.otml") | ||||
|      | ||||
|   connect(g_game, { | ||||
|     onGameStart = online, | ||||
|     onGameEnd = offline, | ||||
|     onSpellGroupCooldown = onSpellGroupCooldown, | ||||
|     onSpellCooldown = onSpellCooldown | ||||
|   }) | ||||
|    | ||||
|   if g_game.isOnline() then | ||||
|     online() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   disconnect(g_game, { | ||||
|     onGameStart = online, | ||||
|     onGameEnd = offline, | ||||
|     onSpellGroupCooldown = onSpellGroupCooldown, | ||||
|     onSpellCooldown = onSpellCooldown | ||||
|   })   | ||||
|  | ||||
|   -- remove hotkeys, also saves config | ||||
|   if actionPanel1.tabBar:getChildCount() > 0 and actionPanel2.tabBar:getChildCount() > 0 then | ||||
|     offline() | ||||
|   end | ||||
|    | ||||
|   actionPanel1:destroy() | ||||
|   actionPanel2:destroy() | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   if not g_game.isOnline() then return end | ||||
|   actionPanel1:setOn(g_settings.getBoolean("actionBar1", false)) | ||||
|   actionPanel2:setOn(g_settings.getBoolean("actionBar2", false)) | ||||
| end | ||||
|  | ||||
| function hide() | ||||
|   actionPanel1:setOn(false) | ||||
|   actionPanel2:setOn(false) | ||||
| end | ||||
|  | ||||
| function switchMode(newMode) | ||||
|   if newMode then | ||||
|     actionPanel1:setImageColor('#ffffff88')   | ||||
|     actionPanel2:setImageColor('#ffffff88')   | ||||
|   else | ||||
|     actionPanel1:setImageColor('white')     | ||||
|     actionPanel2:setImageColor('white')     | ||||
|   end | ||||
| end | ||||
|  | ||||
| function online() | ||||
|   setupActionPanel(1, actionPanel1) | ||||
|   setupActionPanel(2, actionPanel2) | ||||
|   show() | ||||
| end | ||||
|  | ||||
| function offline() | ||||
|   hide() | ||||
|   if hotkeyAssignWindow then | ||||
|     hotkeyAssignWindow:destroy() | ||||
|     hotkeyAssignWindow = nil | ||||
|   end | ||||
|  | ||||
|   local gameRootPanel = modules.game_interface.getRootPanel() | ||||
|   for index, panel in ipairs({actionPanel1, actionPanel2}) do | ||||
|     local config = {} | ||||
|     for i, child in ipairs(panel.tabBar:getChildren()) do | ||||
|       if child.config then | ||||
|         table.insert(config, child.config) | ||||
|         if type(child.config.hotkey) == 'string' and child.config.hotkey:len() > 0 then | ||||
|           g_keyboard.unbindKeyPress(child.config.hotkey, child.callback, gameRootPanel) | ||||
|         end | ||||
|       else | ||||
|         table.insert(config, {}) | ||||
|       end | ||||
|       if child.cooldownEvent then | ||||
|         removeEvent(child.cooldownEvent) | ||||
|       end | ||||
|     end | ||||
|     actionConfig:setNode('actions_' .. index, config) | ||||
|     panel.tabBar:destroyChildren() | ||||
|   end | ||||
|   actionConfig:save() | ||||
| end | ||||
|  | ||||
| function setupActionPanel(index, panel) | ||||
|   local rawConfig = actionConfig:getNode('actions_' .. index) or {} | ||||
|   local config = {} | ||||
|   for i, buttonConfig in pairs(rawConfig) do -- sorting, because key in rawConfig is string | ||||
|     config[tonumber(i)] = buttonConfig | ||||
|   end | ||||
|    | ||||
|   for i=1,actionButtonsInPanel do | ||||
|     local action = g_ui.createWidget('ActionButton', panel.tabBar) | ||||
|     action.config = config[i] or {} | ||||
|     setupAction(action) | ||||
|   end   | ||||
|    | ||||
|   panel.nextButton.onClick = function() | ||||
|     panel.tabBar:moveChildToIndex(panel.tabBar:getFirstChild(), panel.tabBar:getChildCount()) | ||||
|   end | ||||
|   panel.prevButton.onClick = function() | ||||
|     panel.tabBar:moveChildToIndex(panel.tabBar:getLastChild(), 1)   | ||||
|   end | ||||
| end | ||||
|  | ||||
| function setupAction(action) | ||||
|   local config = action.config | ||||
|   action.item:setShowCount(false) | ||||
|   action.onMouseRelease = actionOnMouseRelease | ||||
|   action.onTouchRelease = actionOnMouseRelease | ||||
|   action.callback = function(k, c, ticks) executeAction(action, ticks) end | ||||
|   action.item.onItemChange = nil -- disable callbacks for setup | ||||
|    | ||||
|   if config then | ||||
|     if type(config.text) == 'number' then | ||||
|       config.text = tostring(config.text) | ||||
|     end | ||||
|     if type(config.hotkey) == 'number' then | ||||
|       config.hotkey = tostring(config.hotkey) | ||||
|     end | ||||
|     if type(config.hotkey) == 'string' and config.hotkey:len() > 0 then | ||||
|       local gameRootPanel = modules.game_interface.getRootPanel() | ||||
|       g_keyboard.bindKeyPress(config.hotkey, action.callback, gameRootPanel) | ||||
|       action.hotkeyLabel:setText(config.hotkey) | ||||
|     else | ||||
|       action.hotkeyLabel:setText("") | ||||
|     end | ||||
|  | ||||
|     action.text:setImageSource("") | ||||
|     action.cooldownTill = 0 | ||||
|     action.cooldownStart = 0 | ||||
|     if type(config.text) == 'string' and config.text:len() > 0 then | ||||
|       action.text:setText(config.text) | ||||
|       action.item:setBorderColor(ActionColors.text) | ||||
|       action.item:setOn(true) -- removes background | ||||
|       action.item:setItemId(0) | ||||
|       if Spells then | ||||
|         local spell, profile = Spells.getSpellByWords(config.text:lower()) | ||||
|         action.spell = spell | ||||
|         if action.spell and action.spell.icon and profile then | ||||
|           action.text:setImageSource(SpelllistSettings[profile].iconFile) | ||||
|           action.text:setImageClip(Spells.getImageClip(SpellIcons[action.spell.icon][1], profile)) | ||||
|           action.text:setText("") | ||||
|         end | ||||
|       end | ||||
|     else       | ||||
|       action.text:setText("") | ||||
|       action.spell = nil | ||||
|       if type(config.item) == 'number' and config.item > 100 then | ||||
|         --action.item:setOn(true) | ||||
|         --action.item:setItemId(config.item) | ||||
|         --action.item:setItemCount(config.count or 1) | ||||
|         --setupActionType(action, config.actionType) | ||||
|       else | ||||
|         action.item:setItemId(0) | ||||
|         action.item:setOn(false) | ||||
|         action.item:setBorderColor(ActionColors.empty) | ||||
|       end     | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   action.item.onItemChange = actionOnItemChange | ||||
| end | ||||
|  | ||||
| function setupActionType(action, actionType) | ||||
|   local item = action.item:getItem() | ||||
|   if action.item:getItem():isMultiUse() then | ||||
|     if not actionType or actionType <= ActionTypes.USE then | ||||
|      actionType = ActionTypes.USE_WITH | ||||
|     end | ||||
|   elseif g_game.getClientVersion() >= 910 then | ||||
|     if actionType ~= ActionTypes.USE and actionType ~= ActionTypes.EQUIP then | ||||
|       actionType = ActionTypes.USE | ||||
|     end | ||||
|   else | ||||
|     actionType = ActionTypes.USE | ||||
|   end | ||||
|  | ||||
|   action.config.actionType = actionType | ||||
|   if action.config.actionType == ActionTypes.USE then | ||||
|     action.item:setBorderColor(ActionColors.itemUse) | ||||
|   elseif action.config.actionType == ActionTypes.USE_SELF then | ||||
|     action.item:setBorderColor(ActionColors.itemUseSelf) | ||||
|   elseif action.config.actionType == ActionTypes.USE_TARGET then | ||||
|     action.item:setBorderColor(ActionColors.itemUseTarget) | ||||
|   elseif action.config.actionType == ActionTypes.USE_WITH then | ||||
|     action.item:setBorderColor(ActionColors.itemUseWith) | ||||
|   elseif action.config.actionType == ActionTypes.EQUIP then | ||||
|     action.item:setBorderColor(ActionColors.itemEquip) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function updateAction(action, newConfig) | ||||
|   local config = action.config | ||||
|   if type(config.hotkey) == 'string' and config.hotkey:len() > 0 then | ||||
|     local gameRootPanel = modules.game_interface.getRootPanel() | ||||
|     g_keyboard.unbindKeyPress(config.hotkey, action.callback, gameRootPanel) | ||||
|   end | ||||
|   for key, val in pairs(newConfig) do | ||||
|     action.config[key] = val | ||||
|   end | ||||
|   setupAction(action) | ||||
| end | ||||
|  | ||||
| function actionOnMouseRelease(action, mousePosition, mouseButton) | ||||
|   if mouseButton == MouseTouch then return end | ||||
|   if mouseButton == MouseRightButton or not action.item:isOn() then | ||||
|     local menu = g_ui.createWidget('PopupMenu') | ||||
|     menu:setGameMenu(true) | ||||
|     menu:addOption(tr('Set text'), function()  | ||||
|       modules.client_textedit.singlelineEditor(action.config.text or "", function(newText) | ||||
|         updateAction(action, {text=newText, item=0}) | ||||
|       end) | ||||
|     end) | ||||
|     menu:addOption(tr('Set hotkey'), function() | ||||
|       if hotkeyAssignWindow then | ||||
|         hotkeyAssignWindow:destroy() | ||||
|       end | ||||
|       local assignWindow = g_ui.createWidget('ActionAssignWindow', rootWidget) | ||||
|       assignWindow:grabKeyboard() | ||||
|       assignWindow.comboPreview.keyCombo = '' | ||||
|       assignWindow.onKeyDown = function(assignWindow, keyCode, keyboardModifiers) | ||||
|         local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers) | ||||
|         assignWindow.comboPreview:setText(tr('Current action hotkey: %s', keyCombo)) | ||||
|         assignWindow.comboPreview.keyCombo = keyCombo | ||||
|         assignWindow.comboPreview:resizeToText() | ||||
|         return true | ||||
|       end | ||||
|       assignWindow.onDestroy = function(widget) | ||||
|         if widget == hotkeyAssignWindow then | ||||
|           hotkeyAssignWindow = nil | ||||
|         end | ||||
|       end | ||||
|       assignWindow.addButton.onClick = function() | ||||
|         updateAction(action, {hotkey=tostring(assignWindow.comboPreview.keyCombo)}) | ||||
|         assignWindow:destroy() | ||||
|       end | ||||
|       hotkeyAssignWindow = assignWindow | ||||
|     end) | ||||
|     menu:addSeparator() | ||||
|     menu:addOption(tr('Clear'), function() | ||||
|       updateAction(action, {hotkey="", text="", item=0, count=1}) | ||||
|     end) | ||||
|     menu:display(mousePosition) | ||||
|     return true | ||||
|   elseif mouseButton == MouseLeftButton or mouseButton == MouseTouch2 or mouseButton == MouseTouch3 then | ||||
|     action.callback() | ||||
|     return true | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function actionOnItemChange(widget) | ||||
|   updateAction(widget:getParent(), {text="", item=widget:getItemId(), count=widget:getItemCountOrSubType()}) | ||||
| end | ||||
|  | ||||
| function onSpellCooldown(iconId, duration) | ||||
|   for index, panel in ipairs({actionPanel1, actionPanel2}) do | ||||
|     for i, child in ipairs(panel.tabBar:getChildren()) do | ||||
|       if child.spell and child.spell.id == iconId then | ||||
|         startCooldown(child, duration) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function onSpellGroupCooldown(groupId, duration) | ||||
|   for index, panel in ipairs({actionPanel1, actionPanel2}) do | ||||
|     for i, child in ipairs(panel.tabBar:getChildren()) do | ||||
|       if child.spell and child.spell.group then | ||||
|         for group, duration in pairs(child.spell.group) do | ||||
|           if groupId == group then | ||||
|             startCooldown(child, duration) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| function startCooldown(action, duration) | ||||
|   if type(action.cooldownTill) == 'number' and action.cooldownTill > g_clock.millis() + duration then | ||||
|     return -- already has cooldown with greater duration | ||||
|   end | ||||
|   action.cooldownStart = g_clock.millis() | ||||
|   action.cooldownTill = g_clock.millis() + duration | ||||
|   updateCooldown(action) | ||||
| end | ||||
|  | ||||
| function updateCooldown(action) | ||||
|   if not action or not action.cooldownTill then return end | ||||
|   local timeleft = action.cooldownTill - g_clock.millis() | ||||
|   if timeleft <= 30 then | ||||
|     action.cooldown:setPercent(100) | ||||
|     action.cooldownEvent = nil     | ||||
|     return | ||||
|   end | ||||
|   local duration = action.cooldownTill - action.cooldownStart | ||||
|   action.cooldown:setPercent(100 - math.floor(100 * timeleft / duration)) | ||||
|   action.cooldownEvent = scheduleEvent(function() updateCooldown(action) end, 30) | ||||
| end | ||||
|  | ||||
| function executeAction(action, ticks) | ||||
|   if not action.config then return end | ||||
|   if type(ticks) ~= 'number' then ticks = 0 end | ||||
|  | ||||
|   local actionDelay = 100   | ||||
|   if ticks == 0 then | ||||
|     actionDelay = 200 -- for first use | ||||
|   elseif action.actionDelayTo ~= nil and g_clock.millis() < action.actionDelayTo then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   local actionType = action.config.actionType | ||||
|  | ||||
|   if type(action.config.text) == 'string' and action.config.text:len() > 0 then | ||||
|     if g_app.isMobile() then -- turn to direction of targer | ||||
|       local target = g_game.getAttackingCreature() | ||||
|       if target then | ||||
|         local pos = g_game.getLocalPlayer():getPosition() | ||||
|         local tpos = target:getPosition() | ||||
|         if pos and tpos then | ||||
|           local offx = tpos.x - pos.x | ||||
|           local offy = tpos.y - pos.y | ||||
|           if offy < 0 and offx <= 0 and math.abs(offx) < math.abs(offy) then | ||||
|             g_game.turn(Directions.North) | ||||
|           elseif offy > 0 and offx >= 0 and math.abs(offx) < math.abs(offy) then | ||||
|             g_game.turn(Directions.South) | ||||
|           elseif offx < 0 and offy <= 0 and math.abs(offx) > math.abs(offy) then | ||||
|             g_game.turn(Directions.West) | ||||
|           elseif offx > 0 and offy >= 0 and math.abs(offx) > math.abs(offy) then | ||||
|             g_game.turn(Directions.East) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|     if modules.game_interface.isChatVisible() then | ||||
|       modules.game_console.sendMessage(action.config.text)     | ||||
|     else | ||||
|       g_game.talk(action.config.text) | ||||
|     end | ||||
|     action.actionDelayTo = g_clock.millis() + actionDelay | ||||
|   elseif action.item:getItemId() > 0 then     | ||||
|     if actionType == ActionTypes.USE then | ||||
|       action.actionDelayTo = g_clock.millis() + actionDelay | ||||
|     elseif actionType == ActionTypes.USE_SELF then | ||||
|       action.actionDelayTo = g_clock.millis() + actionDelay | ||||
|     elseif actionType == ActionTypes.USE_TARGET then | ||||
|       action.actionDelayTo = g_clock.millis() + actionDelay | ||||
|     elseif actionType == ActionTypes.USE_WITH then | ||||
|       action.actionDelayTo = g_clock.millis() + actionDelay | ||||
|     elseif actionType == ActionTypes.EQUIP then | ||||
|       action.actionDelayTo = g_clock.millis() + actionDelay | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: game_actionbar | ||||
|   description: Action bar | ||||
|   author: otclient@otclient.ovh | ||||
|   website: otclient.ovh | ||||
|   sandboxed: true | ||||
|   scripts: [ actionbar ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										145
									
								
								SabrehavenOTClient/modules/game_actionbar/actionbar.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								SabrehavenOTClient/modules/game_actionbar/actionbar.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| ActionButton < Panel | ||||
|   font: cipsoftFont | ||||
|   anchors.top: parent.top | ||||
|   anchors.bottom: parent.bottom | ||||
|   width: 40 | ||||
|   padding: 1 1 1 1 | ||||
|   margin-left: 1 | ||||
|    | ||||
|   $first: | ||||
|     anchors.left: parent.left | ||||
|  | ||||
|   $!first: | ||||
|     anchors.left: prev.right | ||||
|      | ||||
|   Item | ||||
|     id: item | ||||
|     anchors.fill: parent | ||||
|     &selectable: true | ||||
|     &editable: false | ||||
|     virtual: true | ||||
|     border-width: 1 | ||||
|      | ||||
|     border-color: #00000000 | ||||
|      | ||||
|     $!on: | ||||
|       image-source: /images/game/actionbarslot | ||||
|  | ||||
|   Label | ||||
|     id: text | ||||
|     anchors.fill: parent | ||||
|     text-auto-resize: true | ||||
|     text-wrap: true | ||||
|     phantom: true | ||||
|     text-align: center | ||||
|     font: verdana-9px | ||||
|  | ||||
|   Label | ||||
|     id: hotkeyLabel | ||||
|     anchors.top: parent.top | ||||
|     anchors.left: parent.left | ||||
|     margin: 2 3 3 3 | ||||
|     text-auto-resize: true | ||||
|     text-wrap: false | ||||
|     phantom: true | ||||
|     font: small-9px | ||||
|     color: yellow | ||||
|  | ||||
|   UIProgressRect | ||||
|     id: cooldown | ||||
|     background: #585858AA | ||||
|     percent: 100 | ||||
|     focusable: false | ||||
|     phantom: true | ||||
|     anchors.fill: parent | ||||
|     margin: 1 1 1 1 | ||||
|      | ||||
| Panel | ||||
|   id: actionBar | ||||
|   focusable: false | ||||
|   image-source: /images/ui/panel_side | ||||
|   image-border: 4 | ||||
|   margin-top: -1 | ||||
|    | ||||
|   $on: | ||||
|     height: 40 | ||||
|     visible: true | ||||
|      | ||||
|   $!on: | ||||
|     height: 0 | ||||
|     visible: false | ||||
|  | ||||
|   TabButton | ||||
|     id: prevButton | ||||
|     icon: /images/game/console/leftarrow | ||||
|     anchors.left: parent.left | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: parent.bottom | ||||
|     margin-left: 1 | ||||
|     margin-top: 1 | ||||
|     margin-bottom: 2 | ||||
|      | ||||
|   Panel | ||||
|     id: tabBar | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: parent.bottom | ||||
|     anchors.left: prev.right | ||||
|     anchors.right: next.left | ||||
|     margin-right: 3 | ||||
|     clipping: true | ||||
|      | ||||
|   TabButton | ||||
|     id: nextButton | ||||
|     icon: /images/game/console/rightarrow | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: parent.top | ||||
|     anchors.bottom: parent.bottom | ||||
|     margin-right: 1 | ||||
|     margin-top: 1 | ||||
|     margin-bottom: 2 | ||||
|  | ||||
|  | ||||
| ActionAssignWindow < MainWindow | ||||
|   id: assignWindow | ||||
|   !text: tr('Button Assign') | ||||
|   size: 360 150 | ||||
|   @onEscape: self:destroy() | ||||
|  | ||||
|   Label | ||||
|     !text: tr('Please, press the key you wish to use for action') | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.top: parent.top | ||||
|     text-auto-resize: true | ||||
|     text-align: left | ||||
|  | ||||
|   Label | ||||
|     id: comboPreview | ||||
|     !text: tr('Current action hotkey: %s', 'none') | ||||
|     anchors.horizontalCenter: parent.horizontalCenter | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 10 | ||||
|     text-auto-resize: true | ||||
|  | ||||
|   HorizontalSeparator | ||||
|     id: separator | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: next.top | ||||
|     margin-bottom: 10 | ||||
|  | ||||
|   Button | ||||
|     id: addButton | ||||
|     !text: tr('Add') | ||||
|     width: 64 | ||||
|     anchors.right: next.left | ||||
|     anchors.bottom: parent.bottom | ||||
|     margin-right: 10 | ||||
|  | ||||
|   Button | ||||
|     id: cancelButton | ||||
|     !text: tr('Cancel') | ||||
|     width: 64 | ||||
|     anchors.right: parent.right | ||||
|     anchors.bottom: parent.bottom | ||||
|     @onClick: self:getParent():destroy() | ||||
							
								
								
									
										458
									
								
								SabrehavenOTClient/modules/game_battle/battle.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										458
									
								
								SabrehavenOTClient/modules/game_battle/battle.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,458 @@ | ||||
| battleWindow = nil | ||||
| battleButton = nil | ||||
| battlePanel = nil | ||||
| filterPanel = nil | ||||
| toggleFilterButton = nil | ||||
|  | ||||
| mouseWidget = nil | ||||
| updateEvent = nil | ||||
|  | ||||
| hoveredCreature = nil | ||||
| newHoveredCreature = nil | ||||
| prevCreature = nil | ||||
|  | ||||
| battleButtons = {} | ||||
| local ageNumber = 1 | ||||
| local ages = {} | ||||
|  | ||||
| function init()   | ||||
|   g_ui.importStyle('battlebutton') | ||||
|   battleButton = modules.client_topmenu.addRightGameToggleButton('battleButton', tr('Battle') .. ' (Ctrl+B)', '/images/topbuttons/battle', toggle, false, 2) | ||||
|   battleButton:setOn(true) | ||||
|   battleWindow = g_ui.loadUI('battle', modules.game_interface.getRightPanel()) | ||||
|   g_keyboard.bindKeyDown('Ctrl+B', toggle) | ||||
|  | ||||
|   -- this disables scrollbar auto hiding | ||||
|   local scrollbar = battleWindow:getChildById('miniwindowScrollBar') | ||||
|   scrollbar:mergeStyle({ ['$!on'] = { }}) | ||||
|  | ||||
|   battlePanel = battleWindow:recursiveGetChildById('battlePanel') | ||||
|  | ||||
|   filterPanel = battleWindow:recursiveGetChildById('filterPanel') | ||||
|   toggleFilterButton = battleWindow:recursiveGetChildById('toggleFilterButton') | ||||
|  | ||||
|   if isHidingFilters() then | ||||
|     hideFilterPanel() | ||||
|   end | ||||
|  | ||||
|   local sortTypeBox = filterPanel.sortPanel.sortTypeBox | ||||
|   local sortOrderBox = filterPanel.sortPanel.sortOrderBox | ||||
|  | ||||
|   mouseWidget = g_ui.createWidget('UIButton') | ||||
|   mouseWidget:setVisible(false) | ||||
|   mouseWidget:setFocusable(false) | ||||
|   mouseWidget.cancelNextRelease = false | ||||
|  | ||||
|   battleWindow:setContentMinimumHeight(80) | ||||
|  | ||||
|   sortTypeBox:addOption('Name', 'name') | ||||
|   sortTypeBox:addOption('Distance', 'distance') | ||||
|   sortTypeBox:addOption('Total age', 'age') | ||||
|   sortTypeBox:addOption('Screen age', 'screenage') | ||||
|   sortTypeBox:addOption('Health', 'health') | ||||
|   sortTypeBox:setCurrentOptionByData(getSortType()) | ||||
|   sortTypeBox.onOptionChange = onChangeSortType | ||||
|  | ||||
|   sortOrderBox:addOption('Asc.', 'asc') | ||||
|   sortOrderBox:addOption('Desc.', 'desc') | ||||
|   sortOrderBox:setCurrentOptionByData(getSortOrder()) | ||||
|   sortOrderBox.onOptionChange = onChangeSortOrder | ||||
|  | ||||
|   battleWindow:setup() | ||||
|    | ||||
|   for i=1,30 do | ||||
|     local battleButton = g_ui.createWidget('BattleButton', battlePanel) | ||||
|     battleButton:setup() | ||||
|     battleButton:hide() | ||||
|     battleButton.onHoverChange = onBattleButtonHoverChange | ||||
|     battleButton.onMouseRelease = onBattleButtonMouseRelease | ||||
|     table.insert(battleButtons, battleButton) | ||||
|   end | ||||
|    | ||||
|   updateBattleList() | ||||
|    | ||||
|   connect(LocalPlayer, { | ||||
|     onPositionChange = onPlayerPositionChange | ||||
|   }) | ||||
|   connect(Creature, { | ||||
|     onAppear = updateSquare, | ||||
|     onDisappear = updateSquare | ||||
|   })   | ||||
|   connect(g_game, {  | ||||
|     onAttackingCreatureChange = updateSquare, | ||||
|     onFollowingCreatureChange = updateSquare  | ||||
|   }) | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   if battleButton == nil then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   battleButtons = {} | ||||
|    | ||||
|   g_keyboard.unbindKeyDown('Ctrl+B') | ||||
|   battleButton:destroy() | ||||
|   battleWindow:destroy() | ||||
|   mouseWidget:destroy() | ||||
| 	 | ||||
|   disconnect(LocalPlayer, { | ||||
|     onPositionChange = onPlayerPositionChange | ||||
|   }) | ||||
|   disconnect(Creature, { | ||||
|     onAppear = onCreatureAppear, | ||||
|     onDisappear = onCreatureDisappear | ||||
|   })   | ||||
|   disconnect(g_game, {  | ||||
|     onAttackingCreatureChange = updateSquare, | ||||
|     onFollowingCreatureChange = updateSquare  | ||||
|   }) | ||||
|  | ||||
|   removeEvent(updateEvent) | ||||
| end | ||||
|  | ||||
| function toggle() | ||||
|   if battleButton:isOn() then | ||||
|     battleWindow:close() | ||||
|     battleButton:setOn(false) | ||||
|   else | ||||
|     battleWindow:open() | ||||
|     battleButton:setOn(true) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function onMiniWindowClose() | ||||
|   battleButton:setOn(false) | ||||
| end | ||||
|  | ||||
| function getSortType() | ||||
|   local settings = g_settings.getNode('BattleList') | ||||
|   if not settings then | ||||
|     if g_app.isMobile() then | ||||
|       return 'distance' | ||||
|     else | ||||
|       return 'name' | ||||
|     end | ||||
|   end | ||||
|   return settings['sortType'] | ||||
| end | ||||
|  | ||||
| function setSortType(state) | ||||
|   settings = {} | ||||
|   settings['sortType'] = state | ||||
|   g_settings.mergeNode('BattleList', settings) | ||||
|  | ||||
|   checkCreatures() | ||||
| end | ||||
|  | ||||
| function getSortOrder() | ||||
|   local settings = g_settings.getNode('BattleList') | ||||
|   if not settings then | ||||
|     return 'asc' | ||||
|   end | ||||
|   return settings['sortOrder'] | ||||
| end | ||||
|  | ||||
| function setSortOrder(state) | ||||
|   settings = {} | ||||
|   settings['sortOrder'] = state | ||||
|   g_settings.mergeNode('BattleList', settings) | ||||
|  | ||||
|   checkCreatures() | ||||
| end | ||||
|  | ||||
| function isSortAsc() | ||||
|     return getSortOrder() == 'asc' | ||||
| end | ||||
|  | ||||
| function isSortDesc() | ||||
|     return getSortOrder() == 'desc' | ||||
| end | ||||
|  | ||||
| function isHidingFilters() | ||||
|   local settings = g_settings.getNode('BattleList') | ||||
|   if not settings then | ||||
|     return false | ||||
|   end | ||||
|   return settings['hidingFilters'] | ||||
| end | ||||
|  | ||||
| function setHidingFilters(state) | ||||
|   settings = {} | ||||
|   settings['hidingFilters'] = state | ||||
|   g_settings.mergeNode('BattleList', settings) | ||||
| end | ||||
|  | ||||
| function hideFilterPanel() | ||||
|   filterPanel.originalHeight = filterPanel:getHeight() | ||||
|   filterPanel:setHeight(0) | ||||
|   toggleFilterButton:getParent():setMarginTop(0) | ||||
|   toggleFilterButton:setImageClip(torect("0 0 21 12")) | ||||
|   setHidingFilters(true) | ||||
|   filterPanel:setVisible(false) | ||||
| end | ||||
|  | ||||
| function showFilterPanel() | ||||
|   toggleFilterButton:getParent():setMarginTop(5) | ||||
|   filterPanel:setHeight(filterPanel.originalHeight) | ||||
|   toggleFilterButton:setImageClip(torect("21 0 21 12")) | ||||
|   setHidingFilters(false) | ||||
|   filterPanel:setVisible(true) | ||||
| end | ||||
|  | ||||
| function toggleFilterPanel() | ||||
|   if filterPanel:isVisible() then | ||||
|     hideFilterPanel() | ||||
|   else | ||||
|     showFilterPanel() | ||||
|   end | ||||
| end | ||||
|  | ||||
| function onChangeSortType(comboBox, option, value) | ||||
|   setSortType(value:lower()) | ||||
| end | ||||
|  | ||||
| function onChangeSortOrder(comboBox, option, value) | ||||
|   -- Replace dot in option name | ||||
|   setSortOrder(value:lower():gsub('[.]', '')) | ||||
| end | ||||
|  | ||||
| -- functions | ||||
| function updateBattleList()  | ||||
|   removeEvent(updateEvent) | ||||
| 	updateEvent = scheduleEvent(updateBattleList, 100) | ||||
|   checkCreatures() | ||||
| end | ||||
|  | ||||
| function checkCreatures() | ||||
|   if not battlePanel or not g_game.isOnline() then | ||||
|     return | ||||
|   end | ||||
|  | ||||
|   local player = g_game.getLocalPlayer() | ||||
|   if not player then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   local dimension = modules.game_interface.getMapPanel():getVisibleDimension() | ||||
|   local spectators = g_map.getSpectatorsInRangeEx(player:getPosition(), false, math.floor(dimension.width / 2), math.floor(dimension.width / 2), math.floor(dimension.height / 2), math.floor(dimension.height / 2)) | ||||
|   local maxCreatures = battlePanel:getChildCount() | ||||
|    | ||||
|   local creatures = {} | ||||
|   local now = g_clock.millis() | ||||
|   local resetAgePoint = now - 250 | ||||
|   for _, creature in ipairs(spectators) do | ||||
|     if doCreatureFitFilters(creature) and #creatures < maxCreatures then | ||||
|       if not creature.lastSeen or creature.lastSeen < resetAgePoint then | ||||
|         creature.screenAge = now         | ||||
|       end       | ||||
|       creature.lastSeen = now | ||||
|       if not ages[creature:getId()] then | ||||
|         if ageNumber > 1000 then | ||||
|           ageNumber = 1 | ||||
|           ages = {} | ||||
|         end | ||||
|         ages[creature:getId()] = ageNumber | ||||
|         ageNumber = ageNumber + 1 | ||||
|       end | ||||
|       table.insert(creatures, creature)	 | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   updateSquare() | ||||
|   sortCreatures(creatures) | ||||
|   battlePanel:getLayout():disableUpdates() | ||||
|    | ||||
|   -- sorting | ||||
|   local ascOrder = isSortAsc() | ||||
|   for i=1,#creatures do   | ||||
| 	  local creature = creatures[i] | ||||
| 	  if ascOrder then | ||||
|       creature = creatures[#creatures - i + 1] | ||||
| 	  end | ||||
|     local battleButton = battleButtons[i]       | ||||
|     battleButton:creatureSetup(creature) | ||||
|     battleButton:show() | ||||
|     battleButton:setOn(true) | ||||
|   end | ||||
|    | ||||
|   if g_app.isMobile() and #creatures > 0 then | ||||
|     onBattleButtonHoverChange(battleButtons[1], true) | ||||
|   end | ||||
|      | ||||
|   for i=#creatures + 1,maxCreatures do | ||||
|     if battleButtons[i]:isHidden() then break end | ||||
|     battleButtons[i]:hide() | ||||
|     battleButton:setOn(false) | ||||
|   end | ||||
|  | ||||
|   battlePanel:getLayout():enableUpdates() | ||||
|   battlePanel:getLayout():update() | ||||
| end | ||||
|  | ||||
| function doCreatureFitFilters(creature) | ||||
|   if creature:isLocalPlayer() then | ||||
|     return false | ||||
|   end | ||||
|   if creature:getHealthPercent() <= 0 then | ||||
|     return false | ||||
|   end | ||||
|  | ||||
|   local pos = creature:getPosition() | ||||
|   if not pos then return false end | ||||
|  | ||||
|   local localPlayer = g_game.getLocalPlayer() | ||||
|   if pos.z ~= localPlayer:getPosition().z or not creature:canBeSeen() then return false end | ||||
|  | ||||
|   local hidePlayers = filterPanel.buttons.hidePlayers:isChecked() | ||||
|   local hideNPCs = filterPanel.buttons.hideNPCs:isChecked() | ||||
|   local hideMonsters = filterPanel.buttons.hideMonsters:isChecked() | ||||
|   local hideSkulls = filterPanel.buttons.hideSkulls:isChecked() | ||||
|   local hideParty = filterPanel.buttons.hideParty:isChecked() | ||||
|  | ||||
|   if hidePlayers and creature:isPlayer() then | ||||
|     return false | ||||
|   elseif hideNPCs and creature:isNpc() then | ||||
|     return false | ||||
|   elseif hideMonsters and creature:isMonster() then | ||||
|     return false | ||||
|   elseif hideSkulls and creature:isPlayer() and creature:getSkull() == SkullNone then | ||||
|     return false | ||||
|   elseif hideParty and creature:getShield() > ShieldWhiteBlue then | ||||
|     return false | ||||
|   end | ||||
|  | ||||
|   return true | ||||
| end | ||||
|  | ||||
| local function getDistanceBetween(p1, p2) | ||||
|     return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) | ||||
| end | ||||
|  | ||||
| function sortCreatures(creatures) | ||||
|   local player = g_game.getLocalPlayer() | ||||
|    | ||||
|   if getSortType() == 'distance' then | ||||
|     local playerPos = player:getPosition() | ||||
|     table.sort(creatures, function(a, b)  | ||||
|       if getDistanceBetween(playerPos, a:getPosition()) == getDistanceBetween(playerPos, b:getPosition()) then | ||||
|         return ages[a:getId()] > ages[b:getId()] | ||||
|       end | ||||
|       return getDistanceBetween(playerPos, a:getPosition()) > getDistanceBetween(playerPos, b:getPosition())  | ||||
|     end) | ||||
|   elseif getSortType() == 'health' then | ||||
|     table.sort(creatures, function(a, b)  | ||||
|       if a:getHealthPercent() == b:getHealthPercent() then | ||||
|         return ages[a:getId()] > ages[b:getId()] | ||||
|       end | ||||
|       return a:getHealthPercent() > b:getHealthPercent()  | ||||
|     end) | ||||
|   elseif getSortType() == 'age' then | ||||
|     table.sort(creatures, function(a, b) return ages[a:getId()] > ages[b:getId()] end) | ||||
|   elseif getSortType() == 'screenage' then | ||||
|     table.sort(creatures, function(a, b) return a.screenAge > b.screenAge end) | ||||
|   else -- name | ||||
|     table.sort(creatures, function(a, b) | ||||
|       if a:getName():lower() == b:getName():lower() then | ||||
|         return ages[a:getId()] > ages[b:getId()] | ||||
|       end | ||||
|       return a:getName():lower() > b:getName():lower()  | ||||
|     end) | ||||
|   end | ||||
| end | ||||
|  | ||||
| -- other functions | ||||
| function onBattleButtonMouseRelease(self, mousePosition, mouseButton) | ||||
|   if mouseWidget.cancelNextRelease then | ||||
|     mouseWidget.cancelNextRelease = false | ||||
|     return false | ||||
|   end | ||||
|   if not self.creature then | ||||
|     return false | ||||
|   end | ||||
|   if ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton) | ||||
|     or (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then | ||||
|     mouseWidget.cancelNextRelease = true | ||||
|     g_game.look(self.creature, true) | ||||
|     return true | ||||
|   elseif mouseButton == MouseLeftButton and g_keyboard.isShiftPressed() then | ||||
|     g_game.look(self.creature, true) | ||||
|     return true | ||||
|   elseif mouseButton == MouseRightButton and not g_mouse.isPressed(MouseLeftButton) then | ||||
|     modules.game_interface.createThingMenu(mousePosition, nil, nil, self.creature) | ||||
|     return true | ||||
|   elseif mouseButton == MouseLeftButton and not g_mouse.isPressed(MouseRightButton) then | ||||
|     if self.isTarget then | ||||
|       g_game.cancelAttack() | ||||
|     else | ||||
|       g_game.attack(self.creature) | ||||
|     end | ||||
|     return true | ||||
|   end | ||||
|   return false | ||||
| end | ||||
|  | ||||
| function onBattleButtonHoverChange(battleButton, hovered) | ||||
|   if not hovered then | ||||
|     newHoveredCreature = nil     | ||||
|   else | ||||
|     newHoveredCreature = battleButton.creature | ||||
|   end | ||||
|   if battleButton.isHovered ~= hovered then | ||||
|     battleButton.isHovered = hovered | ||||
|     battleButton:update() | ||||
|   end | ||||
|   updateSquare() | ||||
| end | ||||
|  | ||||
| function onPlayerPositionChange(creature, newPos, oldPos) | ||||
|   addEvent(checkCreatures) | ||||
| end | ||||
|  | ||||
| local CreatureButtonColors = { | ||||
|   onIdle = {notHovered = '#888888', hovered = '#FFFFFF' }, | ||||
|   onTargeted = {notHovered = '#FF0000', hovered = '#FF8888' }, | ||||
|   onFollowed = {notHovered = '#00FF00', hovered = '#88FF88' } | ||||
| } | ||||
|  | ||||
| function updateSquare() | ||||
|   local following = g_game.getFollowingCreature() | ||||
|   local attacking = g_game.getAttackingCreature() | ||||
|      | ||||
|   if newHoveredCreature == nil then | ||||
|     if hoveredCreature ~= nil then | ||||
|       hoveredCreature:hideStaticSquare() | ||||
|       hoveredCreature = nil | ||||
|     end | ||||
|   else | ||||
|     if hoveredCreature ~= nil then | ||||
|       hoveredCreature:hideStaticSquare() | ||||
|     end | ||||
|     hoveredCreature = newHoveredCreature | ||||
|     hoveredCreature:showStaticSquare(CreatureButtonColors.onIdle.hovered) | ||||
|   end | ||||
|    | ||||
|   local color = CreatureButtonColors.onIdle | ||||
|   local creature = nil | ||||
|   if attacking then | ||||
|     color = CreatureButtonColors.onTargeted | ||||
|     creature = attacking | ||||
|   elseif following then | ||||
|     color = CreatureButtonColors.onFollowed | ||||
|     creature = following | ||||
|   end | ||||
|  | ||||
|   if prevCreature ~= creature then | ||||
|     if prevCreature ~= nil then | ||||
|       prevCreature:hideStaticSquare() | ||||
|     end | ||||
|     prevCreature = creature | ||||
|   end | ||||
|    | ||||
|   if not creature then | ||||
|     return | ||||
|   end | ||||
|    | ||||
|   color = creature == hoveredCreature and color.hovered or color.notHovered | ||||
|   creature:showStaticSquare(color) | ||||
| end | ||||
							
								
								
									
										9
									
								
								SabrehavenOTClient/modules/game_battle/battle.otmod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								SabrehavenOTClient/modules/game_battle/battle.otmod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| Module | ||||
|   name: game_battle | ||||
|   description: Manage battle window (new) | ||||
|   author: otclient@otclient.ovh | ||||
|   website: otclient.ovh | ||||
|   sandboxed: true | ||||
|   scripts: [ battle ] | ||||
|   @onLoad: init() | ||||
|   @onUnload: terminate() | ||||
							
								
								
									
										155
									
								
								SabrehavenOTClient/modules/game_battle/battle.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								SabrehavenOTClient/modules/game_battle/battle.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| BattleIcon < UICheckBox | ||||
|   size: 20 20 | ||||
|   image-color: white | ||||
|   image-rect: 0 0 20 20 | ||||
|  | ||||
|   $hover !disabled: | ||||
|     color: #cccccc | ||||
|  | ||||
|   $!checked: | ||||
|     image-clip: 0 0 20 20 | ||||
|  | ||||
|   $hover !checked: | ||||
|     image-clip: 0 40 20 20 | ||||
|  | ||||
|   $checked: | ||||
|     image-clip: 0 20 20 20 | ||||
|  | ||||
|   $hover checked: | ||||
|     image-clip: 0 60 20 20 | ||||
|  | ||||
|   $disabled: | ||||
|     image-color: #ffffff88 | ||||
|  | ||||
| BattlePlayers < BattleIcon | ||||
|   image-source: /images/game/battle/battle_players | ||||
|  | ||||
| BattleNPCs < BattleIcon | ||||
|   image-source: /images/game/battle/battle_npcs | ||||
|  | ||||
| BattleMonsters < BattleIcon | ||||
|   image-source: /images/game/battle/battle_monsters | ||||
|  | ||||
| BattleSkulls < BattleIcon | ||||
|   image-source: /images/game/battle/battle_skulls | ||||
|  | ||||
| BattleParty < BattleIcon | ||||
|   image-source: /images/game/battle/battle_party | ||||
|  | ||||
| MiniWindow | ||||
|   id: battleWindow | ||||
|   !text: tr('Battle') | ||||
|   height: 166 | ||||
|   icon: /images/topbuttons/battle | ||||
|   @onClose: modules.game_battle.onMiniWindowClose() | ||||
|   &save: true | ||||
|   &autoOpen: false | ||||
|  | ||||
|   Panel | ||||
|     id: filterPanel | ||||
|     margin-top: 26 | ||||
|     anchors.top: parent.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: miniwindowScrollBar.left | ||||
|     height: 45 | ||||
|  | ||||
|     Panel | ||||
|       id: buttons | ||||
|       anchors.top: parent.top | ||||
|       anchors.horizontalCenter: parent.horizontalCenter | ||||
|       height: 20 | ||||
|       width: 120 | ||||
|       layout: | ||||
|         type: horizontalBox | ||||
|         spacing: 5 | ||||
|  | ||||
|       BattlePlayers | ||||
|         id: hidePlayers | ||||
|         !tooltip: tr('Hide players') | ||||
|         @onSetup: if g_app.isMobile() then self:setChecked(true) end | ||||
|         @onCheckChange: modules.game_battle.checkCreatures() | ||||
|            | ||||
|       BattleNPCs | ||||
|         id: hideNPCs | ||||
|         !tooltip: tr('Hide Npcs') | ||||
|         @onSetup: if g_app.isMobile() then self:setChecked(true) end | ||||
|         @onCheckChange: modules.game_battle.checkCreatures() | ||||
|  | ||||
|       BattleMonsters | ||||
|         id: hideMonsters | ||||
|         !tooltip: tr('Hide monsters') | ||||
|         @onCheckChange: modules.game_battle.checkCreatures() | ||||
|  | ||||
|       BattleSkulls | ||||
|         id: hideSkulls | ||||
|         !tooltip: tr('Hide non-skull players') | ||||
|         @onCheckChange: modules.game_battle.checkCreatures() | ||||
|  | ||||
|       BattleParty | ||||
|         id: hideParty | ||||
|         !tooltip: tr('Hide party members') | ||||
|         @onSetup: if g_app.isMobile() then self:setChecked(true) end | ||||
|         @onCheckChange: modules.game_battle.checkCreatures() | ||||
|  | ||||
|     Panel | ||||
|       id: sortPanel | ||||
|       anchors.top: prev.bottom | ||||
|       anchors.left: parent.left | ||||
|       anchors.right: parent.right | ||||
|       height: 20 | ||||
|       margin-top: 6 | ||||
|  | ||||
|       ComboBox | ||||
|         id: sortTypeBox | ||||
|         width: 90 | ||||
|         anchors.top: parent.top | ||||
|         anchors.left: prev.right | ||||
|         anchors.horizontalCenter: parent.horizontalCenter | ||||
|         margin-left: -31 | ||||
|  | ||||
|       ComboBox | ||||
|         id: sortOrderBox | ||||
|         width: 60 | ||||
|         anchors.top: parent.top | ||||
|         anchors.left: prev.right | ||||
|         margin-left: 4 | ||||
|  | ||||
|   Panel | ||||
|     height: 18 | ||||
|     anchors.top: prev.bottom | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: miniwindowScrollBar.left | ||||
|     margin-top: 4 | ||||
|  | ||||
|     UIWidget | ||||
|       id: toggleFilterButton | ||||
|       anchors.top: prev.top | ||||
|       width: 21 | ||||
|       anchors.horizontalCenter: parent.horizontalCenter | ||||
|       image-source: /images/ui/arrow_vertical | ||||
|       image-rect: 0 0 21 12 | ||||
|       image-clip: 21 0 21 12 | ||||
|       @onClick: modules.game_battle.toggleFilterPanel() | ||||
|       phantom: false | ||||
|  | ||||
|   HorizontalSeparator | ||||
|     anchors.top: prev.top | ||||
|     anchors.left: parent.left | ||||
|     anchors.right: miniwindowScrollBar.left | ||||
|     margin-right: 1 | ||||
|     margin-top: 11 | ||||
|  | ||||
|   MiniWindowContents | ||||
|     anchors.top: prev.bottom | ||||
|     margin-top: 6 | ||||
|  | ||||
|     Panel | ||||
|       id: battlePanel | ||||
|       anchors.left: parent.left | ||||
|       anchors.right: parent.right | ||||
|       anchors.top: parent.top | ||||
|       margin-top: 5 | ||||
|       padding-right: 5 | ||||
|       layout: | ||||
|         type: verticalBox | ||||
|         fit-children: true | ||||
							
								
								
									
										2
									
								
								SabrehavenOTClient/modules/game_battle/battlebutton.otui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								SabrehavenOTClient/modules/game_battle/battlebutton.otui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| BattleButton < CreatureButton | ||||
|   &isBattleButton: true | ||||
							
								
								
									
										36
									
								
								SabrehavenOTClient/modules/game_bugreport/bugreport.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								SabrehavenOTClient/modules/game_bugreport/bugreport.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| -- TODO: find another hotkey for this. Ctrl+Z will be reserved to undo on textedits. | ||||
| HOTKEY = 'Ctrl+Z' | ||||
|  | ||||
| bugReportWindow = nil | ||||
| bugTextEdit = nil | ||||
|  | ||||
| function init() | ||||
|   g_ui.importStyle('bugreport') | ||||
|  | ||||
|   bugReportWindow = g_ui.createWidget('BugReportWindow', rootWidget) | ||||
|   bugReportWindow:hide() | ||||
|  | ||||
|   bugTextEdit = bugReportWindow:getChildById('bugTextEdit') | ||||
|  | ||||
|   g_keyboard.bindKeyDown(HOTKEY, show, modules.game_interface.getRootPanel()) | ||||
| end | ||||
|  | ||||
| function terminate() | ||||
|   g_keyboard.unbindKeyDown(HOTKEY, modules.game_interface.getRootPanel()) | ||||
|   bugReportWindow:destroy() | ||||
| end | ||||
|  | ||||
| function doReport() | ||||
|   g_game.reportBug(bugTextEdit:getText()) | ||||
|   bugReportWindow:hide() | ||||
|   modules.game_textmessage.displayGameMessage(tr('Bug report sent.')) | ||||
| end | ||||
|  | ||||
| function show() | ||||
|   if g_game.isOnline() then | ||||
|     bugTextEdit:setText('') | ||||
|     bugReportWindow:show() | ||||
|     bugReportWindow:raise() | ||||
|     bugReportWindow:focus() | ||||
|   end | ||||
| end | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 ErikasKontenis
					ErikasKontenis