Page MenuHomeWildfire Games

Fix chat performance issue.
Needs ReviewPublic

Authored by wraitii on Feb 23 2019, 4:18 AM.

Details

Reviewers
vladislavbelov
nani
Group Reviewers
Restricted Owners Package(Owns No Changed Paths)
Restricted Owners Package(Owns No Changed Paths)
Summary

New chat text is added as a copy of all previous chat and new one, use CList so it is not. Also adds a handy JS Interface addItem method to add new comments efficiently.
With this fix you can add a new message every onTick for 5000 ticks and not notice any performance hit (not like the current one that at 500 lines starts to drop fps).

Test Plan

Check text displays correctly when added.

Diff Detail

Repository
rP 0 A.D. Public Repository
Branch
_gui_refacto
Lint
Lint OK
Unit
No Unit Test Coverage
Build Status
Buildable 7886
Build 12832: Vulcan BuildJenkins
Build 12831: arc lint + arc unit

Event Timeline

nani created this revision.Feb 23 2019, 4:18 AM
nani edited the summary of this revision. (Show Details)
Stan added reviewers: vladislavbelov, Restricted Owners Package, Restricted Owners Package.Feb 23 2019, 11:55 AM
elexis removed a reviewer: elexis.Feb 23 2019, 4:04 PM

So should optimize both performance and code complexity.
What is the performance critical part where FPS start to be dropped?
Is it the JS string building from the message history array or the passing of a long string?
I.e. is Engine.GetGuiObjectByName("chat").text += newline similarly quick as the proposed diff?

Notice the session chat is not affected by this patch and it has to be rebuilt in case someone selects a different playerview or triggers the diplomacy colors feature (or muted/unmuted someone, or on other events).
I guess the lobby dialog UI performance is relevant FPS-wise because the lobby can be opened ingame.

Shouldn't selecting multiple lines of text become possible eventually? Would CList allow for that?

binaries/data/mods/public/gui/lobby/lobby_panels.xml
259

follow_last_if_visible -> there was some common name for it in chat apps, autoscroll or something

nani marked an inline comment as done.EditedFeb 23 2019, 4:57 PM
nani added a subscriber: elexis.
In D1781#72304, @elexis wrote:

So should optimize both performance and code complexity.

