The purpose of this tutorial Once every while I see people ask questions about UI modding that have very clear answers, that should be common knowledge with most PA modders. However we are missing a full blown tutorial that states all the "obvious" things that really help when modding PA. This tutorial aims to explain them to you. Part A:gives you an overview of the PA files and how typical UI parts in PA work Part B: how to setup a typical UI mod based on a full example, so give you an overview of a typical workflow that I myself follow, as well as what tools I use and recommend This tutorial will not: be a javascript or webdevelopment tutorial. For that there are other more general things. I will assume you know these things or that you can learn them yourself. If you already have some vague ideas of webdevelopment, I highly recommend the knockout js tutorials here: http://learn.knockoutjs.com/ They were my starting point when I started and I think they give you a very good overview of how knockout in general works. be the absolute way to do things. There are a lot of ways to do what a lot of things in PA UI modding, it is very flexible. So if you spot better alternatives please share them. focus on version specific things. Everything I say here tries to be about stuff that has not changed in a long time and probably never will change.+ be a tutorial on how to use git(hub) Basic Tools I am using Notepad++ for most editing. I also use eclipse to manage projects, but notepad++ is the minimum Git. I use tortoisegit for windows and egit for eclipse. It is highly recommended to make your PA directory a git repository. That way you can easily spot any changes Uber makes in patches A script to reformat json files in PA. By default they are rather unreadable. https://forums.uberent.com/threads/reformat-your-json-files.47529/page-2#post-1077113 When combined with having PA as a git repository, it is best to commit the formatted version, that way you can easily see the changes in json files as well.
Part A How the UI works You probably know already that PA uses coherent UI http://coherent-labs.com/ for the UI. This means PA uses a webkit based webbrowser. So a UI mod is essentially a bunch of javascript that is aimed to modify the existing UI at runtime. If you find something fancy that works in chrome or other browsers that are based on webkit (or whatever fork they use by now) it will very likely work in PAs UI as well. An Overview of the the PA UI. So let's have a look at how the UI html and more importantly javascript works. In your PA directory under stable/media/ui/main/ most of the UI files reside. The UI in PA is based on "scenes". A scene basically is a webpage. The mainmenu is a webpage. The settings is a webpage. The uberbar is a webpage. The main game interface is created from 17 or so webpages. So what you see on the screen of PA can be the result of merging multiple scenes together. Merging means the graphical output of the scenes is merged into one picture and the input is dispatched to the scenes. A scene basically is a browsertab. Every scene is creating a process in your taskmanager, because that is how webkit manages browser tabs: As independent processes. Communication between scenes as well as with PA itself happens mainly over a javascript object called "engine". The normal scenes already have a setup with it, so there every scene can send messages to it and can receive messages back from it. In some cases scenes talk with each other, this happens via an api that basically sends a message to pa.exe and then pa.exe sends it to the other scene. But I won't go more into detail here yet. Let's have a look at how a typical scene is build up. For this we will open stable/media/ui/main/game/start. This is the directory of the start scene. Every scene has such a directory. Most are on the same level as this directory. A typical scene has one html file, one js file and one css file, all named after the scene. As you can see the start scene has a few more things: An img folder is pretty common and contains some images this scene may use. starfield.js is an example of an old file that probably isn't used anymore and just happens to be there. Maybe it isnt there for newer installs of PA, dunno. ytv.css and ytv.js are probably libraries for something youtube related. It is unusual that libraries are directly part of the scene. We'll see soon where they are located usually. So the scene is a webpage, contained in start.html that then fills itself with life by using start.js. If you open start.html you will see a typical webpage with some knockout things in it. In the head you will also notice that it includes a bunch of files from other locations. I will speak about those soon. For now let's notice that all these paths start with coui:// that is the main path that coherent uses as a "root" of the files that you can read from the UI. coui:// means <PA directory>/stable/media. So any file in that directory and its children can be read by the UI. Ofc since the UI is a webbrowser it can also talk with webservers via webpages, you can easily include a file from a webpage just by using the normal http:// path instead of a coui:// path. Now let's have a look at start.js. There are a few important things to notice here. First you notice that the file mainly registers a function to $(document).ready() so it will init all that UI stuff once the page has loaded. All PA scenes usually have a "model" object, for whatever reason it seems somebody forgot to add var model; at the start of this file and since js is js it still works. In most other files you will see var model; at the start of the file and later on an instance of the Knockout ViewModel will be assigned to that variable. The model object is a very important one, we will see more about it later. If you look down just a little you will find a model called LoginViewModel That is the definition of the model object. It defines all kinds of observables and functions that are bound to the UI via knockout. So for many of the things that are defined as self.xyz = ... you will find the name xyz in an knockout binding in the html file if you look for it. Let's press ctrl+f and search for loadMods. You will arrive close to the end of the file, it should look like this: This is the place where all mods that affect the scene "start" are loaded. Mods work by loading js or css files into scenes, based on this piece of code that you can find in all scenes. This is the optimal location to load modifications, because: knockout bind has not yet been called. If you modify something on model, for example replace an observable, you will be able to make it bind to your observable instead. the handlers have not yet been registered with the engine. This happens in the line app.registerWithCoherent(model, handlers); that code registers "handlers" with the engine. "handlers" are functions that are called by native code to push information into the UI. So for example the fact that the game ended will trigger a game state handler in the live_game js file. There are a lot of things that are done via these handlers. Search up a bit to find what handlers the start.js file has. Since the handlers are only registered after your mod is loaded you can hook into them easily. To take advantage of this it is important that all your code is directly executed when your mod js file is loaded, PA guarantees that will be after document ready. We will see in the example later how a template mod file should look like. So let's have a wider overview of how PAs UI files are structured. Most of the UI files are under stable/media/ui. However you can also access for example unit definitions with $.getJSON to paths in pa/units if you need them. In /stable/media/ui you will find a directory main and a directory mods. That mods directory should not be used anymore, however it still contains a readme that explains what is at the core of the mod loading system, we will cover it later. There is also a file boot.json here. This file contains a list of all css and js files that are loaded in all scenes. Internally this works by loaded all these files into a single big text block and injecting it into the page. Afaik this big text string is only created at startup of PA.exe, so if you modify libraries or you modify the files listed here or boot.json itself you need to restart PA to see the changes applied. The list of js files already gives a good idea of where to find what library files. They are all under /ui/main/shared in media. Here you can find shared files for many scenes. The most interesting ones are in the js directory. What is defined here is available as api.<something> in most scenes. If you want to have a look at how mod loading works exactly, open common.js, it contains the functions to load scripts, html and css files, they all basically work by directly injecting the text of the file into the scene at runtime. This means the scripts are executed sequentially.
Debugging the UI (or your mods) To do quick tests of how the default UI behaves, or how your mods behave, you can use (and absolutely SHOULD) use the UI debugger. This is basically the debugger you can also get in chrome for any webpage by pressing ctrl+shift+i. In PA that debugger is a separate program. The debugger is located at \stable\Coherent\Debugger\debugger.exe To use it start PA and then clock Go in the debugger. If your PA is in the main menu you will see now this list: These are the scenes currently active in PA. As you can see there isn't just the "Start Page", but also some other scenes. I won't go into detail on them, you can look into them yourself if you are curious. So let's click the link to "Start Page" and now you will see this: This is a view of the html currently active in the scene. You can change it from here by right clicking it and editing it as html. You probably also notice that when you move your mouse over the html in the debugger your PA shows blue markers where that piece of html is. You can also go the other direction. To find a piece of html from a UI element you can click the small magnifing glass in the bottom left. Now click a UI part and the html will be highlighted. If you cant seem to "click" it you probably are looking at the wrong scene. For example the uberbar is not clickable when you are in the start-scene. The debugger has a few more options, I marked the ones I deem important. Resources. Here you can see all kinds of resources that the page has loaded. Most interesting are the local storage and the session storage. The local storage contains your PA settings and it is a very important place to store "smaller" data pieces. Google "localStorage javascript" to find more into on it, it is a concept used by all modern webbrowsers. Same goes for the session storage, which isnt as useful, as it forgets all data the moment the specific scenes is destroyed. I don't think anybody uses cookies or whatever the application cache is in PA. The indexedDB is used to store bigger data. For example the minimap mod I make stores the megabyte-sized mapping things here. If you want to store bigger data pieces then have a look at the library file /stable/media/ui/shared/js/db_utility.js This is is some small js file that PA provides you to easily access the indexeddb Network. Here you can see all requests that the scene has made to any file, local or remote. This list only is populated from the moment you start the debugger and connect to the scene. Sources. This... umm it exists. Looks like it has a debugger. Dunno, never used it, I debug the UI by using console.log Timeline. Same, though I have no idea at all what it actually is good for Profiles. Some profiler stuff. Dont think it is very important to PA Audits. Same as profiler Console. A very important point. This gives a live interface to the current js environment of this scene. You can see all kinds of logging stuff here. There may be some red lines. Red lines are errors. They often are bad, but some errors are okay to have. Typical errors that nobody cares about are: "object blabla has no methode _trigger", "Failed to load resources chrome-extension://..." or errors that tell you something about json language files, but I think those are not very common anymore. In this console window many experiments and ideas start. Here you can find the model object again. Type model. and it will show you all things the model has. You can even run code here. For example you can run model.buildVersion() and it will show you the value of the buildVersion observable. Or you can run model.navToServerBrowser(); and PA will do the exact same thing as when you click the button for custom games. Notice that the debugger cleared the log. This happens because the server browser is another scene and the "tab" that displayed the start scene now basically loaded a new webpage, which is the server browser. The default behavior of the debugger is too clear the log when the scene is switched. Sometimes this isn't very helpful, so you can change this option in the debugger, click the gear in the bottom right for this. The option is called "Console > Preserve log upon navigation" The debugger really is at the core of getting an understanding of what is going on with the UI in PA, so make sure to play around with it a lot.
Part B: Creation of a simple UI mod by example In this part we will go through the steps to create a simple mod that shows the worth of your current army and structure as you play. The idea is to show the value somewhere in the economy bar of the main UI of the game while you play. There is no "easy" way to get this value. However you can get all alerts for "unit created" and "unit lost", so you can conclude what units you have from that. Getting alerts isn't all that easy, as there is no easy system to get them without conflicting with other mods, like PA Stats, that use them. For this reason I wrote this file, that contains my "alertsManager": http://pastats.com/mod/live/lib/alertsManager.js Additionally this alertsManager depends on another file, that is used to easier parse unit information: http://pastats.com/mod/live/lib/unitInfoParser.js We'll use these files soon and we will use these very links, without downloading the files, as an example of how mods can be loaded from _anywhere_ you want. Even remote servers. For now let's setup the mod directory. PA keeps mods in "%LOCALAPPDATA%\Uber Entertainment\Planetary Annihilation\client_mods" (or \mods on older installs) I however would recommend to keep your mods somewhere else while you develop them. For this purpose I use junctions, which can be created with this shell extension. So I create a new folder called ShowArmyMetal somewhere, where I usually have my projects when I develop. To make PA recognize that mod, you right click the folder, select "select as link source" (or similar, mine is in german, whatever you have in your version) and then you right click in your mods directory and select drop as > junction. Now inside your mods directory you should see the link to the file. (This btw is also really helpful to move big files off your ssd ) Inside this folder we now first create a file called modinfo.json. This is the file that tells pamm how to handle the mod. Create the file with this content and fill in the parts that are in <> Code: { "context": "client", "identifier": "<an identifier for the mods. Use a domain backwards for this. So I would use: info.nanodesu.showarmymetal. Use whatever domain you like. Many use com.pa.mods or the like>", "display_name": "Show Army Metal", "description": "Adds a simple display of the worth of your current army in metal", "author": "<your name>", "version": "1.0", "build": "<the PA build you develop on>", "date": "<careful: US time format, so the 3 in the middle is the month. So for today I would enter 2015/3/17>", "signature": "not yet implemented", "id": "uimod-showarmymetal", "priority": 100, "forum": "<here you will need to place a link to the thread in the forums you create for your mod, you can leave this empty>", "scenes": { }, "category": [ "in-game", "ui" ], "enabled": true } I think most of the properties are self explaining. What we need to look at now is the scenes property. First lets create a few folders in our mod: ui/mods/showarmymetal/lib The mods are mounted into a virtual file system together with the default files. So PA will think your ui/mods/showarmymetal stuff is a file that is directly whereever your PA is as well. You can in fact replace PA files by using the exact same path, however in UI modding that is _very_ bad style and if you think you need to do it, rethink it and ask in the forums, only in very few cases it was required so far. It has major drawbacks in that patches will break your mod very very easily and only one mod at a time can shadow a file. So try to follow this kind of naming scheme for your mod folders: ui/mods/<your mod directory>/<your files and whatever you want in whatever form you need> to prevent all kinds of possible conflicts Now we will first add the library files I mentioned earlier to be loaded. We will mod the live_game_econ scene. This is a scene that show the economy in the live_game scene. As I mentioned earlier, live_game is split in a lot of subscenes. You'll notice that /ui/main/game/live_game has a lot of files in it that are all named after the typical scene-name pattern. We are interested into the files with the namme live_game_econ.js and html for our mod. For now to configure our modinfo.json we just need to know that the scene is called live_game_econ and loads mods for that name. You can verify that by opening live_game_econ.js and looking for the loadMods bit. You'll find this code: loadMods(scene_mod_list['live_game_econ']); This is where we want to load our own js files, starting with the library files. Add the following into your scenes of the modinfo.json: Code: "live_game_econ": [ "http://pastats.com/mod/live/lib/unitInfoParser.js", "http://pastats.com/mod/live/lib/alertsManager.js" ] This will tell pamm to make it so that PA loads these files in this scene. Notice how you can just directly put a link to a remote file there. You can use this to make mods that are "automatically" updated whenever you as the author want it. PA Stats works that way for example. However also realize that using this means that the mod won't work offline. If you want a mod for offline play you will need include the files and load them from a coui:// path, just like the js file of our mod itself, that we will soon add. If you want to know how pamm does that read the readme.txt in stable/media/ui/mods, that was the file given to us by Uber that explained how to make pa loads mods before we had pamm. You'll realize how awesome it is that we have pamm So before we continue let's test if we did everything right so far. For this we will now open pamm. If our modinfo.json is correct it should show the mod. yey it worked Enable the mod. If you can't find your mod, first check you created the link correctly in your mods directory. If you open the link in there you should end up in the place where the modinfo.json is placed. Next check the validity of the modinfo.json. For this I recommend this page: http://jsonformatter.curiousconcept.com/ Copy paste all your json data in there and let it verify and fix whatever mistakes show up. I personally love to add , where they do not belong So now let's check if PA recognizes our mod as well. For this we'll start PA (or if you have it started already you don't need to restart it. Changes to what files are part of a typical mod are accepted without restarting PA). Start a game vs the AI with the AI economy multiplier set to 0. When the game starts disable PA Stats to prevent it from sending, if you have PA Stats. (You should have it ) So far our mod doesn't do much, apart from adding the alertsManager to the live_econ scene. So if everything worked out correctly you should be able to open the debugger, go to the "Live Game: Economy Bar" scene and in the console alertsManager. should show suggestions. alertsManager also prints "load alertsManager" into the console somewhere at the top. To test that alertsManager works run this code in the console and build something: Code: alertsManager.addListener(function(l){console.log(l);}) This should allow you to inspect the objects passed into the listener function when alerts trigger: Good. Now we just need one more js file where we can place our actual code for the stuff we want to do. Create the file showarmymetal.js and showarmymetal.css in ShowArmyMetal/ui/mods/showarmymetal/ and make your scenes definition look like this: Code: "scenes": { "live_game_econ": [ "http://pastats.com/mod/live/lib/unitInfoParser.js", "http://pastats.com/mod/live/lib/alertsManager.js", "coui://ui/mods/showarmymetal/showarmymetal.js", "coui://ui/mods/showarmymetal/showarmymetal.css" ] }, Now restart pamm and disable/enable the mod again. If the mod suddenly is gone, you probably have invalid json, use the validator to find it. The css file for now will stay empty, you can omit it in cases where you do not add any css classes for your own html, this mod will later contain one class, so it is needed. Open showarmymetal.js in your favorite editor and put this basic template into it: Code: (function() { console.log("load showarmymetal.js"); // here is where your code belongs. }()); Now let's try if this file is loaded correctly with the PA you still have running. In the debugger of the live_game_econ scene press F5. You will see the economy bar hide for a short while. After this you'll very likely get an error message in the log that the file could not be loaded. You need to restart PA for it to mount new files. If you add a link to an outside file or you add a link to a file that existed when PA started it should work without restart. If in doubt restart Once you restarted PA and joined another vs AI (at 0 economy) game you should see the log statement in the console of the economy bar scene.
Now we're ready for the main thing: Write our mod code into the console. The basic outline is this: create a map with unit_spec to the metal value of the unit track all alerts on every "unit created" alert look up the value of the unit and add it to our counter on every "unit destroyed" alert look up the value of the unit and subtract it from our counter Add a simple display of our counter somewhere in the live_game_econ html That's it. Here is the code of the mod with a lot of comments that explain what is going on. https://gist.github.com/ColaColin/a1067f0f7c29ef11f515 Put this code into the showarmymetal.js file The css class added looks like this and belongs into the css file you created: Code: .army_value_text { margin: 0px 0px -4px 0px; text-transform: uppercase; color: #FFF; font-family: 'Sansation Bold'; font-size: 10px; text-shadow: 0px 0px 20px #00B3FF; } Put this into the css file and then and reload the scene. Now try to place some buildings and delete them to see the mod in action. Last words I think that is important to notice is that the typical way to hook functions looks like this: Given for example the handler handlers.display_mode if you want to hook into it, you should do this in your mod: Code: var oldDisplayModeHandler = handlers.display_mode; handlers.display_mode = function(payload) { myExtraCode(payload) // your extra code here, for example print something to the console return oldDisplayModeHandler(payload); // call the default PA code. Notice return isnt strictly required for this function, but it does not hurt to include it. }; This is the normal way to hook into a function that won't damage PA and won't conflict with other mods. You can use this pattern to "change" the behavior of any function that PA has by default. So if you want to do something when some function is called, or you want the info some function is passed when it is called, use this. So I hope this helps to get into modding the UI. It's really surprisingly simple and has a very nice workflow where you do a lot of quick trial and error loops. This tutorial only covered the basics. A few links to mods that can serve as example of some more complex topics: AlertsMiniMap have a look at the injectpanel file and the injected stuff, it shows how to create your own scenes within live_game. This can be helpful when you want to add "heavy" UI elements that like to eat CPU ... actually that is the only one I can come up with right now. If anybody knows other mods that can serve as a good example of specific things please tell me. If there are any more questions or any mistakes or whatever post here. Attached is the complete example mod.
So, I want to mod build bar for a server mod and I kinda have zero expirience with web. There are some simple things that I don't understand. I went through http://learn.knockoutjs.com/ first tutorial and I get what observable is... but there are multiple variables called visible in both live_game_build_bar.js and .html, how are they associated?
It depends on the context. I have not looked at the build bar files, but a simple example might clear things up: Code: var model = { showList: ko.observable(), a: ko.observable("referenced via $parent"), b: ko.observable("referenced via global model"), elements: [{ elementVisible: ko.observable() }, { elementVisible: ko.observable() }, { elementVisible: ko.observable() }]; }; ko.applyBindings to this html with this body: <!-- showList in the model determines if the whole list is visible at all --> <div data-bind="visible: showList"> <div data-bind="foreach: elements"> <!-- inside a foreach the context switches to the child element that is currently being processed --> <!-- so this now references properties of the objectrs in the elements array --> <div data-bind="visible: elementVisible">I am a visible element</div> <!-- some ways to break free of this behavior --> <div data-bind="text: $parent.a">This text will be replaced by the parents a</div> <div data-bind="text: model.b">This text will be bound to b on the global variabl model</div> </div> </div> see this for more info on the binding context: http://knockoutjs.com/documentation/binding-context.html
Code: handlers.unit_specs = function (payload) { delete payload.message_type; model.unitSpecs.resolve(payload); }; If I understand correcly this function is called by something, but it is not mentioned anywhere else. When is it called then? There is also this: Code: function BuildBarViewModel() { ... self.unitSpecs = $.Deferred(); ... self.unitSpecs.then(function(payload) { ... function addBuildInfo(unit, id) { unit.buildIcon = 'img/build_bar/units/' + getBaseFileName(unit) + '.png' var strip = /.*\.json/.exec(id); if (strip) id = strip.pop(); var target = self.buildHotkeyModel.SpecIdToGridMap()[id]; if (!target) { target = ['misc', misc_unit_count]; misc_unit_count++; } unit.buildGroup = target[0]; unit.buildIndex = target[1]; }; for (var id in payload) { addBuildInfo(payload[id], id); } ... }); ... } model = new BuildBarViewModel(); Code takes data from build.js and puts it into unit.buildGroup and unit.buildIndex with unit specs being passed through payload... But where is 'unit' class defined?
handlers.xxx are usually called by coherent. They are the way coherent pushes data to the javascript layer, somewhere after all handlers are declared they are passed to the coherent lib. the 2nd codeblock calls the function addBuildInfo on every object in the payload that is passed for the unitspecs, once it is available. There is no unit "class". All that can be deduced from this code is that payload[id] is some object that has the properties that are used in addBuildInfo. There are no classes as you know them from languages like c++ or java in javascript. You could pass anything into addBuildInfo, as long as the objects have the properties required it will work. If the objects do not have the properties they will be "undefined" when accessed, which may or may not cause obvious errors. To get an understand of what kind of unit objects are normally processed by that code add a console.log(unit) into the function and see what it prints. EDIT: oh I just realize the units object properties are nott actually read, they are written. So that function takes any sort of object as first parameter and adds a few additional parameters to them (or overwrites them, though based on the name they are added). You can add any sort of property to any sort of object anywhere in js. In JS nothing is certain basically. You just have objects and you can modify them one by one however you like. In a way any object is its own class in javascript, inheritance is based on prototype-objects. Basically one object inherits from another object. A concept I never used tbh, but you can read more about it on google if you want to. I never really needed it so far.
So, I made it so that build bar doesn't have tabs and it hide empty raw and columns, but there is one more thing that I can't figure out, that is how to make max-width to depend on screen size, so that build bar doesn't get too wide. Bottom two: getting way too wide with many items. Middle two: eating items on the left, shrinks when there is no need. (margin set for child element of element with overflow) Top two: slants to the left, shrink when there is no need. (max-width set for parent element of overflow element).
The standard CSS solution would be width media queries. However panels get a little weird because they often get resized to their content, so the panel won't know the real screen size. You might have to communicate the screen size from the main window (Sandbox Unit Organizer recently did something similar for small displays), and then data-bind a calculated width and/or offset.
Thanks, I looked at your code and copied some lines and now it works! Is there a way to make build bar scroll with scroll wheel. There is some code in live_game_build_bar.js that supposedly prevents it from scrolling, but removing it doesn't work. I also tried to set focus on element, that doesn't help either. (I really don't know what I'm doing, lol.)
I'm assuming horizontal scrolling, as in your earlier screenshots. Browser default behavior only does vertical (unless you have a trackpad or something) Most of the early search hits point at this jQuery plugin: https://github.com/jquery/jquery-mousewheel
Well, I managed to make it so that it scrolls on click inside the element, but wheel just doesn't work Code: self.setup = function () { $(window).focus(function() { self.active(true); }); $(window).blur(function () { self.active(false); }); var table_scroll = document.getElementById("the_table_scroll"); function tableScroll (e) { table_scroll.scrollLeft += 10; console.log(table_scroll.scrollLeft); } if (table_scroll.addEventListener) { table_scroll.addEventListener('click', tableScroll, false); table_scroll.addEventListener('wheel', tableScroll, false); table_scroll.addEventListener('mousewheel', tableScroll, false); table_scroll.addEventListener('DOMMouseScroll', tableScroll, false); table_scroll.addEventListener('MozMousePixelScroll', tableScroll, false); } window.onmousewheel = document.onmousewheel = table_scroll.onmousewheel = tableScroll; };
Tweaking with ignoreMouse/receiveMouse didn't seem to help. I can get it to to work on document or window of the live_game scene. Technically this is the game panel inside of the main scene so there may be a way to pass it down further, but I haven't made an exhaustive study. The event includes which element it was over, so you could bind it on live_game document and send a message when it's over the build bar.
Oh, yeah, I didn't think about it. But then it seems to also intercept keyboard input at the same time. I only need to pass mouse scroll. Uhhh, I think it's a little bit over my head. I'll just stick with dragging scrollbar with mouse and blame Uber for not natively passing wheel events to scrollable elements...
Looks like no-keyboard on the build bar panel in live_game is the limiting factor. Of course changing that may have side effects, and zoom will still happen regardless.