diff --git a/assets/data0_21pure/scripts/ui_testui.shader b/assets/data0_21pure/scripts/ui_testui.shader new file mode 100644 index 0000000000..b7ba89c8b9 --- /dev/null +++ b/assets/data0_21pure/scripts/ui_testui.shader @@ -0,0 +1,60 @@ +ui/testui/gfx/background +{ + nopicmip + nomipmaps + nocompress + nofiltering + cull none + + { + map ui/testui/gfx/back2.png + blendFunc blend + alphaGen const .1 + } + + { + map textures/billboard/scanlinenoise.png + blendFunc blend + tcMod scale 2 2 + tcMod scroll 0 0.02 + alphaGen const 0.05 + } + + { + map textures/billboard/scanlinenoise.png + blendFunc blend + tcMod scale 2 2 + tcMod transform 1 1 0 0 0.32 0.32 + tcMod scroll 0 0.05 + alphaGen const 0.05 + } +} + +ui/testui/gfx/background2 +{ + nopicmip + nomipmaps + nocompress + cull none + + { + map ui/testui/gfx/bandes2.png + blendFunc blend + alphagen wave sin 0.05 0.1 0 0.05 + tcmod scroll 0 -.08 + } +} + +ui/testui/gfx/loader_simple +{ + noPicmip + noMipmaps + nocompress + cull none + + { + clampmap ui/testui/gfx/loader_simple.png + blendfunc blend + tcmod rotate 500 + } +} diff --git a/assets/data0_21pure/ui/porkui/options_player.rml b/assets/data0_21pure/ui/porkui/options_player.rml index 0d74ae1825..76d5e18e8d 100644 --- a/assets/data0_21pure/ui/porkui/options_player.rml +++ b/assets/data0_21pure/ui/porkui/options_player.rml @@ -218,6 +218,21 @@ break; } } + + void newUiModal(void) + { + window.modal('modal_basic.rml?text=Warning%3A%20Experemental%20UI%20is%20still%20under%20development%20and%20may%20contain%20bugs%20or%20incomplete%20features.%20You%20will%20have%20an%20option%20to%20switch%20back%20to%20legacy%20(this)%20version.%20Please%20use%20it%20at%20your%20own%20risk%20and%20report%20any%20issues%20you%20encounter%20on%20our%20GitHub%20or%20in%20Discord.%20Thank%20you%20for%20your%20understanding%20and%20support&ypos=0.25'); + if(window.getModalValue() == 0) + { + switchToNewUi(); + } + } + + + void switchToNewUi(void) + { + game.execAppend( "ui_basepath ui/testui; ui_reload\n" ); + } @@ -493,7 +508,11 @@
- +
+
+ + + diff --git a/assets/data0_21pure/ui/testui/as/animations.as b/assets/data0_21pure/ui/testui/as/animations.as new file mode 100644 index 0000000000..53dfa2d931 --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/animations.as @@ -0,0 +1,162 @@ +/* +Copyright (C) 2012 Jannik Kolodziej ("drahtmaul") + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +funcdef void callback(); + +const int EASE_NONE = 0; +const int EASE_IN = 1; +const int EASE_OUT = 2; +const int EASE_INOUT = 3; + +Vec3 ANIM_LEFT( -120, 0, 0 ); +Vec3 ANIM_BOTTOM( 0, 100, 0 ); +Vec3 ANIM_RIGHT( 100, 0, 0 ); +Vec3 ANIM_TOP( 0, -100, 0 ); +Vec3 ANIM_ORIGIN( 0, 0, 0 ); + +const int ANIMATION_DURATION_FAST = 150; // how long will animations last? + +const int ANIMATION_TICK = 10; + +// This class will move your element from a certain point to another point. cVec3s need to be percantage values! +// Be aware that the elements position-property will be set to 'relative'! +// Will call the callback as soon as it is done with animation. +class MoveAnimation +{ + int animTime; + int animStartTime; + int animLastTime; + int animMoveTime; + Vec3 animStart; + Vec3 animDest; + Element @animElement; + int animEase; + bool ceased; + + callback @animDoneCallback; + + MoveAnimation( Element @elem, int time, Vec3 start, Vec3 dest, int ease, callback @animDoneCallback = null ) + { + @this.animElement = @elem; + this.animTime = time; + this.animStart = start; + this.animDest = dest; + this.animEase = ease; + this.ceased = false; + @this.animDoneCallback = @animDoneCallback; + if( time <= 0 ) { + animate(); + } + else { + this.startAnimation(); + } + } + + ~MoveAnimation( ) + { + @this.animDoneCallback = null; + @this.animElement = null; + } + + private void startAnimation() + { + if( @animElement == null || animTime <= 0 ) + return; + animMoveTime = 0; + animStartTime = animLastTime = window.time; + animElement.css( 'position', 'relative' ) + .css( 'left', animStart.x + "%" ) + .css( 'top', animStart.y + "%" ); + window.setInterval( __MoveAnimationCallback, ANIMATION_TICK, any(@this) ); + } + + bool animate() // do not call this one, it's just for the scheduler + { + if( @this.animElement == null ) + return false; // something went wrong + + if( this.ceased ) + { + if( @this.animDoneCallback is null ) // check whether we need to inform someone that we are done + return false; // doesn't look like it + else + this.animDoneCallback(); + return false; + } + + float frac; + + if( this.animTime > 0 ) + { + int moveTime = window.time - this.animLastTime; + if( moveTime > ANIMATION_TICK ) + moveTime = ANIMATION_TICK; + + animMoveTime += moveTime; + frac = float(animMoveTime) / this.animTime; + frac = applyEase( frac, this.animEase ); + if( frac > 1 ) + frac = 1; + } + else + { + frac = 1; + } + + this.animLastTime = window.time; + this.setElementPosition( 'left', animStart.x, animDest.x, frac ); + this.setElementPosition( 'top', animStart.y, animDest.y, frac ); + + if( frac == 1 ) // we are done + this.ceased = true; + return true; // continue to call this function + } + + private void setElementPosition( String prop, float start, float dest, float frac ) + { + float tmp = dest - start; + tmp = start + tmp * frac; + this.animElement.css( prop, int( tmp ) + "%" ); + } +} + +float applyEase( float x, int ease ) // x needs to be between 0 and 1 +{ + if( ease == EASE_NONE ) // FIXME: switch()? + return x; + else if( ease == EASE_IN ) + return x*x*x*x; // f(x)=x^4 + else if( ease == EASE_OUT ) + return pow( x, 0.25 ); // f(x)=x^0.25 + else if( ease == EASE_INOUT ) + return x * x * ( -2.0f * x + 3.0f ); // f(x)=-2x^3+3x^2 -- That math was fucking hard if you're not used to to that kind of stuff... + return 0; +} + +bool __MoveAnimationCallback( any & obj ) // this one will serve as a relay, handing scheduler-call over to the class +{ + MoveAnimation @anim; + obj.retrieve(@anim); + + if( @anim == null ) + return false; // something went wrong, just disable that scheduler + + return anim.animate(); +} diff --git a/assets/data0_21pure/ui/testui/as/base.as b/assets/data0_21pure/ui/testui/as/base.as new file mode 100644 index 0000000000..b64a808fdb --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/base.as @@ -0,0 +1,83 @@ +/* +Copyright (C) 2012 Chasseur de bots + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +const String S_COLOR_BLACK = "^0"; +const String S_COLOR_RED = "^1"; +const String S_COLOR_GREEN = "^2"; +const String S_COLOR_YELLOW = "^3"; +const String S_COLOR_BLUE = "^4"; +const String S_COLOR_CYAN = "^5"; +const String S_COLOR_MAGENTA = "^6"; +const String S_COLOR_WHITE = "^7"; +const String S_COLOR_ORANGE = "^8"; +const String S_COLOR_GREY = "^9"; + +/* +* Converts "R G B" decimal representation to #hhhhhh representation +*/ +String @RGB2Hex( const String &in rgb ) +{ + array @components = StringUtils::Split( rgb, " " ); + + String hex = "#"; + + uint count = 0; + uint components_size = components.size(); + for( uint i = 0; i < components_size; i++ ) { + String @component = components[i]; + if( component.empty() ) { + continue; + } + + hex += StringUtils::FormatInt( component.toInt(), '0h', 2 ); + + count++; + if( count == 3 ) { + // parsed all 3 components + break; + } + } + + return hex; +} + +/* +* Converts #hhhhhh representation to "R G B" decimal representation +*/ +const String @Hex2RGB( const String &in hex ) +{ + if( hex.length() < 7 || hex.substr( 0, 1 ) != '#' ) { + // wrong format + return '0 0 0'; + } + + uint r = StringUtils::Strtol( hex.substr( 1, 2 ), 16 ) & 0xff; + uint g = StringUtils::Strtol( hex.substr( 3, 2 ), 16 ) & 0xff; + uint b = StringUtils::Strtol( hex.substr( 5, 2 ), 16 ) & 0xff; + + return '' + r + ' ' + g + ' ' + b; +} + +void OpenQuitDialog( void ) +{ + window.modal('modal_basic.rml?text=Are%20you%20sure%20you%20want%20to%20quit%3F&ypos=0.25'); + if (window.getModalValue() == 0) + game.execAppend('quit\n'); +} diff --git a/assets/data0_21pure/ui/testui/as/callvotes.as b/assets/data0_21pure/ui/testui/as/callvotes.as new file mode 100644 index 0000000000..2f016987de --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/callvotes.as @@ -0,0 +1,43 @@ +/* +Copyright (C) 2013 Chasseur de bots + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +namespace Callvotes +{ + bool playerHasVoted( int playerNum ) + { + String votes = ::game.cs( ::CS_ACTIVE_CALLVOTE_VOTES ); + + if (votes.empty()) { + return false; + } + + arrayvectors = StringUtils::Split( votes, ' ' ); + int vectorid = playerNum / 31; + + if( playerNum < 0 || vectorid >= int(vectors.length()) ) { + return false; + } + + String hex = '0x' + vectors[vectorid]; + int bits = hex.toInt(); + bool voted = bits & (1<<(playerNum&31)) != 0; + return voted; + } +} diff --git a/assets/data0_21pure/ui/testui/as/favorites.as b/assets/data0_21pure/ui/testui/as/favorites.as new file mode 100644 index 0000000000..7fe92ba3cf --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/favorites.as @@ -0,0 +1,90 @@ +/* +Copyright (C) 2012 Jannik Kolodziej ("drahtmaul") + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +namespace Favorites +{ + void init() + { + // cycle through favorites + int favcount = Cvar( 'favorites', '0', ::CVAR_ARCHIVE ).integer; + for( int i = 1; i <= favcount; i++ ) + ::serverBrowser.addFavorite( Cvar( 'favorite_' + i, '', ::CVAR_ARCHIVE ).string ); // inform UI of the favorites + } + + void add( const String &address ) + { + if( address.empty() ) { + return; + } + if( !::serverBrowser.addFavorite( address ) ) { + // duplicate entry, or maybe some other error + return; + } + + Cvar favorites( 'favorites', '0', ::CVAR_ARCHIVE ); + int favcount = favorites.integer; + + Cvar favorite( 'favorite_' + (favcount + 1), '', ::CVAR_ARCHIVE ); + favorite.set( address ); + + // update counter + favorites.set( favcount + 1 ); + } + + void remove( const String &address ) + { + if( address.empty() ) { + return; + } + if( !::serverBrowser.removeFavorite( address ) ) { + // no such server in favorites or some other error? + return; + } + + int favslot = getSlot( address ); + if( favslot == 0 ) + return; + + // replace this entry with the last one as we don't care about the + // order we store favorites in + Cvar favorites( 'favorites', '0', ::CVAR_ARCHIVE ); + int favcount = favorites.integer; + + Cvar favlast( 'favorite_' + favcount, '', ::CVAR_ARCHIVE ); + Cvar favorite( 'favorite_' + favslot, '', ::CVAR_ARCHIVE ); + + favorite.set( favlast.string ); + favlast.set( '' ); + + // update counter + favorites.set( favcount - 1 ); + } + + int getSlot( const String &address ) + { + int favcount = Cvar( 'favorites', '', ::CVAR_ARCHIVE ).integer; + for( int i = 1; i <= favcount; i++ ) + { + if( Cvar( 'favorite_' + i, '', ::CVAR_ARCHIVE ).string == address ) + return i; + } + return 0; + } +} diff --git a/assets/data0_21pure/ui/testui/as/fuzzy_search.as b/assets/data0_21pure/ui/testui/as/fuzzy_search.as new file mode 100644 index 0000000000..4a5fb42666 --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/fuzzy_search.as @@ -0,0 +1,104 @@ +String fuzzySearch_savedQuery = ""; + +bool fuzzySearch_contains( const String &in text, const String &in query ) +{ + if( query.empty() ) { + return true; + } + if( text.empty() ) { + return false; + } + String lowerText = text.tolower(); + String lowerQuery = query.tolower(); + uint pos = lowerText.locate( lowerQuery, 0 ); + return pos < lowerText.length(); +} + +bool fuzzySearch_match( const String &in text, const String &in query ) +{ + if( query.empty() ) { + return true; + } + if( text.empty() ) { + return false; + } + array @terms = StringUtils::Split( query.tolower(), " " ); + String lowerText = text.tolower(); + for( uint i = 0; i < terms.length(); i++ ) { + if( terms[i].empty() ) { + continue; + } + uint pos = lowerText.locate( terms[i], 0 ); + if( pos >= lowerText.length() ) { + return false; + } + } + return true; +} + +String fuzzySearch_buildText( DataSource @data, const String &in table, int rowIndex, array @fields ) +{ + String combined; + for( uint i = 0; i < fields.length(); i++ ) { + if( i > 0 ) { + combined += " "; + } + combined += data.getField( table, rowIndex, fields[i] ); + } + return combined; +} + +void fuzzySearch_filterDatagrid( ElementDataGrid @grid, const String &in sourceName, const String &in table, + const String &in query, array @fields ) +{ + if( @grid == null ) { + return; + } + DataSource @data = getDataSource( sourceName ); + if( @data == null ) { + return; + } + uint numRows = grid.getNumRows(); + for( uint i = 0; i < numRows; i++ ) { + Element @row = grid.getRow( i ); + if( @row == null ) { + continue; + } + String searchText = fuzzySearch_buildText( data, table, int( i ), fields ); + if( fuzzySearch_match( searchText, query ) ) { + row.css( "display", "block" ); + } else { + row.css( "display", "none" ); + } + } +} + +void fuzzySearch_saveQuery( const String &in query ) +{ + fuzzySearch_savedQuery = query; +} + +String fuzzySearch_loadQuery() +{ + return fuzzySearch_savedQuery; +} + +void fuzzySearch_filterDivElementChildren( Element @container, const String &in query ) +{ + if( @container == null ) { + return; + } + uint numChildren = container.getNumChildren(); + for( uint i = 0; i < numChildren; i++ ) { + Element @child = container.getChild( i ); + if( @child == null ) { + continue; + } + String text = child.getInnerRML(); + if( fuzzySearch_match( text, query ) ) { + child.css( "display", "block" ); + } else { + child.css( "display", "none" ); + } + } +} diff --git a/assets/data0_21pure/ui/testui/as/modal.as b/assets/data0_21pure/ui/testui/as/modal.as new file mode 100644 index 0000000000..0a371b8f99 --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/modal.as @@ -0,0 +1,12 @@ +void setModalY( float y ) +{ + Element @body = @window.document.body; + + Element @abs = @body.getElementById( 'modal-block-abs' ); + Element @rel = @body.getElementById( 'modal-block-rel' ); + if( @abs == null || @rel == null ) { + return; + } + abs.css( 'top', String( ( body.clientHeight() - abs.clientHeight() ) * y ) + 'px' ); + rel.css( 'top', '0px' ); +} diff --git a/assets/data0_21pure/ui/testui/as/model_setup.as b/assets/data0_21pure/ui/testui/as/model_setup.as new file mode 100644 index 0000000000..85c3c8beda --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/model_setup.as @@ -0,0 +1,256 @@ +/* +Copyright (C) 2011 Cervesato Andrea ("koochi") + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +const String DEFAULT_MODEL_SKIN = 'default'; + +/* + This class provides to manage the Warsow's Player Setup + and the Team Setup section. + */ +class ModelSetup +{ + // cvar + Cvar cModel; + Cvar cSkin; + Cvar cColor; + + // elements id + String modelViewId; + String skinId; + String colorId; + + // model + ModelView mView; + int oldModel; + int currentModel; + int numberOfModels; + + // if true, cvars are set in realtime + bool realtime; + + ModelSetup( String modelViewId, String skinId, String colorId, + String modelCvar, String skinCvar, String colorCvar, + bool realtime ) + { + this.modelViewId = modelViewId; + this.skinId = skinId; + this.colorId = colorId; + + Cvar cM( modelCvar, "bigvic", 0 ); + Cvar cS( skinCvar, "default", 0 ); + Cvar cC( colorCvar, "255 255 255", 0 ); + + this.cModel = cM; + this.cSkin = cS; + this.cColor = cC; + + this.realtime = realtime; + } + + ~ModelSetup( void ) + { + } + + // this must be called after constructor + void InitializeModelSetup( Element @elem ) + { + String model = cModel.string; + + // model + DataSource @data = getDataSource( 'models' ); + numberOfModels = data.numRows( 'list' ); + + // find the current model in the data source + for( int i = 0; i < numberOfModels; i++ ) + { + if( model == data.getField( 'list', i, 'name' ) ) + { + oldModel = currentModel = i; + break; + } + } + + // model view decorator + ModelView m( modelViewId ); + m.Initialize( @elem , model, /*cSkin.string*/DEFAULT_MODEL_SKIN ); + m.SetYRotationSpeed( @elem, '220' ); + m.SetScale( @elem, '0.9' ); + m.SetFovX( @elem, 'auto' ); + m.SetFovY( @elem, '30' ); + mView = m; + + // reset elements and model view + Reset( @elem ); + + SetColor( @elem ); + } + + String getCurrentModel( void ) + { + DataSource @data = getDataSource( 'models' ); + return data.getField( 'list', currentModel, 'name' ); + } + + String getOldModel( void ) + { + DataSource @data = getDataSource( 'models' ); + return data.getField( 'list', oldModel, 'name' ); + } + + void UpdateModel( Element @elem ) + { + String model = getCurrentModel(); + mView.SetModel( elem, model ); + } + + //=========================== + // called by rocket elements + //=========================== + void SetSkin( Element @elem ) + { + Element @skinElement = elem.getElementById( skinId ); + if( @skinElement == null ) + return; + + String skin = DEFAULT_MODEL_SKIN; + + if( skinElement.hasAttr( 'checked' ) ) + skin = 'fullbright'; + + mView.SetSkin( @elem, skin ); + + if( realtime ) + cSkin.set( skin ); + } + + void SetColor( Element @elem ) + { + ElementFormControl @colorElement = elem.getElementById( colorId ); + if( @colorElement == null ) + return; + + String color = colorElement.value; + + String colorHex = "#", outlineHex = "#"; + array @components = StringUtils::Split( color, " " ); + uint count = 0; + uint components_size = components.size(); + for( uint i = 0; i < components_size; i++ ) { + int component = components[i].toInt(); + colorHex += StringUtils::FormatInt( component, '0h', 2 ); + outlineHex += StringUtils::FormatInt( int( float( component ) * 0.25f ), '0h', 2 ); + count++; + if( count == 3 ) { + break; + } + } + mView.SetShaderColor( @elem, colorHex ); + mView.SetOutlineColor( @elem, outlineHex ); + + if( realtime ) + cColor.set( color ); + } + + void SelectPrevModel( Element @elem ) + { + if( currentModel == 0 ) + currentModel = numberOfModels-1; + else + currentModel--; + + UpdateModel( @elem ); + + if( realtime ) + { + String model = getCurrentModel(); + cModel.set( model ); + oldModel = currentModel; + } + } + + void SelectNextModel( Element @elem ) + { + currentModel++; + currentModel %= numberOfModels; + + UpdateModel( @elem ); + + if( realtime ) + { + String model = getCurrentModel(); + cModel.set( model ); + oldModel = currentModel; + } + } + + //=========================== + // called on submit + //=========================== + + // Reset elements and model view to the old values + void Reset( Element @elem ) + { + // ===== model ===== + String oldM = getOldModel(); + mView.SetModel( @elem, oldM ); + currentModel = oldModel; + + + // ===== skin ===== + Element @skinElement = elem.getElementById( skinId ); + if( @skinElement == null ) + return; + + String oldSkin = cSkin.string; + + // koochi: libRocket accept 2 checked for 1 checkbox + if( skinElement.hasAttr( 'checked' ) ) + skinElement.removeAttr( 'checked' ); + + if( oldSkin == 'fullbright' ) + skinElement.setAttr( 'checked', '1' ); + + mView.SetSkin( @elem, oldSkin ); + + // ====== color ====== + mView.SetShaderColor( @elem, RGB2Hex( cColor.string ) ); + } + + void Fix( Element @elem ) + { + // ===== model ===== + String model = getCurrentModel(); + cModel.set( model ); + oldModel = currentModel; + + + // ===== skin ===== + Element @skinElement = elem.getElementById( skinId ); + if( @skinElement == null ) + return; + + String skin = DEFAULT_MODEL_SKIN; + + if( skinElement.hasAttr( 'checked' ) ) + skin = 'fullbright'; + + cSkin.set( skin ); + } +} \ No newline at end of file diff --git a/assets/data0_21pure/ui/testui/as/modelview.as b/assets/data0_21pure/ui/testui/as/modelview.as new file mode 100644 index 0000000000..5810caf141 --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/modelview.as @@ -0,0 +1,270 @@ +/* +Copyright (C) 2011 Cervesato Andrea ("koochi") + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +/* + This class provide to manage the model view properties. + */ +String basePath = 'models/players/'; +String modelFile = 'tris.iqm'; +String skinExt = '.skin'; + +funcdef bool MVIEW_ANIM_CALLBACK(any &input); + +class ModelView +{ + String modelId; + String modelType; + String modelSkin; + + /* These are css settings for modelview */ + /* + String modelPath; + String skinPath; + int fov; + int scale; + int outlineHeight; + int outlineColor; + String shaderColor; + int xRotation; + int yRotation; + int zRotation; + int xRotationSpeed; + int yRotationSpeed; + int zRotationSpeed; + */ + + ModelView( String modelId ) + { + this.modelId = modelId; + } + + void Initialize( Element @elem, const String &modelType, const String &skin ) + { + this.modelType = modelType; + this.modelSkin = skin; + + // initialize model and skin + SetModel( @elem, modelType ); + SetSkin( @elem, skin ); + } + + Element@ GetModel( Element @elem ) + { + return elem.getElementById( modelId ); + } + + // refresh the view + void Refresh( Element @elem ) + { + SetModel( @elem, this.modelType ); + SetSkin( @elem, this.modelSkin ); + } + + // Set the model + void SetModel( Element @elem, const String &modelType ) + { + Element @model = GetModel( @elem ); + + if( @model != null && modelType.length() != 0 ) + { + // save model type + this.modelType = modelType; + + String modelPath = basePath + this.modelType + '/' + modelFile; + + model.setProp( 'model-scale', "0.0" ); + MVIEW_ANIM_CALLBACK @animate = MVIEW_ANIM_CALLBACK(this.__AnimModelScale); + window.setInterval( animate, 10, any(@model) ); + + // skin looks bad when model changes + SetSkin( @elem, this.modelSkin ); + + if( !model.setProp( 'model-modelpath', modelPath ) ) + console.warn( "ModelView: modelpath parsing failed\n" ); + } + } + + bool __AnimModelScale(any &obj) + { + Element @model; + obj.retrieve(@model); + + float scale = model.getProp( 'model-scale' ).toFloat() + 0.05; + if( scale >= 1.0 ) { + model.setProp( 'model-scale', "1.0" ); + return false; + } + model.setProp( 'model-scale', "" + scale ); + return true; + } + + // Set the model's skin + void SetSkin( Element @elem, const String &skin ) + { + Element @model = GetModel( @elem ); + + if( @model != null ) + { + if( skin.empty() ) { + this.modelSkin = 'default'; + } else { + // save current skin for future refresh + this.modelSkin = skin; + } + + String skinPath = basePath + this.modelType + '/' + this.modelSkin + skinExt; + + if( !model.setProp( 'model-skinpath', skinPath ) ) + console.warn("ModelView: skinpath parsing failed\n"); + } + } + + // Set the horizontal fov to use for the model + void SetFovX( Element @elem, const String &fov ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !fov.empty() ) + { + model.setProp( 'model-fov-x', fov ); + } + } + + // Set the vertical fov to use for the model + void SetFovY( Element @elem, const String &fov ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !fov.empty() ) + { + model.setProp( 'model-fov-y', fov ); + } + } + + // Set the model's scaling factor + void SetScale( Element @elem, const String &scale ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !scale.empty() ) + { + model.setProp( 'model-scale', scale ); + } + } + + // Set the height of the model's outlines + void SetOutlineHeight( Element @elem, const String &height ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !height.empty() ) + { + model.setProp( 'model-outline-height', height ); + } + } + + // color in hex form: #f0f0f0f0 + void SetOutlineColor( Element @elem, const String &color ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !color.empty() ) + { + model.setProp( 'model-outline-color', color ); + } + } + + // color in hex form: #f0f0f0f0 + void SetShaderColor( Element @elem, const String &color ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !color.empty() ) + { + model.setProp( 'model-shader-color', color ); + } + } + + // model's x axis rotation + void XRotation( Element @elem, const String &rotation ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !rotation.empty() ) + { + model.setProp( 'model-rotation-pitch', rotation ); + } + } + + // model's y axis rotation + void YRotation( Element @elem, const String &rotation ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !rotation.empty() ) + { + model.setProp( 'model-rotation-yaw', rotation ); + } + } + + // model's z axis rotation + void ZRotation( Element @elem, const String &rotation ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !rotation.empty() ) + { + model.setProp( 'model-rotation-roll', rotation ); + } + } + + // set the model's x axis rotation speed + void SetXRotationSpeed( Element @elem, const String &speed ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !speed.empty() ) + { + model.setProp( 'model-rotation-speed-pitch', speed ); + } + } + + // set the model's y axis rotation speed + void SetYRotationSpeed( Element @elem, const String &speed ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !speed.empty() ) + { + model.setProp( 'model-rotation-speed-yaw', speed ); + } + } + + // set the model's z axis rotation speed + void SetZRotationSpeed( Element @elem, const String &speed ) + { + Element @model = GetModel( @elem ); + + if( @model != null && !speed.empty() ) + { + model.setProp( 'model-rotation-speed-roll', speed ); + } + } +} \ No newline at end of file diff --git a/assets/data0_21pure/ui/testui/as/popup.as b/assets/data0_21pure/ui/testui/as/popup.as new file mode 100644 index 0000000000..7f43dd1488 --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/popup.as @@ -0,0 +1,141 @@ +/* notification bar */ +class NotificationPopup +{ + float alpha; + uint timerStart; + uint timerUpdate; + uint timerStayDuration; + Element @notbar; + + NotificationPopup( const String &in id, const String &in text, uint stayTime, const String &in css_class = ".ok" ) + { + // FIXME: cache this somehow? + @this.notbar = window.document.body.getElementById( id ); + + this.init( text, stayTime, css_class ); + } + + ~NotificationPopup() + { + @this.notbar = null; + } + + private void init( const String &in text, uint stayTime, const String &in css_class ) + { + Element @notbar = @this.notbar; + + if( @notbar == null ) { + return; + } + if ( text.length() == 0 ) { + // empty text + return; + } + if( stayTime < 0 ) // wtf? + return; + + timerStayDuration = stayTime; + + alpha = 0.0; + timerStart = timerUpdate = window.time; + + /* set visibility and text */ + notbar.css( 'display', 'block' ).addClass( css_class ); + notbar.setInnerRML( text ); + + window.setInterval( NotificationPopupCallbacks::FadeInCallback, 10, any(@this) ); + } + + bool fadeIn() + { + alpha += (window.time - timerUpdate) * 0.012; // fadein should be really fast + timerUpdate = window.time; + + bool enabled = (alpha < 1.0); + if( !enabled ) { + alpha = 1.0; + window.setInterval( NotificationPopupCallbacks::StartFadeCallback, timerStayDuration, any(@this) ); + } + + this.updateNotBarColorAlpha( 'color' ); + this.updateNotBarColorAlpha( 'background-color' ); + + if( !enabled ) + return false; + return true; + } + + void startFade() + { + timerStart = timerUpdate = window.time; + window.setInterval( NotificationPopupCallbacks::FadeCallback, 10, any(@this) ); + } + + bool fade() + { + Element @notbar = @this.notbar; + + if( @notbar == null ) { + return false; + } + + bool enabled; + + /* alpha fade background and font colors, preserving RGB values */ + alpha -= (window.time - timerUpdate) * 0.001; + timerUpdate = window.time; + + enabled = (alpha > 0.0); + if( !enabled ) { + /* DONE */ + alpha = 0.0; + notbar.css( 'display', 'none' ); + return false; + } + + this.updateNotBarColorAlpha( 'color' ); + this.updateNotBarColorAlpha( 'background-color' ); + return true; + } + + private void updateNotBarColorAlpha( const String &in colorProperty ) + { + Element @notbar = @this.notbar; + + /* get current color value */ + String color = notbar.getProp( colorProperty ); + + /* strip alpha from 'R, G, B, A' output */ + String rgb = color.subString( 0, color.locate( ',', 2 ) ); + String rgba = rgb + ', ' + (255 * alpha); + + /* set new value */ + notbar.css( colorProperty, 'rgba(' + rgba + ')' ); + } +}; + +/* FIXME: move this to NotificationPopup as a public static method */ +namespace NotificationPopupCallbacks +{ + bool FadeInCallback( any & obj ) + { + NotificationPopup @popup; + obj.retrieve(@popup); + return popup.fadeIn(); + } + + bool StartFadeCallback( any & obj ) + { + NotificationPopup @popup; + obj.retrieve(@popup); + popup.startFade(); + return false; + } + + bool FadeCallback( any & obj ) + { + NotificationPopup @popup; + obj.retrieve(@popup); + return popup.fade(); + } +} diff --git a/assets/data0_21pure/ui/testui/as/tutorial.as b/assets/data0_21pure/ui/testui/as/tutorial.as new file mode 100644 index 0000000000..0ae4ff9247 --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/tutorial.as @@ -0,0 +1,61 @@ +/* +Copyright (C) 2015 Chasseur de bots + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +namespace Tutorial +{ + +void OpenModal1() +{ + Cvar ui_tutorial_launch( 'ui_tutorial_launch', '0', 0 ); + Cvar ui_tutorial_taken( 'ui_tutorial_taken', '0', ::CVAR_ARCHIVE ); + int taken = ui_tutorial_launch.integer | ui_tutorial_taken.integer; + + if( taken == 0 ) { + // don't ask again this game launch + ui_tutorial_launch.set( '1' ); + + String text = "%3Ch3%3EWelcome%20to%20Warfork%21%3C%2Fh3%3E"; + + text += "%3Ct%3EWarfork+is+an+Early+Access+game+currently+in+development.+All+maps%2C+models%2C+textures%2C+etc.+are+in+the+process+of+being+recreated.+In+the+interim+there+will+be+no+marketing+and+player+population+will+remain+low.+We+invite+you+to+join+us+on+Warfork+Wednesdays+%28Europe%29+and+Fork+Fridays+%28North+America%29.+Please+join+chat+for+more+information%21%3C%2Ft%3E"; + window.modal( "modal_basic.rml?text=" + text + "&other=Don't%20tell%20me%20again&ypos=0.5" ); + + int val = window.getModalValue(); + if( val == 0 ) { + ui_tutorial_taken.set( '1' ); + return; + } + + if( val == 1 ) { + // no/cancel + } + else if( val == 2 ) { + ui_tutorial_taken.set( '2' ); + } + } +} + +void OpenModal2() +{ + window.modal('modal_basic.rml?text=You%20are%20about%20to%20launch%20the%20tutorial%20stage%20of%20Warfork.%20Continue%3F&ypos=0.25'); + if (window.getModalValue() == 0) + game.execAppend('map wftutorial1\n'); +} + +} diff --git a/assets/data0_21pure/ui/testui/as/video_setup.as b/assets/data0_21pure/ui/testui/as/video_setup.as new file mode 100644 index 0000000000..495f2de861 --- /dev/null +++ b/assets/data0_21pure/ui/testui/as/video_setup.as @@ -0,0 +1,436 @@ +/* +Copyright (C) 2011 Cervesato Andrea ("koochi") + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +/* + This class provides to manage some cvars into the + Graphics section. + */ + +class VideoSetup +{ + // use to recognize changes and inhibit vid_restart cmd + bool allowVidRestart; + + // whether video mode-related options should be shown + bool showVideoFrame; + + // video profile + Cvar ui_video_profile( "ui_video_profile", "medium", CVAR_ARCHIVE ); + + // video mode + Cvar vid_width( "vid_width", "0", 0 ); + Cvar vid_height( "vid_height", "0", 0 ); + + // vertical sync + Cvar r_swapinterval_min( "r_swapinterval_min", "0", 0 ); + + // multithreading + Cvar r_multithreading( "r_multithreading", "1", 0 ); + + // renderer maxfps + Cvar r_maxfps( "r_maxfps", "250", 0 ); + + // quality of textures + Cvar r_picmip( "r_picmip", "0", 0 ); + + // filtering + Cvar r_texturefilter( "r_texturefilter", "4", 0 ); // 0==1, 2, 4, .. values + Cvar r_texturefilter_max( "r_texturefilter_max", "0", 0 ); + Cvar r_soft_particles_available( "r_soft_particles_available", "0", 0 ); + + // lighting + Cvar r_lighting_vertexlight( "r_lighting_vertexlight", "0", 0 ); + Cvar r_lighting_deluxemapping( "r_lighting_deluxemapping", "1", 0 ); + Cvar ui_lighting( "ui_lighting", "2", 0 ); // used to store index of selector + + // shadows + Cvar cg_shadows( "cg_shadows", "1", CVAR_ARCHIVE ); + + // ids + String idProfile; + String idVideoFrame; + String idMode; + String idModeFrame; + String idFullscreenFrame; + String idVsyncFrame; + String idRMaxFpsFrame; + String idClMaxFpsFrame; + String idGammaFrame; + String idPicmip; + String idPicmipFrame; + String idFiltering; + String idFilteringFrame; + String idLighting; + String idSoftParticlesFrame; + + VideoSetup( Element @elem, + const String &idProfile, + const String &idVideoFrame, + const String &idMode, const String &idModeFrame, + const String &idFullscreenFrame, const String &idVsyncFrame, + const String &idRMaxFpsFrame, const String &idClMaxFpsFrame, + const String &idGammaFrame, + const String &idPicmip, const String &idPicmipFrame, + const String &idFiltering, + const String &idFilteringFrame, + const String &idLighting, + const String &idSoftParticlesFrame ) + { + this.idProfile = idProfile; + this.idVideoFrame = idVideoFrame; + this.idMode = idMode; + this.idModeFrame = idModeFrame; + this.idFullscreenFrame = idFullscreenFrame; + this.idVsyncFrame = idVsyncFrame; + this.idRMaxFpsFrame = idRMaxFpsFrame; + this.idClMaxFpsFrame = idClMaxFpsFrame; + this.idGammaFrame = idGammaFrame; + this.idPicmip = idPicmip; + this.idPicmipFrame = idPicmipFrame; + this.idFiltering = idFiltering; + this.idFilteringFrame = idFilteringFrame; + this.idLighting = idLighting; + this.idSoftParticlesFrame = idSoftParticlesFrame; + + // We only have 3 choices in lightning listbox: + // vertex lighting: lighting_vertexlight = 1, lighting_deluxemapping = 0 + // lightmaps: lighting_vertexlight = 0, lighting_deluxemapping = 0 + // per-pixel lighting: lighting_vertexlight = 0, lighting_deluxemapping = 1 + // since the variant lighting_vertexlight = 1, lighting_deluxemapping = 1 is + // equivalent to vertex lighting. + if( r_lighting_vertexlight.value == 1 ) + ui_lighting.set( 0 ); + else if( r_lighting_deluxemapping.value == 1 ) + ui_lighting.set( 2 ); + else + ui_lighting.set( 1 ); + + Initialize( @elem ); + } + + ~VideoSetup() + { + } + + void Initialize( Element @elem ) + { + showVideoFrame = false; + + PopulateModeSelector( @elem ); + PopulateFilteringSelector( @elem ); + CheckFullscreenAvailability( @elem ); + CheckVsyncAvailability( @elem ); + CheckMultiThreadingAvailability( @elem ); + CheckGammaAvailability( @elem ); + CheckSoftParticlesAvailability( @elem ); + + if( !showVideoFrame ) // all video options are hidden, so hide the separator as well + { + Element @frame = elem.getElementById( idVideoFrame ); + if( @frame != null ) + frame.css( 'display', 'none' ); + } + + // reset elements + SelectGraphicsProfile( @elem, true ); + Reset( @elem ); + } + + void PopulateModeSelector( Element @elem ) + { + Element @selector = elem.getElementById( idMode ); + if( @selector == null ) + return; + + DataSource @data = getDataSource( 'video' ); + int numberOfModes = data.numRows( 'list' ); + String selected = vid_width.string + ' x ' + vid_height.string; + String mode; + + String rml = ''; + for( int i = 0; i < numberOfModes; i++ ) + { + mode = data.getField( 'list', i, 'resolution' ); + rml += ''; + } + selector.setInnerRML( rml ); + + if( numberOfModes <= 1 ) + { + Element @frame = elem.getElementById( idModeFrame ); + if( @frame != null ) + frame.css( 'display', 'none' ); + } + else + { + showVideoFrame = true; + } + } + + void PopulateFilteringSelector( Element @elem ) + { + Element @selector = elem.getElementById( idFiltering ); + + if( @selector == null ) + return; + + int max = r_texturefilter_max.integer; + + String rml = ''; + for( int i = 2; i <= max; i*=2 ) + rml += ''; + + selector.setInnerRML( rml ); + + if( max <= 1 ) + { + Element @frame = elem.getElementById( idFilteringFrame ); + if( @frame != null ) + frame.css( 'display', 'none' ); + } + } + + void CheckFullscreenAvailability( Element @elem ) + { + showVideoFrame = true; + } + + void CheckVsyncAvailability( Element @elem ) + { + if( r_swapinterval_min.integer != 0 ) + { + Element @frame = elem.getElementById( idVsyncFrame ); + if ( @frame != null ) + frame.css( 'display', 'none' ); + } + else + { + showVideoFrame = true; + } + } + + void CheckMultiThreadingAvailability( Element @elem ) + { + Element @frame; + + if( r_swapinterval_min.integer != 0 ) + { + @frame = elem.getElementById( idClMaxFpsFrame ); + if ( @frame != null ) + frame.css( 'display', 'none' ); + + @frame = elem.getElementById( idRMaxFpsFrame ); + if ( @frame != null ) + frame.css( 'display', 'none' ); + } + else + { + showVideoFrame = true; + + if( r_multithreading.integer != 0 ) + { + @frame = elem.getElementById( idClMaxFpsFrame ); + if ( @frame != null ) + frame.css( 'display', 'none' ); + } + else + { + @frame = elem.getElementById( idRMaxFpsFrame ); + if ( @frame != null ) + frame.css( 'display', 'none' ); + } + } + } + + void CheckGammaAvailability( Element @elem ) + { + showVideoFrame = true; + } + + void CheckSoftParticlesAvailability( Element @elem ) + { + if( r_soft_particles_available.integer == 0 ) + { + Element @frame = elem.getElementById( idSoftParticlesFrame ); + if ( @frame != null ) + frame.css( 'display', 'none' ); + } + } + + void SelectGraphicsProfile( Element @elem, bool reset ) + { + ElementFormControl @profile = elem.getElementById( idProfile ); + + if( @profile == null ) + return; + + String gfx; + + if( reset ) + { + gfx = ui_video_profile.string; + profile.value = gfx; + } + else + { + gfx = profile.value; + game.execAppend( "exec profiles/" + gfx + "\n" ); + ui_video_profile.set( gfx ); + + allowVidRestart = false; + } + } + + void SetMode( Element @elem, bool reset ) + { + ElementFormControl @mode = elem.getElementById( idMode ); + if( @mode == null ) + return; + + if( reset ) + { + mode.value = vid_width.string + ' x ' + vid_height.string; + } + else + { + array resolution = StringUtils::Split( mode.value, ' x ' ); + if( resolution.size() < 2 ) + return; + + vid_width.set( resolution[0] ); + vid_height.set( resolution[1] ); + Changed(); + } + } + + void SetPicmip( Element @elem, bool reset ) + { + Element @picmip_el = elem.getElementById( idPicmip ); + ElementFormControl @picmip = @picmip_el; + Element @picmip_frame = elem.getElementById( idPicmipFrame ); + + if( @picmip == null ) + return; + + int maxvalue = picmip_el.getAttr( "max", "0" ).toInt(); + + if( reset ) + { + if( @picmip_frame != null ) + picmip_frame.css( 'display', ( r_picmip.integer <= maxvalue ) ? 'block' : 'none' ); + picmip.value = String( maxvalue - r_picmip.integer ); + } + else + { + r_picmip.set( maxvalue - picmip.value.toInt() ); + Changed(); + } + } + + void SetFiltering( Element @elem, bool reset ) + { + ElementFormControl @filter = elem.getElementById( idFiltering ); + + if( @filter == null ) + return; + + if( reset ) + { + int value = r_texturefilter.integer; + int max = r_texturefilter_max.integer; + if( value > max ) + value = max; + if( value < 1 ) + value = 1; + filter.value = String( value ); + } + else + { + r_texturefilter.set( filter.value.toInt() ); + Changed(); + } + } + + void SetLighting( Element @elem, bool reset ) + { + ElementFormControl @slideLighting = elem.getElementById( idLighting ); + + if( @slideLighting == null ) + return; + + if( reset ) + { + slideLighting.value = ui_lighting.string; + } + else + { + int value = slideLighting.value.toInt(); + + switch( value ) + { + case 0: + r_lighting_vertexlight.set("1"); + r_lighting_deluxemapping.set("0"); + break; + case 1: + r_lighting_vertexlight.set("0"); + r_lighting_deluxemapping.set("0"); + break; + default: // 2 + r_lighting_vertexlight.set("0"); + r_lighting_deluxemapping.set("1"); + break; + } + + ui_lighting.set( value ); + Changed(); + } + } + + void Changed( void ) + { + allowVidRestart = true; + } + + void Reset( Element @elem ) + { + SetMode( @elem, true ); + SetPicmip( @elem, true ); + SetFiltering( @elem, true ); + SetLighting( @elem, true ); + + // cvars are not changed + allowVidRestart = false; + } + + void Apply( Element @elem ) + { + // apply changes if something changed + if( allowVidRestart ) + { + SetMode( @elem, false ); + SetPicmip( @elem, false ); + SetFiltering( @elem, false ); + SetLighting( @elem, false ); + + game.execAppend ( "vid_restart\n" ); + } + } +} diff --git a/assets/data0_21pure/ui/testui/blocklist.rml b/assets/data0_21pure/ui/testui/blocklist.rml new file mode 100644 index 0000000000..131d005030 --- /dev/null +++ b/assets/data0_21pure/ui/testui/blocklist.rml @@ -0,0 +1,226 @@ + + + + + players + + + + + + + + diff --git a/assets/data0_21pure/ui/testui/callvotes.rml b/assets/data0_21pure/ui/testui/callvotes.rml new file mode 100644 index 0000000000..13101004f3 --- /dev/null +++ b/assets/data0_21pure/ui/testui/callvotes.rml @@ -0,0 +1,303 @@ + + + + + + callvotes + + + + + + +