A little hard to add features without adding complexity. (can be refactored later tho but that's not the scope of this patch)

What is the performance critical part where FPS start to be dropped?

Having the lobby open for 1h (lots of comments on chat)

Is it the JS string building from the message history array or the passing of a long string?

Creates a big string from array.join("/n)

I.e. is Engine.GetGuiObjectByName("chat").text += newline similarly quick as the proposed diff?

First: += doesn't work as there is a JS <-> C++ conversion going on.
Second: even if it worked the C++ implementation would anyways delete all the list and make a new one.

Notice the session chat is not affected by this patch and it has to be rebuilt in case someone selects a different playerview or triggers the diplomacy colors feature (or muted/unmuted someone, or on other events).

Yep. Nothing can be done there (unless if <objects type="list"> is used for each filter but that sounds ugly.

I guess the lobby dialog UI performance is relevant FPS-wise because the lobby can be opened in-game.

Is not about the opening (first load) is about the accumulated text being added as if it were a whole new text each time.

Shouldn't selecting multiple lines of text become possible eventually? Would CList allow for that?

For the moment selected setting tag is a int type so no (an array of ints or a set could).

binaries/data/mods/public/gui/lobby/lobby_panels.xml
259

auto_scroll is a different concept as it follows the selected one (e.g. when you move with key_up key_down)
follow_last_if_visible always "follows" the last item (the bottom-most one) if visible. Doesn't really follow the last item but gets current scroll position and if it is 20px near of the max scroll position then it sets it to to the max position (as a message message could be very long and make knowing the text size useless).

elexis added a comment.EditedFeb 23 2019, 6:00 PM

The question is why this should become a CList to begin with if one doesn't need to change C++ code in order to remove the .join call. text .= newline should work, even if it will go through the JS interface once to read and once to write.

Edit: Could even add a .pushText method to the CText or CInput or whatever this should become. Would need a bit of coding, but I don't see the reason for making this a CList.

elexis added inline comments.Feb 23 2019, 6:11 PM
source/gui/scripting/JSInterface_IGUIObject.cpp
81

The mess with adding new entries here is that these properties are available to every GUIObject type, not only the one type that it was implemented with so far. So there ought to be GUIObject-type specific JSInterfaces (while possibly keeping the properties that share the same code in a shared JSInterface file)

So if one adds such a property here, then only knowingly that it's increasing the mess.

(The alternative was adding a "Setting" in the CFoo.cpp, but that is even worse because functions aren't settings and we don't want a setter for that)

nani marked 2 inline comments as done.Feb 23 2019, 6:22 PM
nani added inline comments.
source/gui/scripting/JSInterface_IGUIObject.cpp
81

I know. When I started I thought of different strategies but for the meanwhile this is the cleanest of all without doing a massive changes on how GUI code works. :/

nani marked an inline comment as done.Feb 23 2019, 6:24 PM
nani added inline comments.
source/gui/scripting/JSInterface_IGUIObject.cpp
81

Also having random macro definitions spread all over the place doesn't help knowing what can be changed.

nani added a comment.Feb 23 2019, 7:29 PM
In D1781#72322, @elexis wrote:

The question is why this should become a CList to begin with

CList could store metadata of each text in data_list settings (which CText could not)
and well, usually all chats (just look any webpage code) are designed as a list of text (<divs>) and a chat message is like a unit of independent text so the reasoning would be why have it as CTextOwner and not CList.

nani updated this revision to Diff 7498.Feb 23 2019, 10:01 PM
nani edited the test plan for this revision. (Show Details)

Remove setting follow_last_if_visible as is orthogonal to the patch (will add in another patch if this goes through).

So the title "Fix chat performance issue" is actually not the purpose of this patch, but to allow another patch to add the possibility of metadata?

If the slow part is the JS .join call concatenating and sprintf-formatting + 500 strings, we should remove that first and foremost (simply using .caption += new_line) before considering any C++ change.
It could be that this is already sufficient enough performance to provide sufficient performance.

Then if one wants to consider any C++ change, it's also not inherently clear whether creating a new CList item is actually faster than appending a string in the CText (or CInput for selectable text).
That one won't be able to select multiple lines with a CList seems like, undesirable.
The only reason why one can't use a CInput for chat currently is that the GUI tags (color, font) won't be applied to the text. (We should have selectable text for the terms and conditions).

nani added a comment.EditedFeb 26 2019, 9:33 PM
In D1781#72462, @elexis wrote:

If the slow part is the JS .join call concatenating and sprintf-formatting + 500 strings, we should remove that first and foremost (simply using .caption += new_line) before considering any C++ change.
It could be that this is already sufficient enough performance to provide sufficient performance.

I must say i haven't tried that (will try) but I'm 99% sure the problem is C++ given it needlessly deletes all computations as text parsing text sprite creation (expensive) and makes new ones for something which was already done.

ffffffff added a subscriber: ffffffff.EditedMar 3 2019, 3:34 AM

i feel fear in every elexis sentence

very dangerous

i dont see where this should get more complex.

complex != bigger

its just a new function feature

dont get urself insane about that

its just new nice stuff

xD

easy

Peace

elexis added a comment.Mar 3 2019, 3:50 PM
binaries/data/mods/public/gui/lobby/lobby.js
1408

Engine.GetMicroseconds() can be used to measure

nani added a comment.Mar 3 2019, 8:06 PM

@elexis chat from the lobby for 2.5 h at sunday.

Code measured:

        // the chat join("/n") text code is not takken into account
	let time1 = Engine.GetMicroseconds();
	Engine.GetGUIObjectByName("chatText").caption = textSmashup;
	let time2 = Engine.GetMicroseconds();

versus

	let time1 = Engine.GetMicroseconds();
	Engine.GetGUIObjectByName("chatText").addItem(formatted);
	let time2 = Engine.GetMicroseconds();

For reference at 60 fps, 1 frame takes 16666 microseconds

eae what a nice chart

elexis eae

elexis added a comment.Mar 4 2019, 4:00 PM

// the chat join("/n") text code is not takken into account
let time1 = Engine.GetMicroseconds();
Engine.GetGUIObjectByName("chatText").caption = textSmashup;
let time2 = Engine.GetMicroseconds();

That means there is a textSmashup = chatHistory.join("\n") prior to that code, i.e. that the only thing you measure is the caption assignment?

So it takes 5 milliseconds for .caption = text if text is 500 lines long and 5ms equate to 1/200th of a second.
That is JSI_IGUIObject::setProperty which calls ScriptInterface::FromJSVal and GUI<CStrW>::SetSetting, the latter calls CText::SetupText(), which calls GetGUI()->GenerateText and computes the scrollbar properties.

So it's the GenerateText call on a 500line string that consumes 5ms?
But reading CList::AddItem, that also calls CList::SetupText which calls GenerateText for every item, but your diff replaces that call to a call to SetupTextLastItem removing all GenerateText calls except the one for the last item.
If there were numbers to back that up that would demonstrate that claim.
For instance one could call GenerateText to a 500 line string and see how much time that costs.
I guess it's the parsing of the GUI tags that might take so long (linear complexity).
So if the analysis is correct, the SetupTextLastItem part of the diff might be correct to fix that removed TODO in CList.cpp (should just verify that one really doesnt need to rebuild texts or whatever else is rebuilt upon AddItem).

Now the remaining quesiton would be whether the same performance improvement could be achieved for CText and CInput.
Perhaps they could store a vector of GeneratedTexts to implement an AddCaption with time complexity that has less than linear relation to the total caption length;
and then some day enable GUITag parsing for read-only CInputs, and thus make the chat both selectable and high-performant / O(1).

source/gui/CList.cpp
213

Sounds like copypasta, at least the TODO from 2004 is, helper function preferable if possible

nani added a comment.Mar 4 2019, 9:39 PM

That means there is a textSmashup = chatHistory.join("\n") prior to that code, i.e. that the only thing you measure is the caption assignment?

Yes. I'm 100% sure c++ code is the problem.

So it takes 5 milliseconds for .caption = text if text is 500 lines long and 5ms equate to 1/200th of a second.
That is JSI_IGUIObject::setProperty which calls ScriptInterface::FromJSVal and GUI<CStrW>::SetSetting, the latter calls CText::SetupText(), which calls GetGUI()->GenerateText and computes the scrollbar properties.
So it's the GenerateText call on a 500line string that consumes 5ms?
But reading CList::AddItem, that also calls CList::SetupText which calls GenerateText for every item, but your diff replaces that call to a call to SetupTextLastItem removing all GenerateText calls except the one for the last item.
If there were numbers to back that up that would demonstrate that claim.

I did a chart in the previous demonstrating that is indeed GenerateText (what else could be? is clear as water that that for loop in GenerateText is unnecessary to add only another item-.-)

For instance one could call GenerateText to a 500 line string and see how much time that costs.

Already done in the chart.

I guess it's the parsing of the GUI tags that might take so long (linear complexity).

Parsing + text sprite generation

So if the analysis is correct, the SetupTextLastItem part of the diff might be correct to fix that removed TODO in CList.cpp (should just verify that one really doesnt need to rebuild texts or whatever else is rebuilt upon AddItem).
Now the remaining quesiton would be whether the same performance improvement could be achieved for CText and CInput.

Yes it can be done.

Perhaps they could store a vector of GeneratedTexts to implement an AddCaption with time complexity that has less than linear relation to the total caption length;
and then some day enable GUITag parsing for read-only CInputs, and thus make the chat both selectable and high-performant / O(1).

They already do have a vector, just needs someone to implement the JSinterface + methods for each case. :)

elexis added a comment.Mar 5 2019, 4:05 PM

Yes. I'm 100% sure c++ code is the problem.

That 'Yes' was the sought answer. (Being sure oneself is good, but it doesn't demonstrate the statement is the case.)

I did a chart in the previous demonstrating that is indeed GenerateText

The chart measures "caption" property change time, not GenerateText performance.

what else could be?

Given that GenerateText is only one of many statements called, it's not trivial that this statement is the bottleneck.

is clear as water that that for loop in GenerateText is unnecessary to add only another item-.-

So it's as clear as the source code at best, and the source code itself only tells us the complexity (but different statements can also cost different time to perform.)
While didding through it I find IGUITextOwner::AddText which sounds inviting for according changes to other GUI object types.
The performance defect in question was the CText.cpp one, not CList (so "to add only another line" that is.)

(Notice that addItem would be problematic for COList since an item consists of multiple cells there. I had a patch that changes the assignment, then one could make it an addItem that receives a JS::Value, a JSObject for COList and a String for CList.)
(As mentioned, it would be cleaner IMO to name it addText for CText and use specific functions per GUI object type.)

They already do have a vector, just needs someone to implement the JSinterface + methods for each case. :)

Seems like a vector with exactly one item for CText, so it would also need to revisit CText.cpp.

Yes it can be done.

If it can be done, the question is should it be done? i.e. why should this become a CList when it isn't one? I guess it's because the use case analysis was capped to what is currently implemented.
As mentioned some polished version of the CList diff seems appropriate given that it relates to the TODO.

source/gui/scripting/JSInterface_IGUIObject.cpp
728

(IIRC JS_THIS_OBJECT is replaced in some future SM version)

nani added a comment.Mar 6 2019, 12:26 AM
In D1781#72577, @elexis wrote:

As mentioned some polished version of the CList diff seems appropriate given that it relates to the TODO.

So can we have middle ground? The new method can be polished and use it at face value for the meanwhile so the chat doesn't hog the lobby.
All chat features will still be the same (needs that new extra tag to follow text bottom given that CList doesn't have it).
Nothing will change for the end user except 0ad being a little bit more performant. Not bad deal IMHO.

elexis added a comment.Mar 6 2019, 6:57 PM

I didn't investigate the code specifically, but in principle sure. The argument would be that this thing is written now and writing the better thing is too boring.
The problem is that selectable text isn't unlikely to happen in the next years, and then someone still has to write the patch the way it should be and make the current code unused.
And the same method would still be missing from the other GUI object types - ideally every GUI type would have an according append function.
Should comment on the affected GUI objects that this uses the list type for the complexity, since otherwie the next best reader may think it's a mistake and revert it.

Also if one identified such a unique problem in one case like you did here, one should abstract and see if one can spot the same problem in other parts of the code.
I.e. do we have other GUI objects that replace text repeatedly and possibly contain so many GUI tags that it can affect the performance?

For instance the gamesetup refreshes all UI objects, i.e. many GUI objects with maybe one GUI tag. And the gamesetup also contains the gamedescriptions.js thing listing all selected matchsettings, i.e. many GUI tags.
The replay menu uses a table (COList.cpp), contains many tags too.
So it would be good to check a bit whether one can add similar fixes for GUI object types in general.
A low hanging fruit for example would be not regenerating the text if the caption is still the same as before. Might cut 90% of the time if that isn't already the case.

nani added a comment.Mar 6 2019, 8:06 PM

The thing is I did look into to changing CText CTextOwnder and similar files but the current inheritance model that is made with is quite difficult to expand so I went for the most straight forward/short solution. Maybe composition over inheritance would give more leverage but that means a rewrite of the GUI.

elexis added a comment.Mar 6 2019, 8:15 PM

Isn't it possible to edit CText.cpp and CInput.cpp like you did with CList.cpp here, without messing with the text owner class at all? An addText or appendLine function in these GUI objects that just adds to m_GeneratedTexts and the caption string without rebuilding the existing code.

nani added a comment.Mar 6 2019, 8:26 PM

Technically yes but I'm not sure it won't break something else (like the charts).

elexis added a comment.Mar 6 2019, 8:48 PM

CChart doesn't inherit anything from CText or CInput, does it? And if we can't tell for CText or CInput whether something might break somewhere, how can we tell for CList?

nani added a comment.Mar 6 2019, 8:59 PM

Not sure. But does look deceptively easy.

nani added a comment.Mar 6 2019, 9:01 PM

Still, it doesn't solve the problem of bloating the base object if we want to make a JS interface for each gui type.

elexis added a comment.Mar 6 2019, 9:37 PM

It'd probably be the cleanest to have one small JSInterface_GUIObjectType class that contains the functions and properties specific to that GUI type, while inheriting or calling a JSInterface_GUIObject with the functions and properties common to all GUI Object types (as mentioned in D1781#72323).
If it's done before, that would leave clean insertions of the proposed new JSInterface GUIObject function/s, but I suppose it can also be done afterwards (never) if it's not a requirement for the CList diff to straighten that. (That commit would also be accompanied by other commits IIRC.)

Still, it doesn't solve the problem of bloating the base object if we want to make a JS interface for each gui type.

Otherwise not sure what base object actually you're refering to.

Also shouldn't forget to check

not regenerating the text if the caption is still the same as before. Might cut 90% of the time if that isn't already the case.

I guess that's what you refered to when not being sure about the consequences of changing the IGUITextOwner? (Since that's the place where it checks for caption property changes).
It looks like it currently regenerates the caption even if it didn't change, and would probably cut a lot of performance in some of the mentioned cases. (And another Gee TODO from 2004 running gag there.)
The consequences of not regenerating the texts might be foreseeable for all child classes and one might conclude whether it's good or not to do so. I expect that all GUI Object types that inherit the TextOwner are expected to keep the caption property in sync with the generated texts, thus it would be harmless and useful with the current code, and a code where CInput and CText receive an AddLine function that extend the caption property and add an item to the m_GeneratedTexts vector.

Stan added a subscriber: Stan.Mar 7 2019, 11:42 AM
Stan added inline comments.
source/gui/CList.cpp
163

Still accurate ?

440

Any way to make a helper function with the one below ?

481

Might want to add doxygen here as well.

source/gui/CList.h
46

C++ uses Doxygen, not JSDoc, so I'm not sure this works.

https://stackoverflow.com/questions/11991616/use-of-see-or-link-in-doxygen

In case it doesn't, you should add @param

source/gui/IGUIObject.h
133

@paramsmissing :)

135

Typo in warning.

source/gui/scripting/JSInterface_IGUIObject.cpp
756

Add one ;) If you are using vscode, you can configure it to do it for you :)

wraitii commandeered this revision.May 28 2019, 10:57 PM
wraitii added a reviewer: nani.
wraitii updated this revision to Diff 8195.EditedMay 28 2019, 11:00 PM
wraitii added a subscriber: Itms.

(This was far harder than it should have been. I feel like a complete refactor of these classes is necessary, but it's a bit out of scope here).

There's a fun little comment in SetupText() from 2006 in CList.cpp:

	// Delete all generated texts. Some could probably be saved,
	//  but this is easier, and this function will never be called
	//  continuously, or even often, so it'll probably be okay.

Turns out... It wasn't.

Indeed regenerating the text every time we append can get slow. Sadly, CText handles text as one big SGUIText and not breaking on new lines, so it's fairly difficult to extend to support appending.

As it turns out, CList is barely better - I went with something that minimises the code duplication, but this really should be refactored entirely. COList is even more annoying with the columns - I've given up.

To make @elexis happy, this only adds "addItem" on the list objects. I think ultimately we could refactor more of the GUI like that.

@Itms , also interested in your opinion of this JS-GUIObject extension

Successful build - Chance fights ever on the side of the prudent.

Linter detected issues:
Executing section Source...

source/gui/CList.h
|   1| /*·Copyright·(C)·2017·Wildfire·Games.
|    | [NORMAL] LicenseYearBear:
|    | License should have "2019" year instead of "2017"

source/gui/CList.cpp
|   1| /*·Copyright·(C)·2018·Wildfire·Games.
|    | [NORMAL] LicenseYearBear:
|    | License should have "2019" year instead of "2018"
Executing section JS...
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  47|  47|  * The playerlist will be assembled using these values.
|  48|  48|  */
|  49|  49| var g_PlayerStatuses = {
|  50|    |-	"available": { "color": "0 219 0",     "status": translate("Online") },
|    |  50|+	"available": { "color": "0 219 0", "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'away'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  48|  48|  */
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|    |-	"away":      { "color": "229 76 13",   "status": translate("Away") },
|    |  51|+	"away": { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  48|  48|  */
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|    |-	"away":      { "color": "229 76 13",   "status": translate("Away") },
|    |  51|+	"away":      { "color": "229 76 13", "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'playing'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|    |-	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|    |  52|+	"playing": { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|    |-	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|    |  52|+	"playing":   { "color": "200 0 0", "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'offline'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|    |-	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|    |  53|+	"offline": { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|  56|  56| 
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|    |-	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|    |  53|+	"offline":   { "color": "0 0 0", "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|  56|  56| 
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'unknown'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|    |-	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|    |  54|+	"unknown": { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|  56|  56| 
|  57|  57| var g_RoleNames = {
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 5 tabs but found 4.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 219| 219| 					me ?
| 220| 220| 						translate("You have been muted.") :
| 221| 221| 						translate("%(nick)s has been muted.") :
| 222|    |-				newrole == "moderator" ?
|    | 222|+					newrole == "moderator" ?
| 223| 223| 					me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 6 tabs but found 5.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 220| 220| 						translate("You have been muted.") :
| 221| 221| 						translate("%(nick)s has been muted.") :
| 222| 222| 				newrole == "moderator" ?
| 223|    |-					me ?
|    | 223|+						me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 221| 221| 						translate("%(nick)s has been muted.") :
| 222| 222| 				newrole == "moderator" ?
| 223| 223| 					me ?
| 224|    |-						translate("You are now a moderator.") :
|    | 224|+							translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 222| 222| 				newrole == "moderator" ?
| 223| 223| 					me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225|    |-						translate("%(nick)s is now a moderator.") :
|    | 225|+							translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 6 tabs but found 4.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 223| 223| 					me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226|    |-				msg.oldrole == "visitor" ?
|    | 226|+						msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 5.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227|    |-					me ?
|    | 227|+							me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228|    |-						translate("You have been unmuted.") :
|    | 228|+								translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229|    |-						translate("%(nick)s has been unmuted.") :
|    | 229|+								translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
| 232| 232| 						translate("%(nick)s is not a moderator anymore.");
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 5.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230|    |-					me ?
|    | 230|+							me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
| 232| 232| 						translate("%(nick)s is not a moderator anymore.");
| 233| 233| 
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231|    |-						translate("You are not a moderator anymore.") :
|    | 231|+								translate("You are not a moderator anymore.") :
| 232| 232| 						translate("%(nick)s is not a moderator anymore.");
| 233| 233| 
| 234| 234| 			addChatMessage({
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
| 232|    |-						translate("%(nick)s is not a moderator anymore.");
|    | 232|+								translate("%(nick)s is not a moderator anymore.");
| 233| 233| 
| 234| 234| 			addChatMessage({
| 235| 235| 				"text": "/special " + sprintf(txt, { "nick": msg.nick }),

binaries/data/mods/public/gui/lobby/lobby.js
|1035| »   »   switch·(sortBy)
|    | [NORMAL] ESLintBear (default-case):
|    | Expected a default case.

binaries/data/mods/public/gui/lobby/lobby.js
| 729| »   »   case·'name':
|    | [NORMAL] JSHintBear:
|    | Expected a 'break' statement before 'default'.
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  62|  62| var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
|  63|  63| 
|  64|  64| var g_PlayerTeamList = prepareForDropdown([{
|  65|    |-		"label": translateWithContext("team", "None"),
|    |  65|+	"label": translateWithContext("team", "None"),
|  66|  66| 		"id": -1
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  63|  63| 
|  64|  64| var g_PlayerTeamList = prepareForDropdown([{
|  65|  65| 		"label": translateWithContext("team", "None"),
|  66|    |-		"id": -1
|    |  66|+	"id": -1
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  64|  64| var g_PlayerTeamList = prepareForDropdown([{
|  65|  65| 		"label": translateWithContext("team", "None"),
|  66|  66| 		"id": -1
|  67|    |-	}].concat(
|    |  67|+}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  65|  65| 		"label": translateWithContext("team", "None"),
|  66|  66| 		"id": -1
|  67|  67| 	}].concat(
|  68|    |-		Array(g_MaxTeams).fill(0).map((v, i) => ({
|    |  68|+	Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|  71|  71| 		}))
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  66|  66| 		"id": -1
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|    |-			"label": i + 1,
|    |  69|+		"label": i + 1,
|  70|  70| 			"id": i
|  71|  71| 		}))
|  72|  72| 	)
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|    |-			"id": i
|    |  70|+		"id": i
|  71|  71| 		}))
|  72|  72| 	)
|  73|  73| );
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|  71|    |-		}))
|    |  71|+	}))
|  72|  72| 	)
|  73|  73| );
|  74|  74| 
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|  71|  71| 		}))
|  72|    |-	)
|    |  72|+)
|  73|  73| );
|  74|  74| 
|  75|  75| /**
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  78|  78| var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1);
|  79|  79| 
|  80|  80| var g_PlayerCivList = g_CivData && prepareForDropdown([{
|  81|    |-		"name": translateWithContext("civilization", "Random"),
|    |  81|+	"name": translateWithContext("civilization", "Random"),
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  79|  79| 
|  80|  80| var g_PlayerCivList = g_CivData && prepareForDropdown([{
|  81|  81| 		"name": translateWithContext("civilization", "Random"),
|  82|    |-		"tooltip": translate("Picks one civilization at random when the game starts."),
|    |  82|+	"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  80|  80| var g_PlayerCivList = g_CivData && prepareForDropdown([{
|  81|  81| 		"name": translateWithContext("civilization", "Random"),
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|    |-		"color": g_ColorRandom,
|    |  83|+	"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  81|  81| 		"name": translateWithContext("civilization", "Random"),
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|    |-		"code": "random"
|    |  84|+	"code": "random"
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|    |-	}].concat(
|    |  85|+}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|  86|    |-		Object.keys(g_CivData).filter(
|    |  86|+	Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|    |-			civ => g_CivData[civ].SelectableInGameSetup
|    |  87|+		civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|    |-		).map(civ => ({
|    |  88|+	).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|    |-			"name": g_CivData[civ].Name,
|    |  89|+		"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|    |-			"tooltip": g_CivData[civ].History,
|    |  90|+		"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|    |-			"color": g_ColorRegular,
|    |  91|+		"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|  94|  94| 	)
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|    |-			"code": civ
|    |  92|+		"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|  94|  94| 	)
|  95|  95| );
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|    |-		})).sort(sortNameIgnoreCase)
|    |  93|+	})).sort(sortNameIgnoreCase)
|  94|  94| 	)
|  95|  95| );
|  96|  96| 
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|  94|    |-	)
|    |  94|+)
|  95|  95| );
|  96|  96| 
|  97|  97| /**
|    | [NORMAL] ESLintBear (no-trailing-spaces):
|    | Trailing spaces not allowed.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|1240|1240| 			offset = -Math.min(slideSpeed * dt, maxOffset);
|1241|1241| 	}
|1242|1242| 
|1243|    |-	updateSettingsPanelPosition(offset);	
|    |1243|+	updateSettingsPanelPosition(offset);
|1244|1244| }
|1245|1245| 
|1246|1246| /**
|    | [NORMAL] ESLintBear (curly):
|    | Unnecessary { after 'if' condition.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|1774|1774| 	let biomeList;
|1775|1775| 
|1776|1776| 	if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes)
|1777|    |-	{
|    |1777|+	
|1778|1778| 		if (typeof g_GameAttributes.settings.SupportedBiomes == "string")
|1779|1779| 			biomeList = g_Settings.Biomes.filter(biome => biome.Id.startsWith(g_GameAttributes.settings.SupportedBiomes));
|1780|1780| 		else
|1781|1781| 			biomeList = g_Settings.Biomes.filter(
|1782|1782| 				biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1);
|1783|    |-	}
|    |1783|+	
|1784|1784| 
|1785|1785| 	g_BiomeList = biomeList && prepareForDropdown(
|1786|1786| 		[{

binaries/data/mods/public/gui/gamesetup/gamesetup.js
|2018| »   while·(g_IsNetworked)
|    | [NORMAL] ESLintBear (no-unmodified-loop-condition):
|    | 'g_IsNetworked' is not modified in this loop.
Executing section cli...

Link to build: https://jenkins.wildfiregames.com/job/differential/1536/display/redirect

wraitii added inline comments.May 29 2019, 9:41 AM
source/gui/CList.cpp
80

The dynamic_cast is required because we are down casting from a virtual base.

It could be skipped if IGuiObject implemented a virtual "GetThis" method, but that's not much better than dynamic_cast imo.

Alternaties include storing a more specific pointer in a reserved slot and refactoring this entirely.

source/gui/CList.cpp
80

I disagree, dynamic_cast is worse: slower (~2 times slower on some machines) and less logic safe. Also it's forbidden by CC. dynamic_cast means bad design in that case, because we already have a type of the object on its creation.

Simple virtual method is just faster.

CList* CList::AsCList() override { return this; }
86

Duplication of code. It can be written clearer.

wraitii marked an inline comment as not done.May 29 2019, 11:12 AM
wraitii added inline comments.
source/gui/CList.cpp
80

The ugliness is that we have to add AsCList to the IGUIObject's interface.

I'm working on an alternative variant using proxy objects , but tbh it remains kinda ugly so far.

vladislavbelov added inline comments.May 29 2019, 7:55 PM
source/gui/CList.cpp
80

I don't want to say that virtual functions are best. But imo they're better than dynamic_cast. Also you could use IGUIObject type (probably convert it to an enum value) and static_cast inside a switch.

But I have an idea, how it could be done without much work (I'm not sure, but still).

We create a GUI object definition once, so we can do the same stuff for derived classes using prototypes.

// How we define a class:
JS_InitClass(
	m->m_cx, global, JS::NullPtr(), // <- This nullptr is a parent prototype, like a "base" class.
	clasp, constructor, minArgs, ps, fs, static_ps, static_fs)

We create a GUI object in 2 places:

JSObject* IGUIObject::GetJSObject()
{
	// ...
	m_JSObject.init(cx, m_pGUI->GetScriptInterface()->CreateCustomObject("GUIObject"));
	// ...
}

bool JSI_IGUIObject::construct(JSContext* cx, uint argc, JS::Value* vp)
{
	// ...
	JS::RootedObject obj(cx, pScriptInterface->CreateCustomObject("GUIObject"));
	// ...
}

The first method can be easily modified to use the one virtual method to override a JS class name for derived classes. I didn't look much, but I think the second is solvable too.

source/gui/CList.cpp
80

The second one you even fixes in D1933.

wraitii added inline comments.Jun 6 2019, 8:06 PM
source/gui/CList.cpp
80

You mean creating "GUiOBject", then "GuiListObject" for example? The problem with that is that is assumes single inheritance, which isn't the case right now.

I haven't understood your enum comment though.

Stan added inline comments.Jun 6 2019, 8:17 PM
source/gui/CList.cpp
467

Early return maybe ?

vladislavbelov added inline comments.Jun 7 2019, 8:43 PM
source/gui/CList.cpp
80

You mean creating "GUiOBject", then "GuiListObject" for example?

Yes, with different functions, the first one is the prototype for the second one.

The problem with that is that is assumes single inheritance, which isn't the case right now.

It's not a problem I think. Most of our multiple inheritances are technical details and can be skipped in the JS interface.

For ex. you may skip some classes in a tree and combine the final object with inherited functions at run time (or even compile time with templates). We don't need to cast different objects in JS, so we can use it.

Like:


class JSObjectCreator {

class A : public B, public C
{
  static Functions GetFunctions()
  {
    return {
      FunctionsOfA(),
      B::GetFunctions(),
      C::GetFunctions()
    };
  }

  static void Create()
  {
    JS_InitClass(
      m->m_cx, global, JS::NullPtr(),
      clasp, constructor, minArgs, ps, GetFunctions(), static_ps, static_fs);
  }
};

I haven't understood your enum comment though.

Usual explicit cast:

enum class GUIObjectType
{
  CHART,
  INPUT,
  // ...
}

class IGUIObject
{
  virtual GUIObjectType GetType() = 0;
}

template<typename T>
T* AsGUIObject(IGUIObject* object)
{
  if (object->GetType != T::Type())
    return nullptr;
  return static_cast<T*>(object);
}

void DoSomething(IGUIObject* object)
{
  switch (object->GetType)
  {
  case GUIObjectType::INPUT: /* ... */ break;
  }
}
elexis added a comment.Jun 7 2019, 9:16 PM

To make @elexis happy

I appreciate it, but it's really in every code users interest to have this file split well, as there are already 3 random functions that are uncommon for JSInterface GUI Objects but defined here (committed or not).
Ticket was created at #5442.

The pattern vladislav posted looks like it separates the concerns (common and specific GUIObject methods) well.
wraitiis version does that too, but splits it at the costs of removing the split of the JSInterface from the GUIObject.

I'm not sure if we need a JSObjectCreator wrapper around class A : public B, public C, as I guess there would be more than class A (we have three use cases for JSInterface functions relating to specific GUI object types).

Also I guess it's better than before, but I still think that you can accomplish the same with CText and an append method, as said. This way the text can become selectable, as CList probably not.

(Another considerable feature was redacting chat messages by moderators after they were posted by sending a redaction chat message, but that's far fetched, and it's rare enough that one can rebuild the chatlog)

source/gui/CList.cpp
78
125

This is code that really sounds like it should be in a JSInterface_*GUI*List* file, it's the only one operating on a JS context and, it defacto is the JS Interface.

source/gui/scripting/JSInterface_IGUIObject.cpp
82

(This idea of #5442 is that this file becomes agnostic of specific GUIObject properties, so ideally this reference (and getTextSize) would become removed here as well.)

wraitiis version does that too, but splits it at the costs of removing the split of the JSInterface from the GUIObject.

TBH I found myself wondering how useful that split is. Separating things in another file wouldn't be too difficult I suppose.

The actual code to enable appending on lists or text is difficult to write because the whole class isn't designed for it, so I suggest we fix this in a somewhat 'quick' way and then come back and clean it up, possibly.

source/gui/CList.cpp
80

Might be the best we'll get indeed.

source/gui/scripting/JSInterface_IGUIObject.cpp
82

That's unfortunately impossible/annoying to do while we still override the "get" property. That needs to be changed since SM5X+ won't support it anymore, but until then the easiest is a line there.

wraitii updated this revision to Diff 8426.EditedJun 10 2019, 9:38 PM

Cleaner diff, without dynamic_cast.

I went with a virtual method returning a pointer to void*. It looks ugly, but it kind of is (as discussed before), and using an enum would require changing a common file every time we add a new type - which seems annoying and slow to compile to me.

(also moves the function to its own scripting file).

Successful build - Chance fights ever on the side of the prudent.

Linter detected issues:
Executing section Source...

source/gui/CList.cpp
|   1| /*·Copyright·(C)·2018·Wildfire·Games.
|    | [NORMAL] LicenseYearBear:
|    | License should have "2019" year instead of "2018"

source/gui/IGUIObject.cpp
|   1| /*·Copyright·(C)·2016·Wildfire·Games.
|    | [NORMAL] LicenseYearBear:
|    | License should have "2019" year instead of "2016"

source/gui/CList.h
|   1| /*·Copyright·(C)·2017·Wildfire·Games.
|    | [NORMAL] LicenseYearBear:
|    | License should have "2019" year instead of "2017"
Executing section JS...
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  47|  47|  * The playerlist will be assembled using these values.
|  48|  48|  */
|  49|  49| var g_PlayerStatuses = {
|  50|    |-	"available": { "color": "0 219 0",     "status": translate("Online") },
|    |  50|+	"available": { "color": "0 219 0", "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'away'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  48|  48|  */
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|    |-	"away":      { "color": "229 76 13",   "status": translate("Away") },
|    |  51|+	"away": { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  48|  48|  */
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|    |-	"away":      { "color": "229 76 13",   "status": translate("Away") },
|    |  51|+	"away":      { "color": "229 76 13", "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'playing'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|    |-	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|    |  52|+	"playing": { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  49|  49| var g_PlayerStatuses = {
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|    |-	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|    |  52|+	"playing":   { "color": "200 0 0", "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'offline'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|    |-	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|    |  53|+	"offline": { "color": "0 0 0",       "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|  56|  56| 
|    | [NORMAL] ESLintBear (no-multi-spaces):
|    | Multiple spaces found before '"status"'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  50|  50| 	"available": { "color": "0 219 0",     "status": translate("Online") },
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|    |-	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|    |  53|+	"offline":   { "color": "0 0 0", "status": translate("Offline") },
|  54|  54| 	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|  56|  56| 
|    | [NORMAL] ESLintBear (key-spacing):
|    | Extra space before value for key 'unknown'.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|  51|  51| 	"away":      { "color": "229 76 13",   "status": translate("Away") },
|  52|  52| 	"playing":   { "color": "200 0 0",     "status": translate("Busy") },
|  53|  53| 	"offline":   { "color": "0 0 0",       "status": translate("Offline") },
|  54|    |-	"unknown":   { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|    |  54|+	"unknown": { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") }
|  55|  55| };
|  56|  56| 
|  57|  57| var g_RoleNames = {
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 5 tabs but found 4.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 219| 219| 					me ?
| 220| 220| 						translate("You have been muted.") :
| 221| 221| 						translate("%(nick)s has been muted.") :
| 222|    |-				newrole == "moderator" ?
|    | 222|+					newrole == "moderator" ?
| 223| 223| 					me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 6 tabs but found 5.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 220| 220| 						translate("You have been muted.") :
| 221| 221| 						translate("%(nick)s has been muted.") :
| 222| 222| 				newrole == "moderator" ?
| 223|    |-					me ?
|    | 223|+						me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 221| 221| 						translate("%(nick)s has been muted.") :
| 222| 222| 				newrole == "moderator" ?
| 223| 223| 					me ?
| 224|    |-						translate("You are now a moderator.") :
|    | 224|+							translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 222| 222| 				newrole == "moderator" ?
| 223| 223| 					me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225|    |-						translate("%(nick)s is now a moderator.") :
|    | 225|+							translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 6 tabs but found 4.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 223| 223| 					me ?
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226|    |-				msg.oldrole == "visitor" ?
|    | 226|+						msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 5.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 224| 224| 						translate("You are now a moderator.") :
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227|    |-					me ?
|    | 227|+							me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 225| 225| 						translate("%(nick)s is now a moderator.") :
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228|    |-						translate("You have been unmuted.") :
|    | 228|+								translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 226| 226| 				msg.oldrole == "visitor" ?
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229|    |-						translate("%(nick)s has been unmuted.") :
|    | 229|+								translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
| 232| 232| 						translate("%(nick)s is not a moderator anymore.");
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 7 tabs but found 5.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 227| 227| 					me ?
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230|    |-					me ?
|    | 230|+							me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
| 232| 232| 						translate("%(nick)s is not a moderator anymore.");
| 233| 233| 
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 228| 228| 						translate("You have been unmuted.") :
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231|    |-						translate("You are not a moderator anymore.") :
|    | 231|+								translate("You are not a moderator anymore.") :
| 232| 232| 						translate("%(nick)s is not a moderator anymore.");
| 233| 233| 
| 234| 234| 			addChatMessage({
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 8 tabs but found 6.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/lobby/lobby.js
| 229| 229| 						translate("%(nick)s has been unmuted.") :
| 230| 230| 					me ?
| 231| 231| 						translate("You are not a moderator anymore.") :
| 232|    |-						translate("%(nick)s is not a moderator anymore.");
|    | 232|+								translate("%(nick)s is not a moderator anymore.");
| 233| 233| 
| 234| 234| 			addChatMessage({
| 235| 235| 				"text": "/special " + sprintf(txt, { "nick": msg.nick }),

binaries/data/mods/public/gui/lobby/lobby.js
|1035| »   »   switch·(sortBy)
|    | [NORMAL] ESLintBear (default-case):
|    | Expected a default case.

binaries/data/mods/public/gui/lobby/lobby.js
| 729| »   »   case·'name':
|    | [NORMAL] JSHintBear:
|    | Expected a 'break' statement before 'default'.
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  62|  62| var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
|  63|  63| 
|  64|  64| var g_PlayerTeamList = prepareForDropdown([{
|  65|    |-		"label": translateWithContext("team", "None"),
|    |  65|+	"label": translateWithContext("team", "None"),
|  66|  66| 		"id": -1
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  63|  63| 
|  64|  64| var g_PlayerTeamList = prepareForDropdown([{
|  65|  65| 		"label": translateWithContext("team", "None"),
|  66|    |-		"id": -1
|    |  66|+	"id": -1
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  64|  64| var g_PlayerTeamList = prepareForDropdown([{
|  65|  65| 		"label": translateWithContext("team", "None"),
|  66|  66| 		"id": -1
|  67|    |-	}].concat(
|    |  67|+}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  65|  65| 		"label": translateWithContext("team", "None"),
|  66|  66| 		"id": -1
|  67|  67| 	}].concat(
|  68|    |-		Array(g_MaxTeams).fill(0).map((v, i) => ({
|    |  68|+	Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|  71|  71| 		}))
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  66|  66| 		"id": -1
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|    |-			"label": i + 1,
|    |  69|+		"label": i + 1,
|  70|  70| 			"id": i
|  71|  71| 		}))
|  72|  72| 	)
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  67|  67| 	}].concat(
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|    |-			"id": i
|    |  70|+		"id": i
|  71|  71| 		}))
|  72|  72| 	)
|  73|  73| );
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  68|  68| 		Array(g_MaxTeams).fill(0).map((v, i) => ({
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|  71|    |-		}))
|    |  71|+	}))
|  72|  72| 	)
|  73|  73| );
|  74|  74| 
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  69|  69| 			"label": i + 1,
|  70|  70| 			"id": i
|  71|  71| 		}))
|  72|    |-	)
|    |  72|+)
|  73|  73| );
|  74|  74| 
|  75|  75| /**
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  78|  78| var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1);
|  79|  79| 
|  80|  80| var g_PlayerCivList = g_CivData && prepareForDropdown([{
|  81|    |-		"name": translateWithContext("civilization", "Random"),
|    |  81|+	"name": translateWithContext("civilization", "Random"),
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  79|  79| 
|  80|  80| var g_PlayerCivList = g_CivData && prepareForDropdown([{
|  81|  81| 		"name": translateWithContext("civilization", "Random"),
|  82|    |-		"tooltip": translate("Picks one civilization at random when the game starts."),
|    |  82|+	"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  80|  80| var g_PlayerCivList = g_CivData && prepareForDropdown([{
|  81|  81| 		"name": translateWithContext("civilization", "Random"),
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|    |-		"color": g_ColorRandom,
|    |  83|+	"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  81|  81| 		"name": translateWithContext("civilization", "Random"),
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|    |-		"code": "random"
|    |  84|+	"code": "random"
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  82|  82| 		"tooltip": translate("Picks one civilization at random when the game starts."),
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|    |-	}].concat(
|    |  85|+}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  83|  83| 		"color": g_ColorRandom,
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|  86|    |-		Object.keys(g_CivData).filter(
|    |  86|+	Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  84|  84| 		"code": "random"
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|    |-			civ => g_CivData[civ].SelectableInGameSetup
|    |  87|+		civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  85|  85| 	}].concat(
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|    |-		).map(civ => ({
|    |  88|+	).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  86|  86| 		Object.keys(g_CivData).filter(
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|    |-			"name": g_CivData[civ].Name,
|    |  89|+		"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  87|  87| 			civ => g_CivData[civ].SelectableInGameSetup
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|    |-			"tooltip": g_CivData[civ].History,
|    |  90|+		"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  88|  88| 		).map(civ => ({
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|    |-			"color": g_ColorRegular,
|    |  91|+		"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|  94|  94| 	)
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 2 tabs but found 3.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  89|  89| 			"name": g_CivData[civ].Name,
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|    |-			"code": civ
|    |  92|+		"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|  94|  94| 	)
|  95|  95| );
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 1 tab but found 2.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  90|  90| 			"tooltip": g_CivData[civ].History,
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|    |-		})).sort(sortNameIgnoreCase)
|    |  93|+	})).sort(sortNameIgnoreCase)
|  94|  94| 	)
|  95|  95| );
|  96|  96| 
|    | [NORMAL] ESLintBear (indent):
|    | Expected indentation of 0 tabs but found 1.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|  91|  91| 			"color": g_ColorRegular,
|  92|  92| 			"code": civ
|  93|  93| 		})).sort(sortNameIgnoreCase)
|  94|    |-	)
|    |  94|+)
|  95|  95| );
|  96|  96| 
|  97|  97| /**
|    | [NORMAL] ESLintBear (no-trailing-spaces):
|    | Trailing spaces not allowed.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|1240|1240| 			offset = -Math.min(slideSpeed * dt, maxOffset);
|1241|1241| 	}
|1242|1242| 
|1243|    |-	updateSettingsPanelPosition(offset);	
|    |1243|+	updateSettingsPanelPosition(offset);
|1244|1244| }
|1245|1245| 
|1246|1246| /**
|    | [NORMAL] ESLintBear (curly):
|    | Unnecessary { after 'if' condition.
|----|    | /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|    |++++| /mnt/data/jenkins-phabricator/workspace/differential/binaries/data/mods/public/gui/gamesetup/gamesetup.js
|1774|1774| 	let biomeList;
|1775|1775| 
|1776|1776| 	if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes)
|1777|    |-	{
|    |1777|+	
|1778|1778| 		if (typeof g_GameAttributes.settings.SupportedBiomes == "string")
|1779|1779| 			biomeList = g_Settings.Biomes.filter(biome => biome.Id.startsWith(g_GameAttributes.settings.SupportedBiomes));
|1780|1780| 		else
|1781|1781| 			biomeList = g_Settings.Biomes.filter(
|1782|1782| 				biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1);
|1783|    |-	}
|    |1783|+	
|1784|1784| 
|1785|1785| 	g_BiomeList = biomeList && prepareForDropdown(
|1786|1786| 		[{

binaries/data/mods/public/gui/gamesetup/gamesetup.js
|2018| »   while·(g_IsNetworked)
|    | [NORMAL] ESLintBear (no-unmodified-loop-condition):
|    | 'g_IsNetworked' is not modified in this loop.
Executing section cli...

Link to build: https://jenkins.wildfiregames.com/job/differential/1686/display/redirect

nani awarded a token.Jun 11 2019, 9:57 PM