├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── LICENSE ├── Makefile ├── Makefile.win32 ├── README.md ├── addrinfolist.cc ├── addrinfolist.h ├── client ├── Makefile └── testclient.cc ├── configuration.cc ├── configuration.h ├── exception.cc ├── exception.h ├── game_event.proto ├── gamecontroller.cc ├── gamecontroller.h ├── installDeps.sh ├── keypad.png ├── keypad.xfig ├── legacypublisher.cc ├── legacypublisher.h ├── logger.cc ├── logger.h ├── main.cc ├── mainwindow.cc ├── mainwindow.h ├── noncopyable.h ├── protobufpublisher.cc ├── protobufpublisher.h ├── publisher.h ├── rcon-client ├── Makefile └── rconclient.cc ├── rcon.proto ├── rconsrv.cc ├── rconsrv.h ├── referee.conf ├── referee.proto ├── savegame.cc ├── savegame.h ├── savestate.proto ├── scoreboard ├── Makefile ├── addrinfolist.cc ├── addrinfolist.h ├── exception.cc ├── exception.h ├── flags │ ├── EMEnents.png │ ├── ER-Force.png │ ├── IRSS Deluxe.png │ ├── Immortals.png │ ├── KIKS.png │ ├── MCT Susano Logics.png │ ├── MRL.png │ ├── NEUIslanders.png │ ├── ODENS.png │ ├── RFC Cambridge.png │ ├── RoboDragons.png │ ├── RoboFEI.png │ ├── RoboIME.png │ ├── RoboJackets.png │ ├── STOx's.png │ ├── Tigers Mannheim.png │ ├── UBC Thunderbots.png │ └── Warthog Robotics.png ├── gamestate.cc ├── gamestate.h ├── imagedb.cc ├── imagedb.h ├── logos │ ├── CMDragons.png │ ├── EMEnents.png │ ├── ER-Force.png │ ├── IRSS Deluxe.png │ ├── Immortals.png │ ├── KIKS.png │ ├── MCT Susano Logics.png │ ├── MRL.png │ ├── NEUIslanders.png │ ├── ODENS.png │ ├── Parsian.png │ ├── RFC Cambridge.png │ ├── RoboDragons.png │ ├── RoboFEI.png │ ├── RoboIME.png │ ├── RoboJackets.png │ ├── STOx's.png │ ├── Tigers Mannheim.png │ ├── UBC Thunderbots.png │ └── Warthog Robotics.png ├── main.cc ├── mainwindow.cc ├── mainwindow.h ├── noncopyable.h ├── scoreboard.conf ├── socket.cc └── socket.h ├── socket.cc ├── socket.h ├── sslrefbox.desktop ├── teams.cc ├── teams.h ├── timing.cc ├── timing.h ├── udpbroadcast.cc └── udpbroadcast.h /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | client/testclient 3 | rcon-client/rconclient 4 | scoreboard/scoreboard 5 | *.o 6 | *.pb.cc 7 | *.pb.h 8 | referee.log 9 | referee.sav 10 | sslrefbox 11 | tags 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | os: linux 3 | compiler: 4 | - gcc 5 | 6 | before_install: 7 | - sudo apt-get update -qq 8 | - sudo ./installDeps.sh 9 | 10 | matrix: 11 | include: 12 | - sudo: required 13 | dist: trusty 14 | 15 | script: make -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.6) 2 | project(ssl_refbox) 3 | 4 | add_definitions(-std=gnu++0x -Wall -Wextra -Wundef -O2 -g) 5 | add_definitions(-DPROTOBUF_INLINE_NOT_IN_HEADERS=0) 6 | 7 | SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}) 8 | 9 | find_package(PkgConfig) 10 | pkg_check_modules(GTKMM gtkmm-2.4) 11 | link_directories(${GTKMM_LIBRARY_DIRS}) 12 | include_directories(${GTKMM_INCLUDE_DIRS}) 13 | 14 | find_package(Protobuf REQUIRED) 15 | if (EXISTS ${PROTOBUF_PROTOC_EXECUTABLE}) 16 | message(STATUS "Found PROTOBUF Compiler: ${PROTOBUF_PROTOC_EXECUTABLE}") 17 | else () 18 | message(FATAL_ERROR "Could not find PROTOBUF Compiler") 19 | endif () 20 | protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS game_event.proto rcon.proto referee.proto savestate.proto) 21 | 22 | include_directories( 23 | ${PROJECT_BINARY_DIR} 24 | ${PROTOBUF_INCLUDE_DIRS} 25 | ${PROJECT_SOURCE_DIR} 26 | ) 27 | 28 | set(SOURCE_FILES 29 | addrinfolist.cc 30 | configuration.cc 31 | exception.cc 32 | gamecontroller.cc 33 | legacypublisher.cc 34 | logger.cc 35 | main.cc 36 | mainwindow.cc 37 | protobufpublisher.cc 38 | rconsrv.cc 39 | savegame.cc 40 | socket.cc 41 | teams.cc 42 | timing.cc 43 | udpbroadcast.cc) 44 | 45 | add_executable(sslrefbox ${SOURCE_FILES} ${PROTO_SRCS} ${PROTO_HDRS}) 46 | target_link_libraries(sslrefbox ${GTKMM_LIBRARIES} ${PROTOBUF_LIBRARIES}) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | buildDir=build 2 | #buildDir=build_debug 3 | 4 | #change to Debug for debug mode 5 | buildType=Release 6 | #buildType=Debug 7 | 8 | all: build_cmake 9 | 10 | $(buildDir)/CMakeLists.txt.copy: CMakeLists.txt 11 | mkdir -p $(buildDir) && cd $(buildDir) && cmake -DCMAKE_BUILD_TYPE=$(buildType) .. && \ 12 | cp ../CMakeLists.txt ./CMakeLists.txt.copy 13 | 14 | build_cmake: $(buildDir)/CMakeLists.txt.copy 15 | $(MAKE) -C $(buildDir) 16 | 17 | clean: 18 | $(MAKE) -C $(buildDir) clean 19 | rm *.o *.pb.h *.pb.cc 20 | 21 | cleanup_cache: 22 | rm -rf $(buildDir) && mkdir $(buildDir) 23 | 24 | -------------------------------------------------------------------------------- /Makefile.win32: -------------------------------------------------------------------------------- 1 | # 2 | # /* LICENSE: ========================================================================= 3 | # RoboCup F180 Referee Box Source Code Release 4 | # ------------------------------------------------------------------------- 5 | # Copyright (C) 2003 RoboCup Federation 6 | # ------------------------------------------------------------------------- 7 | # This software is distributed under the GNU General Public License, 8 | # version 2. If you do not have a copy of this licence, visit 9 | # www.gnu.org, or write: Free Software Foundation, 59 Temple Place, 10 | # Suite 330 Boston, MA 02111-1307 USA. This program is distributed 11 | # in the hope that it will be useful, but WITHOUT ANY WARRANTY, 12 | # including MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | # ------------------------------------------------------------------------- 14 | # 15 | # */ 16 | # 17 | CC=g++ 18 | 19 | # EDIT: Please, replace the output of "pkg-config gtkmm-2.0 --cflags" 20 | # in the next line. 21 | # CFLAGS = -g $(pkg-config gtkmm-2.4 --cflags) 22 | CFLAGS = \ 23 | -Ic:/GTK/include/gtkmm-2.4 \ 24 | -Ic:/GTK/lib/gtkmm-2.4/include \ 25 | -Ic:/GTK/include/glibmm-2.4 \ 26 | -Ic:/GTK/lib/glibmm-2.4/include \ 27 | -Ic:/GTK/include/gdkmm-2.4 \ 28 | -Ic:/GTK/lib/gdkmm-2.4/include \ 29 | -Ic:/GTK/include/pangomm-1.4 \ 30 | -Ic:/GTK/include/atkmm-1.6 \ 31 | -Ic:/GTK/include/gtk-2.0 \ 32 | -Ic:/GTK/include/sigc++-2.0 \ 33 | -Ic:/GTK/lib/sigc++-2.0/include \ 34 | -Ic:/GTK/include/glib-2.0 \ 35 | -Ic:/GTK/lib/glib-2.0/include \ 36 | -Ic:/GTK/lib/gtk-2.0/include \ 37 | -Ic:/GTK/include/cairomm-1.0 \ 38 | -Ic:/GTK/include/pango-1.0 \ 39 | -Ic:/GTK/include/cairo \ 40 | -Ic:/GTK/include/freetype2 \ 41 | -Ic:/GTK/include \ 42 | -Ic:/GTK/include/atk-1.0 43 | 44 | # EDIT: Please, replace the output of "pkg-config gtkmm-2.0 --libs" 45 | # in the next line. 46 | #LIBS += $(pkg-config gtkmm-2.4 --libs) 47 | 48 | LIBS = \ 49 | -user32 -Wl,-luuid -Lc:/GTK/lib \ 50 | -lgtkmm-2.4 -lgdkmm-2.4 -latkmm-1.6 -lgtk-win32-2.0 \ 51 | -lpangomm-1.4 -lcairomm-1.0 -lglibmm-2.4 -lsigc-2.0 \ 52 | -lgdk-win32-2.0 -lgdi32 -limm32 -lshell32 -lole32 \ 53 | -latk-1.0 -lgdk_pixbuf-2.0 -lpangowin32-1.0 \ 54 | -lpangocairo-1.0 -lcairo -lpangoft2-1.0 -lfontconfig \ 55 | -lfreetype -lz -lpango-1.0 -lm -lgobject-2.0 \ 56 | -lgmodule-2.0 -lglib-2.0 -lintl -liconv -lwsock32 57 | 58 | LDFLAGS= 59 | 60 | OBJS = gameinfo.o refereemm.o gamecontrol.o udp_broadcast.o \ 61 | dialog_gameover.o 62 | 63 | %.o: %.cc 64 | $(CC) -mms-bitfields -c $(CFLAGS) $(DEFS) -o $@ $< 65 | 66 | all::sslrefbox 67 | 68 | clean:: 69 | rm -f *~ *.o *.bak 70 | rm sslrefbox.exe 71 | 72 | sslrefbox: $(OBJS) 73 | $(CC) -o $@ $(LDFLAGS) $^ $(LIBS) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Note: The ssl-refbox has been replaced by the ssl-game-controller: https://github.com/RoboCup-SSL/ssl-game-controller 2 | 3 | # ssl-refbox 4 | RoboCup Small Size League Referee Box 5 | 6 | Installation and usage is documented here: https://robocup-ssl.github.io/ssl-refbox/ 7 | -------------------------------------------------------------------------------- /addrinfolist.cc: -------------------------------------------------------------------------------- 1 | #include "addrinfolist.h" 2 | #include "exception.h" 3 | 4 | AddrInfoList::AddrInfoList(const char *node, const char *service, const addrinfo *hints) { 5 | ai = 0; 6 | int rc = getaddrinfo(node, service, hints, &ai); 7 | if (rc) { 8 | throw GAIError("Cannot look up address info", rc); 9 | } 10 | } 11 | 12 | AddrInfoList::~AddrInfoList() { 13 | freeaddrinfo(ai); 14 | } 15 | 16 | addrinfo *AddrInfoList::get() const { 17 | return ai; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /addrinfolist.h: -------------------------------------------------------------------------------- 1 | #ifndef ADDRINFOLIST_H 2 | #define ADDRINFOLIST_H 3 | 4 | #include "noncopyable.h" 5 | 6 | #ifdef WIN32 7 | #include 8 | #include 9 | #else 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #endif 17 | 18 | class AddrInfoList : public NonCopyable { 19 | public: 20 | AddrInfoList(const char *node, const char *service, const addrinfo *hints); 21 | ~AddrInfoList(); 22 | addrinfo *get() const; 23 | 24 | private: 25 | addrinfo *ai; 26 | }; 27 | 28 | #endif 29 | 30 | -------------------------------------------------------------------------------- /client/Makefile: -------------------------------------------------------------------------------- 1 | # Standard compiler and linker flags. 2 | PKG_CONFIG ?= pkg-config 3 | override CXXFLAGS := -std=gnu++0x -Wall -Wextra -Wold-style-cast -Wconversion -Wundef -O2 -g $(shell $(PKG_CONFIG) --cflags gtkmm-2.4 protobuf | sed 's/-I/-isystem /g') $(CXXFLAGS) 4 | override LDFLAGS := $(shell $(PKG_CONFIG) --libs-only-L --libs-only-other gtkmm-2.4 protobuf) 5 | override LDLIBS := $(shell $(PKG_CONFIG) --libs-only-l gtkmm-2.4 protobuf) 6 | 7 | # The default target. 8 | .PHONY : world 9 | world : testclient 10 | 11 | # Gather lists of files of various types. 12 | protos := referee.proto 13 | proto_sources := $(patsubst %.proto,%.pb.cc,$(protos)) 14 | proto_headers := $(patsubst %.proto,%.pb.h,$(protos)) 15 | proto_objs := $(patsubst %.proto,%.pb.o,$(protos)) 16 | non_proto_sources := $(filter-out $(proto_sources),$(wildcard *.cc)) 17 | non_proto_headers := $(filter-out $(proto_headers),$(wildcard *.h)) 18 | non_proto_objs := $(patsubst %.cc,%.o,$(non_proto_sources)) 19 | all_sources := $(proto_sources) $(non_proto_sources) 20 | all_headers := $(proto_headers) $(non_proto_headers) 21 | all_objs := $(proto_objs) $(non_proto_objs) 22 | 23 | # Normal rule to link the final binary. 24 | testclient : $(all_objs) 25 | @echo "LD $@" 26 | @$(CXX) $(LDFLAGS) -o $@ $+ $(LDLIBS) 27 | 28 | # Static pattern rule to compile a protobuf source file (with warnings disabled, as they make no sense here). 29 | $(proto_objs) : %.pb.o : %.pb.cc $(all_headers) 30 | @echo "CXX $@" 31 | @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -w -c $< 32 | 33 | # Static pattern rule to compile a non-protobuf source file. 34 | $(non_proto_objs) : %.o : %.cc $(all_headers) 35 | @echo "CXX $@" 36 | @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< 37 | 38 | # Pattern rule to run protoc on a message definition file. 39 | %.pb.cc %.pb.h : ../%.proto 40 | @echo "PROTO $(patsubst ../%.proto,%.pb.cc,$<)" 41 | @protoc --proto_path=.. --cpp_out=. $< 42 | 43 | # Rule to clean intermediates and outputs. 44 | .PHONY : clean 45 | clean : 46 | $(RM) testclient *.o *.pb.cc *.pb.h 47 | -------------------------------------------------------------------------------- /client/testclient.cc: -------------------------------------------------------------------------------- 1 | #include "referee.pb.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #define MULTICAST_ADDRESS "224.5.23.1" 14 | #define MULTICAST_PORT "10003" 15 | 16 | namespace { 17 | void usage(const char* appName) { 18 | std::cerr << "Usage:\n" << appName << " multicast_interface" << std::endl; 19 | std::exit(EXIT_FAILURE); 20 | } 21 | } 22 | 23 | int main(int argc, char** argv) { 24 | if (argc != 2) { 25 | usage(argv[0]); 26 | } 27 | 28 | // Get a socket address to bind to. 29 | addrinfo hints; 30 | std::memset(&hints, 0, sizeof(hints)); 31 | hints.ai_family = AF_INET; 32 | hints.ai_socktype = SOCK_DGRAM; 33 | hints.ai_protocol = 0; 34 | hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV; 35 | addrinfo *ai = 0; 36 | int gai_err; 37 | if ((gai_err = getaddrinfo(0, MULTICAST_PORT, &hints, &ai)) != 0) { 38 | std::cerr << gai_strerror(gai_err); 39 | return 1; 40 | } 41 | 42 | // Look up the index of the network interface from its name. 43 | unsigned int ifindex = if_nametoindex(argv[1]); 44 | if (!ifindex) { 45 | std::cerr << std::strerror(errno) << '\n'; 46 | return 1; 47 | } 48 | 49 | // Create and bind a socket. 50 | int sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); 51 | if (sock < 0) { 52 | std::cerr << std::strerror(errno) << '\n'; 53 | return 1; 54 | } 55 | if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) { 56 | std::cerr << std::strerror(errno) << '\n'; 57 | return 1; 58 | } 59 | 60 | // Join the multicast group. 61 | ip_mreqn mcreq; 62 | mcreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_ADDRESS); 63 | mcreq.imr_address.s_addr = INADDR_ANY; 64 | mcreq.imr_ifindex = ifindex; 65 | if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mcreq, sizeof(mcreq)) < 0) { 66 | std::cerr << std::strerror(errno) << '\n'; 67 | return 1; 68 | } 69 | 70 | for (;;) { 71 | // Receive a packet. 72 | uint8_t buffer[65536]; 73 | ssize_t len = recv(sock, buffer, sizeof(buffer), 0); 74 | if (len < 0) { 75 | std::cerr << std::strerror(errno) << '\n'; 76 | return 1; 77 | } 78 | 79 | // Parse a Protobuf structure out of it. 80 | SSL_Referee referee; 81 | if (!referee.ParseFromArray(buffer, static_cast(len))) { 82 | std::cerr << "Protobuf parsing error!\n"; 83 | return 1; 84 | } 85 | 86 | // Display some information. 87 | std::cout << "TS=" << referee.packet_timestamp() << ", stage=" << referee.stage() << ", stage_time_left=" << referee.stage_time_left() << ", command=" << referee.command() << ", yscore=" << referee.yellow().score() << ", bscore=" << referee.blue().score(); 88 | if (referee.has_designated_position()) { 89 | std::cout << ", designated=(" << referee.designated_position().x() << ',' << referee.designated_position().y() << ')'; 90 | } 91 | std::cout << '\n'; 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /configuration.cc: -------------------------------------------------------------------------------- 1 | #include "configuration.h" 2 | #include "logger.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | Configuration::Configuration(const std::string &filename) { 10 | Glib::KeyFile kf; 11 | kf.load_from_file(filename); 12 | 13 | normal_half_seconds = kf.get_integer(u8"normal", u8"HALF"); 14 | normal_half_time_seconds = kf.get_integer(u8"normal", u8"HALF_TIME"); 15 | normal_timeout_seconds = static_cast(kf.get_integer(u8"normal", u8"TIMEOUT_TIME")); 16 | normal_timeouts = static_cast(kf.get_integer(u8"normal", u8"TIMEOUTS")); 17 | 18 | overtime_break_seconds = kf.get_integer(u8"overtime", u8"BREAK"); 19 | overtime_half_seconds = kf.get_integer(u8"overtime", u8"HALF"); 20 | overtime_half_time_seconds = kf.get_integer(u8"overtime", u8"HALF_TIME"); 21 | overtime_timeout_seconds = static_cast(kf.get_integer(u8"overtime", u8"TIMEOUT_TIME")); 22 | overtime_timeouts = static_cast(kf.get_integer(u8"overtime", u8"TIMEOUTS")); 23 | 24 | shootout_break_seconds = kf.get_integer(u8"shootout", u8"BREAK"); 25 | 26 | yellow_card_seconds = static_cast(kf.get_integer(u8"global", u8"YELLOW_CARD_TIME")); 27 | team_names_required = kf.get_boolean(u8"global", u8"TEAM_NAMES_REQUIRED"); 28 | rcon_enabled_by_default = kf.get_boolean(u8"global", u8"RCON_ENABLED_BY_DEFAULT"); 29 | 30 | if (kf.has_key(u8"files", u8"SAVE")) { 31 | save_filename = Glib::filename_from_utf8(Glib::ustring::compose(kf.get_string(u8"files", u8"SAVE"), Glib::DateTime::create_now_local().format(u8"%Y%m%dT%H%M%S"))); 32 | } 33 | log_filename = kf.has_key(u8"files", u8"LOG") ? Glib::filename_from_utf8(kf.get_string(u8"files", u8"LOG")) : ""; 34 | 35 | address = kf.get_string(u8"ip", u8"ADDRESS"); 36 | legacy_port = kf.has_key(u8"ip", u8"LEGACY_PORT") ? kf.get_string(u8"ip", u8"LEGACY_PORT") : ""; 37 | protobuf_port = kf.has_key(u8"ip", u8"PROTOBUF_PORT") ? kf.get_string(u8"ip", u8"PROTOBUF_PORT") : ""; 38 | interface = kf.has_key(u8"ip", u8"INTERFACE") ? kf.get_string(u8"ip", u8"INTERFACE") : ""; 39 | if (kf.has_key(u8"ip", u8"RCON_PORT")) { 40 | rcon_port = static_cast(kf.get_integer(u8"ip", u8"RCON_PORT")); 41 | } else { 42 | rcon_port = 0; 43 | } 44 | 45 | for (const Glib::ustring &key : kf.get_keys(u8"teams")) { 46 | teams.push_back(kf.get_string(u8"teams", key)); 47 | } 48 | std::sort(teams.begin(), teams.end()); 49 | } 50 | 51 | void Configuration::dump(Logger &logger) { 52 | logger.write(Glib::ustring::compose(u8"Configuration: Normal halves: %1 seconds.", normal_half_seconds)); 53 | logger.write(Glib::ustring::compose(u8"Configuration: Normal half time: %1 seconds.", normal_half_time_seconds)); 54 | logger.write(Glib::ustring::compose(u8"Configuration: Normal timeouts: %1, totalling up to %2 seconds.", normal_timeouts, normal_timeout_seconds)); 55 | logger.write(Glib::ustring::compose(u8"Configuration: Pre-overtime break: %1 seconds.", overtime_break_seconds)); 56 | logger.write(Glib::ustring::compose(u8"Configuration: Overtime halves: %1 seconds.", overtime_half_seconds)); 57 | logger.write(Glib::ustring::compose(u8"Configuration: Overtime half time: %1 seconds.", overtime_half_time_seconds)); 58 | logger.write(Glib::ustring::compose(u8"Configuration: Overtime timeouts: %1, totalling up to %2 seconds.", overtime_timeouts, overtime_timeout_seconds)); 59 | logger.write(Glib::ustring::compose(u8"Configuration: Pre-shootout break: %1 seconds.", shootout_break_seconds)); 60 | logger.write(Glib::ustring::compose(u8"Configuration: Yellow card: %1 seconds.", yellow_card_seconds)); 61 | if (!save_filename.empty()) { 62 | logger.write(Glib::ustring::compose(u8"Configuration: State save filename: \"%1\".", Glib::filename_to_utf8(save_filename))); 63 | } 64 | if (!log_filename.empty()) { 65 | logger.write(Glib::ustring::compose(u8"Configuration: Log filename: \"%1\".", Glib::filename_to_utf8(log_filename))); 66 | } 67 | logger.write(Glib::ustring::compose(u8"Configuration: Packet destination address: \"%1\".", Glib::locale_to_utf8(address))); 68 | if (!legacy_port.empty()) { 69 | logger.write(Glib::ustring::compose(u8"Configuration: Legacy port: \"%1\".", legacy_port)); 70 | } 71 | if (!protobuf_port.empty()) { 72 | logger.write(Glib::ustring::compose(u8"Configuration: Protobuf port: \"%1\".", protobuf_port)); 73 | } 74 | if (rcon_port) { 75 | logger.write(Glib::ustring::compose(u8"Configuration: Remote control port: %1.", rcon_port)); 76 | } 77 | if (!interface.empty()) { 78 | logger.write(Glib::ustring::compose(u8"Configuration: Network interface: \"%1\".", Glib::locale_to_utf8(interface))); 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /configuration.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGURATION_H 2 | #define CONFIGURATION_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class Logger; 10 | 11 | class Configuration { 12 | public: 13 | // [normal] section 14 | int normal_half_seconds; 15 | int normal_half_time_seconds; 16 | unsigned int normal_timeout_seconds; 17 | unsigned int normal_timeouts; 18 | 19 | // [overtime] section 20 | int overtime_break_seconds; 21 | int overtime_half_seconds; 22 | int overtime_half_time_seconds; 23 | unsigned int overtime_timeout_seconds; 24 | unsigned int overtime_timeouts; 25 | 26 | // [shootout] section 27 | int shootout_break_seconds; 28 | 29 | // [global] section 30 | unsigned int yellow_card_seconds; 31 | bool team_names_required; 32 | bool rcon_enabled_by_default; 33 | 34 | // [files] section 35 | std::string save_filename; 36 | std::string log_filename; 37 | 38 | // [ip] section 39 | std::string address; 40 | std::string legacy_port; 41 | std::string protobuf_port; 42 | std::string interface; 43 | uint16_t rcon_port; 44 | 45 | // [teams] section 46 | std::vector teams; 47 | 48 | Configuration(const std::string &filename); 49 | void dump(Logger &logger); 50 | }; 51 | 52 | #endif 53 | 54 | -------------------------------------------------------------------------------- /exception.cc: -------------------------------------------------------------------------------- 1 | #include "exception.h" 2 | #include 3 | #include 4 | #include 5 | 6 | #ifdef WIN32 7 | #include 8 | #else 9 | #include 10 | #include 11 | #include 12 | #endif 13 | 14 | namespace { 15 | std::string make_message(const std::string &message, int rc) { 16 | std::ostringstream oss; 17 | oss << message << ": " << std::strerror(rc); 18 | return oss.str(); 19 | } 20 | 21 | std::string make_gai_message(const std::string &message, int rc) { 22 | std::ostringstream oss; 23 | oss << message << ": " << gai_strerror(rc); 24 | return oss.str(); 25 | } 26 | } 27 | 28 | SystemError::SystemError(const std::string &message) : std::runtime_error(make_message(message, errno)) { 29 | } 30 | 31 | SystemError::SystemError(const std::string &message, int rc) : std::runtime_error(make_message(message, rc)) { 32 | } 33 | 34 | GAIError::GAIError(const std::string &message, int rc) : std::runtime_error(make_gai_message(message, rc)) { 35 | } 36 | 37 | -------------------------------------------------------------------------------- /exception.h: -------------------------------------------------------------------------------- 1 | #ifndef EXCEPTION_H 2 | #define EXCEPTION_H 3 | 4 | #include 5 | #include 6 | 7 | class SystemError : public std::runtime_error { 8 | public: 9 | SystemError(const std::string &message); 10 | SystemError(const std::string &message, int rc); 11 | }; 12 | 13 | class GAIError : public std::runtime_error { 14 | public: 15 | GAIError(const std::string &message, int rc); 16 | }; 17 | 18 | #endif 19 | 20 | -------------------------------------------------------------------------------- /game_event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | // a game event that caused a referee command 4 | message SSL_Referee_Game_Event { 5 | 6 | enum GameEventType { 7 | // not set 8 | UNKNOWN = 0; 9 | 10 | // an event that is not listed in this enum yet. 11 | // Give further details in the message below 12 | CUSTOM = 1; 13 | 14 | // Law 3: Number of players 15 | NUMBER_OF_PLAYERS = 2; 16 | 17 | // Law 9: Ball out of play 18 | BALL_LEFT_FIELD = 3; 19 | 20 | // Law 10: Team scored a goal 21 | GOAL = 4; 22 | 23 | // Law 9.3: lack of progress while bringing the ball into play 24 | KICK_TIMEOUT = 5; 25 | 26 | // Law ?: There is no progress in game for (10|15)? seconds 27 | NO_PROGRESS_IN_GAME = 6; 28 | 29 | // Law 12: Pushing / Substantial Contact 30 | BOT_COLLISION = 7; 31 | 32 | // Law 12.2: Defender is completely inside penalty area 33 | MULTIPLE_DEFENDER = 8; 34 | 35 | // Law 12: Defender is partially inside penalty area 36 | MULTIPLE_DEFENDER_PARTIALLY = 9; 37 | 38 | // Law 12.3: Attacker in defense area 39 | ATTACKER_IN_DEFENSE_AREA = 10; 40 | 41 | // Law 12: Icing (kicking over midline and opponent goal line) 42 | ICING = 11; 43 | 44 | // Law 12: Ball speed 45 | BALL_SPEED = 12; 46 | 47 | // Law 12: Robot speed during STOP 48 | ROBOT_STOP_SPEED = 13; 49 | 50 | // Law 12: Maximum dribbling distance 51 | BALL_DRIBBLING = 14; 52 | 53 | // Law 12: Touching the opponent goalkeeper 54 | ATTACKER_TOUCH_KEEPER = 15; 55 | 56 | // Law 12: Double touch 57 | DOUBLE_TOUCH = 16; 58 | 59 | // Law 13-17: Attacker not too close to the opponent's penalty area when ball enters play 60 | ATTACKER_TO_DEFENCE_AREA = 17; 61 | 62 | // Law 13-17: Keeping the correct distance to the ball during opponents freekicks 63 | DEFENDER_TO_KICK_POINT_DISTANCE = 18; 64 | 65 | // Law 12: A robot holds the ball deliberately 66 | BALL_HOLDING = 19; 67 | 68 | // Law 12: The ball entered the goal directly after an indirect kick was performed 69 | INDIRECT_GOAL = 20; 70 | 71 | // Law 9.2: Ball placement 72 | BALL_PLACEMENT_FAILED = 21; 73 | 74 | // Law 10: A goal is only scored if the ball has not exceeded a robot height (150mm) between the last 75 | // kick of an attacker and the time the ball crossed the goal line. 76 | CHIP_ON_GOAL = 22; 77 | } 78 | 79 | // the game event type that happened 80 | required GameEventType gameEventType = 1; 81 | 82 | // a team 83 | enum Team { 84 | TEAM_UNKNOWN = 0; 85 | TEAM_YELLOW = 1; 86 | TEAM_BLUE = 2; 87 | } 88 | 89 | // information about an originator 90 | message Originator { 91 | required Team team = 1; 92 | optional uint32 botId = 2; 93 | } 94 | 95 | // the team and optionally a designated robot that is the originator of the game event 96 | optional Originator originator = 2; 97 | 98 | // a message describing further details of this game event 99 | optional string message = 3; 100 | } -------------------------------------------------------------------------------- /gamecontroller.cc: -------------------------------------------------------------------------------- 1 | #include "gamecontroller.h" 2 | #include "configuration.h" 3 | #include "logger.h" 4 | #include "publisher.h" 5 | #include "savegame.h" 6 | #include "teams.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace { 18 | const uint32_t STATE_SAVE_INTERVAL = 5000000UL; 19 | } 20 | 21 | GameController::GameController(Logger &logger, const Configuration &configuration, const std::vector &publishers, const std::string &resume_filename) : 22 | configuration(configuration), 23 | logger(logger), 24 | publishers(publishers), 25 | tick_connection(Glib::signal_timeout().connect(sigc::mem_fun(this, &GameController::tick), 25)), 26 | microseconds_since_last_state_save(0) { 27 | if (!resume_filename.empty()) { 28 | std::ifstream ifs; 29 | ifs.exceptions(std::ios_base::badbit); 30 | ifs.open(resume_filename, std::ios_base::in | std::ios_base::binary); 31 | if (!state.ParseFromIstream(&ifs)) { 32 | throw std::runtime_error(Glib::locale_from_utf8(Glib::ustring::compose(u8"Protobuf error loading saved game state from file \"%1\"!", Glib::filename_to_utf8(resume_filename)))); 33 | } 34 | ifs.close(); 35 | set_command(SSL_Referee::HALT); 36 | } else { 37 | SSL_Referee &ref = *state.mutable_referee(); 38 | ref.set_packet_timestamp(0); 39 | ref.set_stage(SSL_Referee::NORMAL_FIRST_HALF_PRE); 40 | ref.set_command(SSL_Referee::HALT); 41 | ref.set_command_counter(0); 42 | ref.set_command_timestamp(static_cast(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())) * 1000000UL); 43 | ref.set_blueteamonpositivehalf(false); 44 | 45 | for (unsigned int teami = 0; teami < 2; ++teami) { 46 | SaveState::Team team = static_cast(teami); 47 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(ref); 48 | ti.set_name(u8""); 49 | ti.set_score(0); 50 | ti.set_red_cards(0); 51 | ti.set_yellow_cards(0); 52 | ti.set_timeouts(configuration.normal_timeouts); 53 | ti.set_timeout_time(static_cast(configuration.normal_timeout_seconds * 1000000UL)); 54 | ti.set_goalie(0); 55 | } 56 | 57 | state.set_yellow_penalty_goals(0); 58 | state.set_blue_penalty_goals(0); 59 | state.set_time_taken(0); 60 | } 61 | } 62 | 63 | GameController::~GameController() { 64 | // Disconnect the timer connection. 65 | tick_connection.disconnect(); 66 | 67 | // Try to save the current game state. 68 | try { 69 | save_game(state, configuration.save_filename); 70 | } catch (...) { 71 | // Swallow exceptions. 72 | } 73 | } 74 | 75 | bool GameController::can_enter_stage(SSL_Referee::Stage stage) const { 76 | const SSL_Referee &ref = state.referee(); 77 | bool is_stopped = ref.command() == SSL_Referee::STOP || ref.command() == SSL_Referee::GOAL_YELLOW || ref.command() == SSL_Referee::GOAL_BLUE; 78 | bool is_normal_half = ref.stage() == SSL_Referee::NORMAL_FIRST_HALF || ref.stage() == SSL_Referee::NORMAL_SECOND_HALF || ref.stage() == SSL_Referee::EXTRA_FIRST_HALF || ref.stage() == SSL_Referee::EXTRA_SECOND_HALF; 79 | 80 | switch (stage) { 81 | case SSL_Referee::NORMAL_FIRST_HALF_PRE: 82 | case SSL_Referee::NORMAL_FIRST_HALF: 83 | case SSL_Referee::NORMAL_SECOND_HALF: 84 | case SSL_Referee::EXTRA_FIRST_HALF: 85 | case SSL_Referee::EXTRA_SECOND_HALF: 86 | // You can only get to first half pre-game or any running game half when you are ignoring rules; otherwise, you start in first half pre-game and enter other halves via the Normal Start command. 87 | return false; 88 | 89 | case SSL_Referee::NORMAL_SECOND_HALF_PRE: 90 | // You can get to second half when you are in normal half time. 91 | return ref.stage() == SSL_Referee::NORMAL_HALF_TIME; 92 | 93 | case SSL_Referee::EXTRA_FIRST_HALF_PRE: 94 | // You can get to overtime 1 when you are in extra time break. 95 | return ref.stage() == SSL_Referee::EXTRA_TIME_BREAK; 96 | 97 | case SSL_Referee::EXTRA_SECOND_HALF_PRE: 98 | // You can get to overtime 2 when you are in extra time half time. 99 | return ref.stage() == SSL_Referee::EXTRA_HALF_TIME; 100 | 101 | case SSL_Referee::PENALTY_SHOOTOUT: 102 | // You can get to penalty shootout when you are in penalty shootout break. 103 | return ref.stage() == SSL_Referee::PENALTY_SHOOTOUT_BREAK; 104 | 105 | case SSL_Referee::POST_GAME: 106 | // You can get to post-game whenever you are stopped or halted except in post-game. 107 | // This might be needed at any time throughout the game, due to the stop-at-ten-points rule. 108 | return ref.stage() != SSL_Referee::POST_GAME && (is_stopped || ref.command() == SSL_Referee::HALT); 109 | 110 | case SSL_Referee::NORMAL_HALF_TIME: 111 | case SSL_Referee::EXTRA_TIME_BREAK: 112 | case SSL_Referee::EXTRA_HALF_TIME: 113 | case SSL_Referee::PENALTY_SHOOTOUT_BREAK: 114 | // You can get to half time whenever you are stopped in the proper normal half. 115 | return is_normal_half && is_stopped && stage == next_half_time(); 116 | } 117 | 118 | return false; 119 | } 120 | 121 | void GameController::enter_stage(SSL_Referee::Stage stage) { 122 | SSL_Referee &ref = *state.mutable_referee(); 123 | 124 | // Record what’s happening. 125 | logger.write(Glib::ustring::compose(u8"Entering new stage %1", SSL_Referee::Stage_descriptor()->FindValueByNumber(stage)->name())); 126 | 127 | // Set the new stage. 128 | ref.set_stage(stage); 129 | 130 | // Reset the stage time taken. 131 | state.set_time_taken(0); 132 | 133 | // Set or remove the stage time left as appropriate for the stage. 134 | switch (stage) { 135 | case SSL_Referee::NORMAL_FIRST_HALF: ref.set_stage_time_left(configuration.normal_half_seconds * 1000000); break; 136 | case SSL_Referee::NORMAL_HALF_TIME: ref.set_stage_time_left(configuration.normal_half_time_seconds * 1000000); break; 137 | case SSL_Referee::NORMAL_SECOND_HALF: ref.set_stage_time_left(configuration.normal_half_seconds * 1000000); break; 138 | case SSL_Referee::EXTRA_TIME_BREAK: ref.set_stage_time_left(configuration.overtime_break_seconds * 1000000); break; 139 | case SSL_Referee::EXTRA_FIRST_HALF: ref.set_stage_time_left(configuration.overtime_half_seconds * 1000000); break; 140 | case SSL_Referee::EXTRA_HALF_TIME: ref.set_stage_time_left(configuration.overtime_half_time_seconds * 1000000); break; 141 | case SSL_Referee::EXTRA_SECOND_HALF: ref.set_stage_time_left(configuration.overtime_half_seconds * 1000000); break; 142 | case SSL_Referee::PENALTY_SHOOTOUT_BREAK: ref.set_stage_time_left(configuration.shootout_break_seconds * 1000000); break; 143 | default: ref.clear_stage_time_left(); break; 144 | } 145 | 146 | // If we’re going into a pre-game state before either the normal game or overtime, reset the timeouts. 147 | if (stage == SSL_Referee::NORMAL_FIRST_HALF_PRE || stage == SSL_Referee::EXTRA_FIRST_HALF_PRE) { 148 | unsigned int count = stage == SSL_Referee::NORMAL_FIRST_HALF_PRE ? configuration.normal_timeouts : configuration.overtime_timeouts; 149 | unsigned int seconds = stage == SSL_Referee::NORMAL_FIRST_HALF_PRE ? configuration.normal_timeout_seconds : configuration.overtime_timeout_seconds; 150 | for (unsigned int teami = 0; teami < 2; ++teami) { 151 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[teami].team_info(ref); 152 | ti.set_timeouts(count); 153 | ti.set_timeout_time(static_cast(seconds * 1000000UL)); 154 | } 155 | } 156 | 157 | // Nearly all stage entries correspond to a transition to HALT. 158 | // The exceptions are game half entries, where a NORMAL START accompanies the entry in an atomic transition from kickoff. 159 | bool is_half = stage == SSL_Referee::NORMAL_FIRST_HALF || stage == SSL_Referee::NORMAL_SECOND_HALF || stage == SSL_Referee::EXTRA_FIRST_HALF || stage == SSL_Referee::EXTRA_SECOND_HALF; 160 | if (!is_half) { 161 | // set_command will save the game state and emit a change signal. 162 | set_command(SSL_Referee::HALT); 163 | } 164 | // In case of starting a game half, the function that called 165 | // advance_from_pre will also call set_command. 166 | } 167 | 168 | SSL_Referee::Stage GameController::next_half_time() const { 169 | const SSL_Referee &ref = state.referee(); 170 | 171 | // Which stage to go into depends on which stage we are already in. 172 | switch (ref.stage()) { 173 | case SSL_Referee::NORMAL_FIRST_HALF_PRE: 174 | case SSL_Referee::NORMAL_FIRST_HALF: 175 | return SSL_Referee::NORMAL_HALF_TIME; 176 | case SSL_Referee::NORMAL_HALF_TIME: 177 | case SSL_Referee::NORMAL_SECOND_HALF_PRE: 178 | case SSL_Referee::NORMAL_SECOND_HALF: 179 | return SSL_Referee::EXTRA_TIME_BREAK; 180 | case SSL_Referee::EXTRA_TIME_BREAK: 181 | case SSL_Referee::EXTRA_FIRST_HALF_PRE: 182 | case SSL_Referee::EXTRA_FIRST_HALF: 183 | return SSL_Referee::EXTRA_HALF_TIME; 184 | case SSL_Referee::EXTRA_HALF_TIME: 185 | case SSL_Referee::EXTRA_SECOND_HALF_PRE: 186 | case SSL_Referee::EXTRA_SECOND_HALF: 187 | case SSL_Referee::PENALTY_SHOOTOUT_BREAK: 188 | case SSL_Referee::PENALTY_SHOOTOUT: 189 | return SSL_Referee::PENALTY_SHOOTOUT_BREAK; 190 | case SSL_Referee::POST_GAME: 191 | return SSL_Referee::POST_GAME; 192 | } 193 | 194 | return SSL_Referee::POST_GAME; 195 | } 196 | 197 | bool GameController::can_set_command(SSL_Referee::Command command) const { 198 | const SSL_Referee &ref = state.referee(); 199 | bool is_normal_half = ref.stage() == SSL_Referee::NORMAL_FIRST_HALF || ref.stage() == SSL_Referee::NORMAL_SECOND_HALF || ref.stage() == SSL_Referee::EXTRA_FIRST_HALF || ref.stage() == SSL_Referee::EXTRA_SECOND_HALF; 200 | bool is_break = ref.stage() == SSL_Referee::NORMAL_HALF_TIME || ref.stage() == SSL_Referee::EXTRA_TIME_BREAK || ref.stage() == SSL_Referee::EXTRA_HALF_TIME || ref.stage() == SSL_Referee::PENALTY_SHOOTOUT_BREAK; 201 | bool is_stopped = ref.command() == SSL_Referee::STOP || ref.command() == SSL_Referee::GOAL_YELLOW || ref.command() == SSL_Referee::GOAL_BLUE; 202 | bool is_prepare_kickoff = ref.command() == SSL_Referee::PREPARE_KICKOFF_YELLOW || ref.command() == SSL_Referee::PREPARE_KICKOFF_BLUE; 203 | bool is_prepare_penalty = ref.command() == SSL_Referee::PREPARE_PENALTY_YELLOW || ref.command() == SSL_Referee::PREPARE_PENALTY_BLUE; 204 | bool is_pshootout = ref.stage() == SSL_Referee::PENALTY_SHOOTOUT; 205 | bool is_ball_placement = ref.command() == SSL_Referee::BALL_PLACEMENT_YELLOW || ref.command() == SSL_Referee::BALL_PLACEMENT_BLUE; 206 | bool is_team_name_empty = false; 207 | for (const TeamMeta &team : TeamMeta::ALL) { 208 | is_team_name_empty |= team.team_info(ref).name().empty(); 209 | } 210 | switch (command) { 211 | case SSL_Referee::HALT: 212 | // You can HALT any time you are not already halted. 213 | return ref.command() != SSL_Referee::HALT; 214 | 215 | case SSL_Referee::STOP: 216 | // You can STOP any time you are not already stopped except in post-game. 217 | return !is_stopped && ref.stage() != SSL_Referee::POST_GAME; 218 | 219 | case SSL_Referee::FORCE_START: 220 | // You can FORCE START any time you are stopped in a normal half or a break (for robot testing). 221 | return (is_normal_half || is_break) && is_stopped; 222 | 223 | case SSL_Referee::NORMAL_START: 224 | // You can NORMAL START when you are preparing a prepared play (kickoff or penalty kick), except if a required team name is missing. 225 | return (is_prepare_kickoff || is_prepare_penalty) && !(configuration.team_names_required && is_team_name_empty); 226 | 227 | case SSL_Referee::PREPARE_KICKOFF_YELLOW: 228 | case SSL_Referee::PREPARE_KICKOFF_BLUE: 229 | // A team can take a kickoff whenever the game is stopped or in ball placement and not in a break or penalty shootout. 230 | return !is_break && !is_pshootout && (is_stopped || is_ball_placement); 231 | 232 | case SSL_Referee::DIRECT_FREE_YELLOW: 233 | case SSL_Referee::DIRECT_FREE_BLUE: 234 | case SSL_Referee::INDIRECT_FREE_YELLOW: 235 | case SSL_Referee::INDIRECT_FREE_BLUE: 236 | // A team can take a free kick whenever the game is in a normal half and stopped or after a successfull autonomous ball placement. 237 | return is_normal_half && (is_stopped || is_ball_placement); 238 | 239 | case SSL_Referee::PREPARE_PENALTY_YELLOW: 240 | case SSL_Referee::PREPARE_PENALTY_BLUE: 241 | // A team can take a penalty kick whenever the game is stopped or in ball placement in a normal half or during penalty shootout. 242 | return (is_normal_half || is_pshootout) && (is_stopped || is_ball_placement); 243 | 244 | // A team can start a timeout whenever the game is stopped and not in a break or penalty shootout. 245 | // A team can *resume* a timeout whenever the game is halted and that team already had a timeout in progress before the halt. 246 | case SSL_Referee::TIMEOUT_YELLOW: 247 | return (!is_break && !is_pshootout && is_stopped) || (ref.command() == SSL_Referee::HALT && state.has_timeout() && state.timeout().team() == SaveState::TEAM_YELLOW); 248 | case SSL_Referee::TIMEOUT_BLUE: 249 | return (!is_break && !is_pshootout && is_stopped) || (ref.command() == SSL_Referee::HALT && state.has_timeout() && state.timeout().team() == SaveState::TEAM_BLUE); 250 | 251 | case SSL_Referee::GOAL_YELLOW: 252 | case SSL_Referee::GOAL_BLUE: 253 | // You can award goals whenever you are stopped. 254 | return is_stopped; 255 | 256 | // You can ask for ball placement whenever you are stopped or the other team failed to place the ball 257 | case SSL_Referee::BALL_PLACEMENT_YELLOW: 258 | return is_stopped || ref.command() == SSL_Referee::BALL_PLACEMENT_BLUE; 259 | case SSL_Referee::BALL_PLACEMENT_BLUE: 260 | return is_stopped || ref.command() == SSL_Referee::BALL_PLACEMENT_YELLOW; 261 | } 262 | 263 | return false; 264 | } 265 | 266 | bool GameController::command_needs_designated_position(SSL_Referee::Command command) { 267 | switch (command) { 268 | case SSL_Referee::BALL_PLACEMENT_YELLOW: 269 | case SSL_Referee::BALL_PLACEMENT_BLUE: 270 | return true; 271 | 272 | case SSL_Referee::HALT: 273 | case SSL_Referee::STOP: 274 | case SSL_Referee::FORCE_START: 275 | case SSL_Referee::NORMAL_START: 276 | case SSL_Referee::PREPARE_KICKOFF_YELLOW: 277 | case SSL_Referee::PREPARE_KICKOFF_BLUE: 278 | case SSL_Referee::DIRECT_FREE_YELLOW: 279 | case SSL_Referee::DIRECT_FREE_BLUE: 280 | case SSL_Referee::INDIRECT_FREE_YELLOW: 281 | case SSL_Referee::INDIRECT_FREE_BLUE: 282 | case SSL_Referee::PREPARE_PENALTY_YELLOW: 283 | case SSL_Referee::PREPARE_PENALTY_BLUE: 284 | case SSL_Referee::TIMEOUT_YELLOW: 285 | case SSL_Referee::TIMEOUT_BLUE: 286 | case SSL_Referee::GOAL_YELLOW: 287 | case SSL_Referee::GOAL_BLUE: 288 | return false; 289 | } 290 | 291 | return false; 292 | } 293 | 294 | void GameController::set_game_event(const SSL_Referee_Game_Event *game_event) { 295 | SSL_Referee *ref = state.mutable_referee(); 296 | 297 | // copy game event from request 298 | if(game_event != NULL) { 299 | ref->mutable_gameevent()->CopyFrom(*game_event); 300 | } 301 | } 302 | 303 | void GameController::set_command(SSL_Referee::Command command, float designated_x, float designated_y, bool cancelling_timeout_end) { 304 | SSL_Referee *ref = state.mutable_referee(); 305 | 306 | // Record what’s happening. 307 | logger.write(Glib::ustring::compose(u8"Setting command %1", SSL_Referee::Command_descriptor()->FindValueByNumber(command)->name())); 308 | 309 | // Clear any prior designated position. 310 | ref->clear_designated_position(); 311 | 312 | // Clear any last timeout information, unless we need it. 313 | // It is easier to do this here, before a command might set it, than later. 314 | if (!cancelling_timeout_end) { 315 | state.clear_last_timeout(); 316 | } 317 | 318 | // Implement any special side effects. 319 | switch (command) { 320 | case SSL_Referee::STOP: 321 | // Remember which team had the timeout, if any, so exiting the timeout can be cancelled. 322 | if (state.has_timeout()) { 323 | *state.mutable_last_timeout() = state.timeout(); 324 | } 325 | state.clear_timeout(); 326 | break; 327 | 328 | case SSL_Referee::FORCE_START: 329 | case SSL_Referee::NORMAL_START: 330 | advance_from_pre(); 331 | break; 332 | 333 | case SSL_Referee::TIMEOUT_YELLOW: 334 | case SSL_Referee::TIMEOUT_BLUE: 335 | // Only update any of the accounting if there is not already a record of an in-progress timeout. 336 | // This allows to issue HALT during a timeout, then resume the running timeout, without eating up another of the team’s timeouts and without affecting the Cancel button. 337 | // If that happens, during HALT there will still be a record of a running timeout. 338 | { 339 | SaveState::Team team = (command == SSL_Referee::TIMEOUT_YELLOW) ? SaveState::TEAM_YELLOW : SaveState::TEAM_BLUE; 340 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(*ref); 341 | if (cancelling_timeout_end) { 342 | // We have been asked to cancel the previously ended timeout. 343 | // Do this by just moving the timeout information back from last_timeout. 344 | // We need to keep the old left_before value as well, so that Timeout, Stop, Cancel End, Cancel Timeout refunds the full time, not just the time taken in the second part. 345 | *state.mutable_timeout() = state.last_timeout(); 346 | state.clear_last_timeout(); 347 | } else if (!(state.has_timeout() && state.timeout().team() == team)) { 348 | // Do not debit a timeout if the team has no timeouts left, as we would wrap the counter. 349 | // Assume the referee is granting an extra timeout at their discretion. 350 | if (ti.timeouts()) { 351 | ti.set_timeouts(ti.timeouts() - 1); 352 | } 353 | state.mutable_timeout()->set_team(team); 354 | state.mutable_timeout()->set_left_before(ti.timeout_time()); 355 | } 356 | } 357 | break; 358 | 359 | case SSL_Referee::GOAL_YELLOW: 360 | case SSL_Referee::GOAL_BLUE: 361 | { 362 | SaveState::Team team = (command == SSL_Referee::GOAL_YELLOW) ? SaveState::TEAM_YELLOW : SaveState::TEAM_BLUE; 363 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(*ref); 364 | 365 | // Increase the team’s score. 366 | ti.set_score(ti.score() + 1); 367 | 368 | // Increase the team’s number of penalty goals if in a penalty shootout. 369 | if (ref->stage() == SSL_Referee::PENALTY_SHOOTOUT) { 370 | TeamMeta::ALL[team].set_penalty_goals(state, TeamMeta::ALL[team].penalty_goals(state) + 1); 371 | } 372 | } 373 | break; 374 | 375 | case SSL_Referee::BALL_PLACEMENT_YELLOW: 376 | case SSL_Referee::BALL_PLACEMENT_BLUE: 377 | // Communicate the designated position. 378 | ref->mutable_designated_position()->set_x(designated_x); 379 | ref->mutable_designated_position()->set_y(designated_y); 380 | break; 381 | 382 | case SSL_Referee::HALT: 383 | case SSL_Referee::PREPARE_KICKOFF_YELLOW: 384 | case SSL_Referee::PREPARE_KICKOFF_BLUE: 385 | case SSL_Referee::DIRECT_FREE_YELLOW: 386 | case SSL_Referee::DIRECT_FREE_BLUE: 387 | case SSL_Referee::INDIRECT_FREE_YELLOW: 388 | case SSL_Referee::INDIRECT_FREE_BLUE: 389 | case SSL_Referee::PREPARE_PENALTY_YELLOW: 390 | case SSL_Referee::PREPARE_PENALTY_BLUE: 391 | // No side effects. 392 | break; 393 | } 394 | 395 | // Set the new command. 396 | ref->set_command(command); 397 | 398 | // Increment the command counter. 399 | ref->set_command_counter(ref->command_counter() + 1); 400 | 401 | // Record the command timestamp. 402 | std::chrono::microseconds diff = std::chrono::duration_cast(std::chrono::system_clock::now() - std::chrono::system_clock::from_time_t(0)); 403 | ref->set_command_timestamp(static_cast(diff.count())); 404 | 405 | // We should save the game state now. 406 | save_game(state, configuration.save_filename); 407 | 408 | // Notify listeners of the state change. 409 | signal_other_changed.emit(); 410 | } 411 | 412 | void GameController::set_teamname(SaveState::Team team, const Glib::ustring &name) { 413 | SSL_Referee &ref = *state.mutable_referee(); 414 | TeamMeta::ALL[team].team_info(ref).set_name(name.raw()); 415 | signal_teamname_changed.emit(); 416 | } 417 | 418 | bool GameController::can_set_goalie() const { 419 | // You can change goalies whenever the game is stopped or halted except in post-game. 420 | const SSL_Referee &ref = state.referee(); 421 | bool is_stopped = ref.command() == SSL_Referee::STOP || ref.command() == SSL_Referee::GOAL_YELLOW || ref.command() == SSL_Referee::GOAL_BLUE; 422 | return ref.stage() != SSL_Referee::POST_GAME && (ref.command() == SSL_Referee::HALT || is_stopped); 423 | } 424 | 425 | void GameController::set_goalie(SaveState::Team team, unsigned int goalie) { 426 | SSL_Referee &ref = *state.mutable_referee(); 427 | TeamMeta::ALL[team].team_info(ref).set_goalie(goalie); 428 | } 429 | 430 | bool GameController::can_switch_colours() const { 431 | // You can switch colours when you are halted in a break or pre-half. 432 | const SSL_Referee &ref = state.referee(); 433 | bool is_break = ref.stage() == SSL_Referee::NORMAL_HALF_TIME || ref.stage() == SSL_Referee::EXTRA_TIME_BREAK || ref.stage() == SSL_Referee::EXTRA_HALF_TIME || ref.stage() == SSL_Referee::PENALTY_SHOOTOUT_BREAK; 434 | bool is_pre = ref.stage() == SSL_Referee::NORMAL_FIRST_HALF_PRE || ref.stage() == SSL_Referee::NORMAL_SECOND_HALF_PRE || ref.stage() == SSL_Referee::EXTRA_FIRST_HALF_PRE || ref.stage() == SSL_Referee::EXTRA_SECOND_HALF_PRE; 435 | return ref.command() == SSL_Referee::HALT && (is_break || is_pre); 436 | } 437 | 438 | bool GameController::can_switch_sides() const { 439 | return can_switch_colours(); 440 | } 441 | 442 | void GameController::switch_colours() { 443 | logger.write(u8"Switching colours."); 444 | SSL_Referee &ref = *state.mutable_referee(); 445 | 446 | // Swap the TeamInfo structures. 447 | ref.yellow().GetReflection()->Swap(ref.mutable_yellow(), ref.mutable_blue()); 448 | 449 | // Swap the team to which the last card was given (which can be cancelled), if present. 450 | if (state.has_last_card()) { 451 | state.mutable_last_card()->set_team(TeamMeta::ALL[state.last_card().team()].other()); 452 | } 453 | 454 | // Swap which team is currently in a timeout, if any. 455 | if (state.has_timeout()) { 456 | state.mutable_timeout()->set_team(TeamMeta::ALL[state.timeout().team()].other()); 457 | } 458 | 459 | signal_other_changed.emit(); 460 | } 461 | 462 | void GameController::switch_sides(bool blueTeamOnPositiveHalf) { 463 | logger.write(Glib::ustring::compose(u8"Switching sides: %1 Team on positive half", blueTeamOnPositiveHalf ? "Blue" : "Yellow")); 464 | 465 | SSL_Referee &ref = *state.mutable_referee(); 466 | 467 | ref.set_blueteamonpositivehalf(blueTeamOnPositiveHalf); 468 | 469 | signal_other_changed.emit(); 470 | } 471 | 472 | bool GameController::can_subtract_goal(SaveState::Team team) const { 473 | // You can subtract goals whenever you are stopped and the team has points. 474 | const SSL_Referee &ref = state.referee(); 475 | return (ref.command() == SSL_Referee::STOP || ref.command() == SSL_Referee::GOAL_YELLOW || ref.command() == SSL_Referee::GOAL_BLUE) && TeamMeta::ALL[team].team_info(ref).score(); 476 | } 477 | 478 | void GameController::subtract_goal(SaveState::Team team) { 479 | SSL_Referee &ref = *state.mutable_referee(); 480 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(ref); 481 | 482 | // Subtract a goal. 483 | if (ti.score()) { 484 | ti.set_score(ti.score() - 1); 485 | } 486 | 487 | // If we are in the penalty shootout and have penalty goals, decrement that count as well. 488 | if (TeamMeta::ALL[team].penalty_goals(state)) { 489 | TeamMeta::ALL[team].set_penalty_goals(state, TeamMeta::ALL[team].penalty_goals(state) - 1); 490 | } 491 | 492 | signal_other_changed.emit(); 493 | } 494 | 495 | GameController::CancelType GameController::cancel_type() const { 496 | const SSL_Referee &ref = state.referee(); 497 | if (ref.stage() == SSL_Referee::POST_GAME) { 498 | // You can never cancel anything in post-game. 499 | return CancelType::NONE; 500 | } else if (ref.command() == SSL_Referee::TIMEOUT_YELLOW || ref.command() == SSL_Referee::TIMEOUT_BLUE) { 501 | // You can cancel a timeout only when the timeout is running, not if it is in a nested HALT. 502 | return CancelType::TIMEOUT; 503 | } else if (state.has_last_timeout()) { 504 | // You can cancel ending a timeout when the last team who took a timeout is remembered. 505 | return CancelType::TIMEOUT_END; 506 | } else if (state.has_last_card()) { 507 | // You can cancel a card whenever there is one outstanding. 508 | return CancelType::CARD; 509 | } else { 510 | // There is nothing available to cancel right now. 511 | return CancelType::NONE; 512 | } 513 | } 514 | 515 | void GameController::cancel() { 516 | SSL_Referee &ref = *state.mutable_referee(); 517 | 518 | switch (cancel_type()) { 519 | case CancelType::NONE: 520 | break; 521 | 522 | case CancelType::CARD: 523 | // A card is active; cancel it. 524 | { 525 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[state.last_card().team()].team_info(ref); 526 | switch (state.last_card().card()) { 527 | case SaveState::CARD_YELLOW: 528 | if (ti.yellow_card_times_size()) { 529 | logger.write(Glib::ustring::compose(u8"Cancelling yellow card for %1.", TeamMeta::ALL[state.last_card().team()].COLOUR)); 530 | ti.mutable_yellow_card_times()->RemoveLast(); 531 | ti.set_yellow_cards(ti.yellow_cards() - 1); 532 | } 533 | break; 534 | 535 | case SaveState::CARD_RED: 536 | logger.write(Glib::ustring::compose(u8"Cancelling red card for %1.", TeamMeta::ALL[state.last_card().team()].COLOUR)); 537 | ti.set_red_cards(ti.red_cards() - 1); 538 | break; 539 | } 540 | state.clear_last_card(); 541 | } 542 | break; 543 | 544 | case CancelType::TIMEOUT: 545 | // A timeout is active; cancel it. 546 | { 547 | SaveState::Team team = TeamMeta::command_team(ref.command()); 548 | logger.write(Glib::ustring::compose(u8"Cancelling %1 timeout.", TeamMeta::ALL[team].COLOUR)); 549 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(ref); 550 | ti.set_timeouts(ti.timeouts() + 1); 551 | ti.set_timeout_time(state.timeout().left_before()); 552 | set_command(SSL_Referee::STOP); 553 | // Do not allow cancelling the end of the cancelled timeout, because that way lies madness! 554 | state.clear_last_timeout(); 555 | } 556 | break; 557 | 558 | case CancelType::TIMEOUT_END: 559 | // Resume the timeout that was most recently running without debiting another timeout from the team’s budget. 560 | { 561 | set_command((state.last_timeout().team() == SaveState::TEAM_YELLOW) ? SSL_Referee::TIMEOUT_YELLOW : SSL_Referee::TIMEOUT_BLUE, 0.0f, 0.0f, true); 562 | } 563 | break; 564 | } 565 | 566 | signal_other_changed.emit(); 567 | } 568 | 569 | bool GameController::can_issue_card() const { 570 | const SSL_Referee &ref = state.referee(); 571 | return ref.command() == SSL_Referee::STOP || ref.command() == SSL_Referee::GOAL_YELLOW || ref.command() == SSL_Referee::GOAL_BLUE; 572 | } 573 | 574 | void GameController::yellow_card(SaveState::Team team) { 575 | SSL_Referee &ref = *state.mutable_referee(); 576 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(ref); 577 | logger.write(Glib::ustring::compose(u8"Issuing yellow card to %1.", TeamMeta::ALL[team].COLOUR)); 578 | 579 | // Add the yellow card. 580 | ti.add_yellow_card_times(configuration.yellow_card_seconds * 1000000U); 581 | ti.set_yellow_cards(ti.yellow_cards() + 1); 582 | 583 | // Record the card as the last card issued. 584 | state.mutable_last_card()->set_team(team); 585 | state.mutable_last_card()->set_card(SaveState::CARD_YELLOW); 586 | 587 | signal_other_changed.emit(); 588 | } 589 | 590 | void GameController::red_card(SaveState::Team team) { 591 | SSL_Referee &ref = *state.mutable_referee(); 592 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(ref); 593 | logger.write(Glib::ustring::compose(u8"Issuing red card to %1.", TeamMeta::ALL[team].COLOUR)); 594 | 595 | // Add the red card. 596 | ti.set_red_cards(ti.red_cards() + 1); 597 | 598 | // Record the card as the last card issued. 599 | state.mutable_last_card()->set_team(team); 600 | state.mutable_last_card()->set_card(SaveState::CARD_RED); 601 | 602 | signal_other_changed.emit(); 603 | } 604 | 605 | bool GameController::tick() { 606 | SSL_Referee &ref = *state.mutable_referee(); 607 | 608 | // Read how many microseconds passed since the last tick. 609 | uint32_t delta = timer.read_and_reset(); 610 | 611 | // Update the time since last state save and save if necessary. 612 | microseconds_since_last_state_save += delta; 613 | if (microseconds_since_last_state_save > STATE_SAVE_INTERVAL) { 614 | microseconds_since_last_state_save = 0; 615 | save_game(state, configuration.save_filename); 616 | } 617 | 618 | // Pull out the current command for checking against. 619 | SSL_Referee::Command command = ref.command(); 620 | 621 | // Check if this is a half-time-like stage. 622 | SSL_Referee::Stage stage = ref.stage(); 623 | bool half_time_like = stage == SSL_Referee::NORMAL_HALF_TIME 624 | || stage == SSL_Referee::EXTRA_TIME_BREAK 625 | || stage == SSL_Referee::EXTRA_HALF_TIME 626 | || stage == SSL_Referee::PENALTY_SHOOTOUT_BREAK; 627 | 628 | bool stopped_game_time = command == SSL_Referee::HALT 629 | || command == SSL_Referee::STOP 630 | || command == SSL_Referee::BALL_PLACEMENT_BLUE 631 | || command == SSL_Referee::BALL_PLACEMENT_YELLOW 632 | || command == SSL_Referee::GOAL_BLUE 633 | || command == SSL_Referee::GOAL_YELLOW 634 | || command == SSL_Referee_Command::SSL_Referee_Command_PREPARE_PENALTY_YELLOW 635 | || command == SSL_Referee_Command::SSL_Referee_Command_PREPARE_PENALTY_BLUE 636 | || command == SSL_Referee_Command::SSL_Referee_Command_PREPARE_KICKOFF_YELLOW 637 | || command == SSL_Referee_Command::SSL_Referee_Command_PREPARE_KICKOFF_BLUE; 638 | 639 | // Check if this is a pre-game stage. 640 | bool pre_game = stage == SSL_Referee::NORMAL_FIRST_HALF_PRE || stage == SSL_Referee::NORMAL_SECOND_HALF_PRE || stage == SSL_Referee::EXTRA_FIRST_HALF_PRE || stage == SSL_Referee::EXTRA_SECOND_HALF_PRE; 641 | 642 | // Run some clocks. 643 | if (command == SSL_Referee::TIMEOUT_YELLOW || command == SSL_Referee::TIMEOUT_BLUE) { 644 | // While a team is in a timeout, only its timeout clock runs. 645 | SaveState::Team team = TeamMeta::command_team(command); 646 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(ref); 647 | uint32_t old_left = ti.timeout_time(); 648 | uint32_t old_tenths = old_left / 100000; 649 | uint32_t new_left = old_left > delta ? old_left - delta : 0; 650 | uint32_t new_tenths = new_left / 100000; 651 | ti.set_timeout_time(new_left); 652 | if (new_tenths != old_tenths) { 653 | signal_timeout_time_changed.emit(); 654 | } 655 | } else if (!stopped_game_time || half_time_like) { 656 | // Otherwise, as long as we are not in halt OR we are in a half-time-like stage, the stage clock runs, if this particular stage *has* a stage clock. 657 | // There are two game clocks, the stage time left clock and the stage time taken clock. 658 | // The stage time left clock may or may not exist; the stage time taken clock always exists. 659 | // Keep both in lockstep. 660 | { 661 | bool emit = false; 662 | if (ref.has_stage_time_left()) { 663 | int32_t old_left = ref.stage_time_left(); 664 | int32_t old_tenths = old_left / 100000; 665 | int32_t new_left = old_left - static_cast(delta); 666 | int32_t new_tenths = new_left / 100000; 667 | ref.set_stage_time_left(new_left); 668 | if (new_tenths != old_tenths) { 669 | emit = true; 670 | } 671 | } 672 | { 673 | uint64_t old_taken = state.time_taken(); 674 | uint64_t old_tenths = old_taken / 100000; 675 | uint64_t new_taken = old_taken + delta; 676 | uint64_t new_tenths = new_taken / 100000; 677 | state.set_time_taken(new_taken); 678 | if (new_tenths != old_tenths) { 679 | emit = true; 680 | } 681 | } 682 | if (emit) { 683 | signal_game_clock_changed.emit(); 684 | } 685 | } 686 | 687 | // Also, in all of these states except a half-time-like or pre-game state, yellow cards count down. 688 | if (!half_time_like && !pre_game) { 689 | for (unsigned int teami = 0; teami < 2; ++teami) { 690 | SaveState::Team team = static_cast(teami); 691 | SSL_Referee::TeamInfo &ti = TeamMeta::ALL[team].team_info(ref); 692 | if (ti.yellow_card_times_size()) { 693 | // Tick down all the counters. 694 | bool emit = false; 695 | for (int j = 0; j < ti.yellow_card_times_size(); ++j) { 696 | uint32_t old_left = ti.yellow_card_times(j); 697 | uint32_t old_tenths = old_left / 100000; 698 | uint32_t new_left = old_left > delta ? old_left - delta : 0; 699 | uint32_t new_tenths = new_left / 100000; 700 | ti.set_yellow_card_times(j, new_left); 701 | if (!j && new_tenths != old_tenths) { 702 | emit = true; 703 | } 704 | } 705 | 706 | // Remove any that are at zero. 707 | if (!ti.yellow_card_times(0)) { 708 | auto last_valid = std::remove(ti.mutable_yellow_card_times()->begin(), ti.mutable_yellow_card_times()->end(), 0); 709 | ti.mutable_yellow_card_times()->Truncate(static_cast(last_valid - ti.mutable_yellow_card_times()->begin())); 710 | emit = true; 711 | } 712 | 713 | // If we have reached zero yellow cards for this team, we may need to clear the save state’s idea of the last issued card so it doesn’t try to cancel a missing card. 714 | if (!ti.yellow_card_times_size()) { 715 | if (state.has_last_card()) { 716 | if (state.last_card().team() == team && state.last_card().card() == SaveState::CARD_YELLOW) { 717 | state.clear_last_card(); 718 | signal_other_changed.emit(); 719 | } 720 | } 721 | } 722 | 723 | if (emit) { 724 | signal_yellow_card_time_changed.emit(); 725 | } 726 | } 727 | } 728 | } 729 | } 730 | 731 | // Publish the current state. 732 | for (Publisher *pub : publishers) { 733 | pub->publish(state); 734 | } 735 | 736 | return true; 737 | } 738 | 739 | void GameController::advance_from_pre() { 740 | switch (state.referee().stage()) { 741 | case SSL_Referee::NORMAL_FIRST_HALF_PRE: enter_stage(SSL_Referee::NORMAL_FIRST_HALF); break; 742 | case SSL_Referee::NORMAL_SECOND_HALF_PRE: enter_stage(SSL_Referee::NORMAL_SECOND_HALF); break; 743 | case SSL_Referee::EXTRA_FIRST_HALF_PRE: enter_stage(SSL_Referee::EXTRA_FIRST_HALF); break; 744 | case SSL_Referee::EXTRA_SECOND_HALF_PRE: enter_stage(SSL_Referee::EXTRA_SECOND_HALF); break; 745 | default: break; 746 | } 747 | } 748 | -------------------------------------------------------------------------------- /gamecontroller.h: -------------------------------------------------------------------------------- 1 | #ifndef GAMECONTROLLER_H 2 | #define GAMECONTROLLER_H 3 | 4 | #include "noncopyable.h" 5 | #include "referee.pb.h" 6 | #include "savestate.pb.h" 7 | #include "timing.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class Configuration; 16 | class GameInfo; 17 | class Logger; 18 | class Publisher; 19 | 20 | class GameController : public NonCopyable { 21 | public: 22 | enum class CancelType { 23 | NONE, 24 | CARD, 25 | TIMEOUT, 26 | TIMEOUT_END, 27 | }; 28 | 29 | SaveState state; 30 | const Configuration &configuration; 31 | Logger &logger; 32 | sigc::signal signal_timeout_time_changed, signal_game_clock_changed, signal_yellow_card_time_changed, signal_teamname_changed, signal_other_changed; 33 | 34 | GameController(Logger &logger, const Configuration &configuration, const std::vector &publishers, const std::string &resume_filename); 35 | ~GameController(); 36 | 37 | bool can_enter_stage(SSL_Referee::Stage stage) const; 38 | void enter_stage(SSL_Referee::Stage stage); 39 | SSL_Referee::Stage next_half_time() const; 40 | 41 | bool can_set_command(SSL_Referee::Command command) const; 42 | static bool command_needs_designated_position(SSL_Referee::Command command); 43 | 44 | void set_game_event(const SSL_Referee_Game_Event *game_event = NULL); 45 | void set_command(SSL_Referee::Command command, float designated_x = 0.0f, float designated_y = 0.0f, bool cancelling_timeout_end = false); 46 | 47 | void set_teamname(SaveState::Team team, const Glib::ustring &name); 48 | 49 | bool can_set_goalie() const; 50 | void set_goalie(SaveState::Team team, unsigned int goalie); 51 | 52 | bool can_switch_colours() const; 53 | bool can_switch_sides() const; 54 | void switch_colours(); 55 | void switch_sides(bool blueTeamOnPositiveHalf); 56 | 57 | bool can_subtract_goal(SaveState::Team team) const; 58 | void subtract_goal(SaveState::Team team); 59 | 60 | CancelType cancel_type() const; 61 | void cancel(); 62 | 63 | bool can_issue_card() const; 64 | void yellow_card(SaveState::Team team); 65 | void red_card(SaveState::Team team); 66 | 67 | private: 68 | const std::vector &publishers; 69 | sigc::connection tick_connection; 70 | MicrosecondCounter timer; 71 | uint64_t microseconds_since_last_state_save; 72 | 73 | bool tick(); 74 | void advance_from_pre(); 75 | }; 76 | 77 | #endif 78 | 79 | -------------------------------------------------------------------------------- /installDeps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # stop on errors 4 | set -e 5 | 6 | if [ "`id -nu`" != "root" ]; then 7 | echo "Must be called as root" 8 | exit 1 9 | fi 10 | 11 | if which dnf 2>/dev/null >/dev/null; then 12 | FLAGS="-y" 13 | dnf $FLAGS install cmake gcc-c++ git protobuf-compiler gtkmm24-devel 14 | fi 15 | 16 | if which apt-get 2>/dev/null >/dev/null; then 17 | FLAGS="-qq -y" 18 | apt-get $FLAGS install cmake g++ git libgtkmm-2.4-dev libprotobuf-dev protobuf-compiler 19 | fi 20 | 21 | if which emerge 2>/dev/null >/dev/null; then 22 | emerge dev-cpp/gtkmm:2.4 dev-libs/protobuf dev-vcs/git 23 | fi 24 | 25 | if which pacman 2>/dev/null >/dev/null; then 26 | pacman --noconfirm -S cmake gcc git protobuf gtkmm 27 | fi 28 | -------------------------------------------------------------------------------- /keypad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/keypad.png -------------------------------------------------------------------------------- /keypad.xfig: -------------------------------------------------------------------------------- 1 | #FIG 3.2 Produced by xfig version 3.2.5-alpha5 2 | Landscape 3 | Center 4 | Metric 5 | A4 6 | 100.00 7 | Single 8 | -2 9 | 1200 2 10 | 2 4 0 1 0 7 50 -1 -1 0.000 0 0 7 0 0 5 11 | 5850 4050 5850 2250 4050 2250 4050 4050 5850 4050 12 | 2 4 0 1 0 7 50 -1 -1 0.000 0 0 7 0 0 5 13 | 5850 6300 5850 4500 4050 4500 4050 6300 5850 6300 14 | 2 4 0 1 0 7 50 -1 -1 0.000 0 0 7 0 0 5 15 | 5850 1800 5850 0 4050 0 4050 1800 5850 1800 16 | 2 4 0 1 0 7 50 -1 -1 0.000 0 0 7 0 0 5 17 | 8100 8550 8100 6750 6300 6750 6300 8550 8100 8550 18 | 2 4 0 1 0 7 50 -1 -1 0.000 0 0 7 0 0 5 19 | 5850 8550 5850 6750 1800 6750 1800 8550 5850 8550 20 | 2 4 0 1 0 6 50 -1 30 0.000 0 0 7 0 0 5 21 | 3600 4050 1800 4050 1800 2250 3600 2250 3600 4050 22 | 2 4 0 1 0 1 50 -1 30 0.000 0 0 7 0 0 5 23 | 8100 4050 6300 4050 6300 2250 8100 2250 8100 4050 24 | 2 4 0 1 0 7 50 -1 18 0.000 0 0 7 0 0 5 25 | 10350 8550 8550 8550 8550 4500 10350 4500 10350 8550 26 | 2 4 0 1 0 7 50 -1 18 0.000 0 0 7 0 0 5 27 | 10350 4050 8550 4050 8550 0 10350 0 10350 4050 28 | 2 4 0 1 0 7 50 -1 18 0.000 0 0 7 0 0 5 29 | 10350 -450 8550 -450 8550 -2250 10350 -2250 10350 -450 30 | 2 4 0 1 0 7 50 -1 18 0.000 0 0 7 0 0 5 31 | 8100 -450 6300 -450 6300 -2250 8100 -2250 8100 -450 32 | 2 4 0 1 0 7 50 -1 18 0.000 0 0 7 0 0 5 33 | 5850 -450 4050 -450 4050 -2250 5850 -2250 5850 -450 34 | 2 4 0 1 0 7 50 -1 18 0.000 0 0 7 0 0 5 35 | 3600 -450 1800 -450 1800 -2250 3600 -2250 3600 -450 36 | 2 4 0 1 0 6 50 -1 30 0.000 0 0 7 0 0 5 37 | 3600 6300 1800 6300 1800 4500 3600 4500 3600 6300 38 | 2 4 0 1 0 1 50 -1 30 0.000 0 0 7 0 0 5 39 | 8100 6300 6300 6300 6300 4500 8100 4500 8100 6300 40 | 2 4 0 1 0 6 50 -1 30 0.000 0 0 7 0 0 5 41 | 3600 1800 1800 1800 1800 0 3600 0 3600 1800 42 | 2 4 0 1 0 1 50 -1 30 0.000 0 0 7 0 0 5 43 | 8100 1800 6300 1800 6300 0 8100 0 8100 1800 44 | 4 1 0 50 -1 0 16 0.0000 4 180 1320 2700 3465 FREEKICK\001 45 | 4 1 0 50 -1 0 16 0.0000 4 180 1260 7200 3150 INDIRECT\001 46 | 4 1 0 50 -1 0 16 0.0000 4 180 1320 7200 3465 FREEKICK\001 47 | 4 1 0 50 -1 0 16 0.0000 4 180 855 4950 3465 START\001 48 | 4 1 0 50 -1 0 16 0.0000 4 180 660 3825 7650 STOP\001 49 | 4 1 0 50 -1 0 16 0.0000 4 180 1260 2700 3150 INDIRECT\001 50 | 4 1 0 50 -1 0 16 0.0000 4 180 870 4950 3150 FORCE\001 51 | 4 1 0 50 -1 0 16 0.0000 4 180 930 9450 6525 READY\001 52 | 4 1 0 50 -1 0 12 0.0000 4 135 1500 9450 6750 NORMAL START\001 53 | 4 1 0 50 -1 0 16 0.0000 4 180 720 7200 7650 HALT\001 54 | 4 1 0 50 -1 0 16 0.0000 4 180 1155 2700 5400 KICKOFF\001 55 | 4 1 0 50 -1 0 16 0.0000 4 180 1155 7200 5400 KICKOFF\001 56 | 4 1 0 50 -1 0 16 0.0000 4 180 975 2700 900 DIRECT\001 57 | 4 1 0 50 -1 0 16 0.0000 4 180 1320 2700 1215 FREEKICK\001 58 | 4 1 0 50 -1 0 16 0.0000 4 180 975 7200 900 DIRECT\001 59 | 4 1 0 50 -1 0 16 0.0000 4 180 1320 7200 1215 FREEKICK\001 60 | 4 1 0 50 -1 0 16 0.0000 4 180 750 7200 -1350 GOAL\001 61 | 4 1 0 50 -1 0 16 0.0000 4 180 705 7200 -1035 BLUE\001 62 | 4 1 0 50 -1 0 16 0.0000 4 180 750 4950 -1350 GOAL\001 63 | 4 1 0 50 -1 0 16 0.0000 4 180 1140 4950 -1035 YELLOW\001 64 | -------------------------------------------------------------------------------- /legacypublisher.cc: -------------------------------------------------------------------------------- 1 | #include "legacypublisher.h" 2 | #include "configuration.h" 3 | #include "savestate.pb.h" 4 | #include 5 | #include 6 | 7 | namespace { 8 | char map_stage(SSL_Referee::Stage stage) { 9 | switch (stage) { 10 | case SSL_Referee::NORMAL_FIRST_HALF_PRE: return '1'; 11 | case SSL_Referee::NORMAL_FIRST_HALF: return ' '; 12 | case SSL_Referee::NORMAL_HALF_TIME: return 'h'; 13 | case SSL_Referee::NORMAL_SECOND_HALF_PRE: return '2'; 14 | case SSL_Referee::NORMAL_SECOND_HALF: return ' '; 15 | case SSL_Referee::EXTRA_TIME_BREAK: return 'h'; 16 | case SSL_Referee::EXTRA_FIRST_HALF_PRE: return 'o'; 17 | case SSL_Referee::EXTRA_FIRST_HALF: return ' '; 18 | case SSL_Referee::EXTRA_HALF_TIME: return 'h'; 19 | case SSL_Referee::EXTRA_SECOND_HALF_PRE: return 'O'; 20 | case SSL_Referee::EXTRA_SECOND_HALF: return ' '; 21 | case SSL_Referee::PENALTY_SHOOTOUT_BREAK: return 'h'; 22 | case SSL_Referee::PENALTY_SHOOTOUT: return 'a'; 23 | case SSL_Referee::POST_GAME: return 'H'; 24 | } 25 | 26 | throw std::logic_error("Impossible state!"); 27 | } 28 | 29 | char map_command(SSL_Referee::Command command) { 30 | switch (command) { 31 | case SSL_Referee::HALT: return 'H'; 32 | case SSL_Referee::STOP: return 'S'; 33 | case SSL_Referee::NORMAL_START: return ' '; 34 | case SSL_Referee::FORCE_START: return 's'; 35 | case SSL_Referee::PREPARE_KICKOFF_YELLOW: return 'k'; 36 | case SSL_Referee::PREPARE_KICKOFF_BLUE: return 'K'; 37 | case SSL_Referee::PREPARE_PENALTY_YELLOW: return 'p'; 38 | case SSL_Referee::PREPARE_PENALTY_BLUE: return 'P'; 39 | case SSL_Referee::DIRECT_FREE_YELLOW: return 'f'; 40 | case SSL_Referee::DIRECT_FREE_BLUE: return 'F'; 41 | case SSL_Referee::INDIRECT_FREE_YELLOW: return 'i'; 42 | case SSL_Referee::INDIRECT_FREE_BLUE: return 'I'; 43 | case SSL_Referee::TIMEOUT_YELLOW: return 't'; 44 | case SSL_Referee::TIMEOUT_BLUE: return 'T'; 45 | case SSL_Referee::GOAL_YELLOW: return 'g'; 46 | case SSL_Referee::GOAL_BLUE: return 'G'; 47 | case SSL_Referee::BALL_PLACEMENT_YELLOW: return 'S'; 48 | case SSL_Referee::BALL_PLACEMENT_BLUE: return 'S'; 49 | } 50 | 51 | throw std::logic_error("Impossible state!"); 52 | } 53 | } 54 | 55 | LegacyPublisher::LegacyPublisher(const Configuration &configuration, Logger &logger) : bcast(logger, configuration.address, configuration.legacy_port, configuration.interface), cached_command_char('H'), last_stage(SSL_Referee::NORMAL_FIRST_HALF_PRE), last_command(SSL_Referee::HALT), last_yellow_ycards(0), last_blue_ycards(0), last_yellow_rcards(0), last_blue_rcards(0) { 56 | } 57 | 58 | void LegacyPublisher::publish(SaveState &state) { 59 | // Encode the packet. 60 | uint8_t packet[6]; 61 | packet[0] = static_cast(compute_command(state.referee())); 62 | packet[1] = static_cast(state.referee().command_counter()); 63 | packet[2] = static_cast(state.referee().blue().score()); 64 | packet[3] = static_cast(state.referee().yellow().score()); 65 | if (state.referee().has_stage_time_left() && state.referee().stage_time_left() >= 0) { 66 | int left = state.referee().stage_time_left() / 1000000; 67 | if (left <= 65535) { 68 | packet[4] = static_cast(left / 256); 69 | packet[5] = static_cast(left); 70 | } else { 71 | packet[4] = 0xFF; 72 | packet[5] = 0xFF; 73 | } 74 | } else { 75 | packet[4] = 0; 76 | packet[5] = 0; 77 | } 78 | 79 | // Send the packet. 80 | bcast.send(packet, sizeof(packet)); 81 | } 82 | 83 | char LegacyPublisher::compute_command(const SSL_Referee &state) { 84 | enum class Disposition { 85 | STAGE, 86 | COMMAND, 87 | YELLOW_YCARD, 88 | BLUE_YCARD, 89 | YELLOW_RCARD, 90 | BLUE_RCARD, 91 | CACHE, 92 | } disposition; 93 | 94 | if (state.stage() != last_stage) { 95 | // We have just changed from one stage to another. 96 | // We should announce the new stage. 97 | disposition = Disposition::STAGE; 98 | } else if (state.command() != last_command) { 99 | // We have just changed from one command to another. 100 | // We should announce the new command. 101 | disposition = Disposition::COMMAND; 102 | } else if (state.yellow().yellow_card_times_size() > last_yellow_ycards) { 103 | // Yellow has more yellow cards than before. 104 | // Announce a yellow card issued. 105 | disposition = Disposition::YELLOW_YCARD; 106 | } else if (state.blue().yellow_card_times_size() > last_blue_ycards) { 107 | // Blue has more yellow cards than before. 108 | // Announce a yellow card issued. 109 | disposition = Disposition::BLUE_YCARD; 110 | } else if (state.yellow().red_cards() > last_yellow_rcards) { 111 | // Yellow has more red cards than before. 112 | // Announce a red card issued. 113 | disposition = Disposition::YELLOW_RCARD; 114 | } else if (state.blue().red_cards() > last_blue_rcards) { 115 | // Blue has more red cards than before. 116 | // Announce a red card issued. 117 | disposition = Disposition::BLUE_RCARD; 118 | } else { 119 | // If nothing has changed, just return the cached value from last time. 120 | disposition = Disposition::CACHE; 121 | } 122 | 123 | // Update all saved state to match the current state. 124 | last_stage = state.stage(); 125 | last_command = state.command(); 126 | last_yellow_ycards = state.yellow().yellow_card_times_size(); 127 | last_blue_ycards = state.blue().yellow_card_times_size(); 128 | last_yellow_rcards = state.yellow().red_cards(); 129 | last_blue_rcards = state.blue().red_cards(); 130 | 131 | // Choose a command to announce based on the decision made above. 132 | switch (disposition) { 133 | case Disposition::STAGE: return cached_command_char = map_stage(state.stage()); 134 | case Disposition::COMMAND: return cached_command_char = map_command(state.command()); 135 | case Disposition::YELLOW_YCARD: return cached_command_char = 'y'; 136 | case Disposition::BLUE_YCARD: return cached_command_char = 'Y'; 137 | case Disposition::YELLOW_RCARD: return cached_command_char = 'r'; 138 | case Disposition::BLUE_RCARD: return cached_command_char = 'R'; 139 | case Disposition::CACHE: return cached_command_char; 140 | } 141 | 142 | throw std::logic_error("Impossible state!"); 143 | } 144 | 145 | -------------------------------------------------------------------------------- /legacypublisher.h: -------------------------------------------------------------------------------- 1 | #ifndef LEGACY_PUBLISHER_H 2 | #define LEGACY_PUBLISHER_H 3 | 4 | #include "noncopyable.h" 5 | #include "publisher.h" 6 | #include "referee.pb.h" 7 | #include "udpbroadcast.h" 8 | 9 | class Configuration; 10 | class Logger; 11 | class SaveState; 12 | 13 | class LegacyPublisher : public NonCopyable, public Publisher { 14 | public: 15 | LegacyPublisher(const Configuration &configuration, Logger &logger); 16 | void publish(SaveState &state); 17 | 18 | private: 19 | UDPBroadcast bcast; 20 | char cached_command_char; 21 | SSL_Referee::Stage last_stage; 22 | SSL_Referee::Command last_command; 23 | int last_yellow_ycards, last_blue_ycards; 24 | unsigned int last_yellow_rcards, last_blue_rcards; 25 | 26 | char compute_command(const SSL_Referee &state); 27 | }; 28 | 29 | #endif 30 | 31 | -------------------------------------------------------------------------------- /logger.cc: -------------------------------------------------------------------------------- 1 | #include "logger.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | Logger::Logger(const std::string &filename) : start_time(std::chrono::high_resolution_clock::now()) { 9 | if (!filename.empty()) { 10 | ofs.exceptions(std::ios_base::badbit | std::ios_base::failbit); 11 | ofs.open(filename, std::ios_base::out | std::ios_base::app); 12 | } 13 | std::time_t real_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); 14 | std::wostringstream oss; 15 | static const wchar_t TIME_PATTERN[] = L"%x %X %Z"; 16 | std::use_facet>(std::locale()).put(oss, oss, L' ', std::localtime(&real_time), TIME_PATTERN, TIME_PATTERN + std::wcslen(TIME_PATTERN)); 17 | write(Glib::ustring::compose(u8"Referee box started at %1.", oss.str())); 18 | } 19 | 20 | void Logger::write(const Glib::ustring &message) { 21 | std::chrono::high_resolution_clock::duration diff = std::chrono::high_resolution_clock::now() - start_time; 22 | double seconds = static_cast(std::chrono::duration_cast(diff).count()) / 1000.0; 23 | const Glib::ustring &prefixed = Glib::ustring::compose(u8"[%1] %2\n", Glib::ustring::format(std::fixed, std::setprecision(3), seconds), message); 24 | std::cout << prefixed; 25 | ofs << prefixed; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /logger.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGGER_H 2 | #define LOGGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class Logger { 10 | public: 11 | Logger(const std::string &log_filename); 12 | void write(const Glib::ustring &message); 13 | 14 | private: 15 | std::chrono::high_resolution_clock::time_point start_time; 16 | std::ofstream ofs; 17 | }; 18 | 19 | #endif 20 | 21 | -------------------------------------------------------------------------------- /main.cc: -------------------------------------------------------------------------------- 1 | #include "configuration.h" 2 | #include "gamecontroller.h" 3 | #include "legacypublisher.h" 4 | #include "logger.h" 5 | #include "mainwindow.h" 6 | #include "protobufpublisher.h" 7 | #include "publisher.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace { 23 | int main_impl(int argc, char **argv) { 24 | // Set the current locale. 25 | std::locale::global(std::locale("")); 26 | 27 | // Parse the command-line arguments. 28 | Glib::OptionContext option_context; 29 | option_context.set_summary(u8"Runs the RoboCup Small Size League Referee Box."); 30 | option_context.set_description(u8"The Referee Box is © RoboCup Federation, 2003–2013."); 31 | 32 | Glib::OptionGroup option_group(u8"referee", u8"Referee Box Options", u8"Show Referee Box Options"); 33 | 34 | Glib::OptionEntry config_file_entry; 35 | config_file_entry.set_long_name(u8"config"); 36 | config_file_entry.set_short_name('C'); 37 | config_file_entry.set_description(u8"Sets the name of the configuration file (defaults to referee.conf)."); 38 | config_file_entry.set_arg_description(u8"CONFIGFILE"); 39 | std::string config_filename("referee.conf"); 40 | option_group.add_entry_filename(config_file_entry, config_filename); 41 | 42 | Glib::OptionEntry resume_entry; 43 | resume_entry.set_long_name(u8"resume"); 44 | resume_entry.set_short_name('r'); 45 | resume_entry.set_description(u8"Resumes an in-progress game by loading a saved state file."); 46 | resume_entry.set_arg_description(u8"SAVEFILE"); 47 | std::string resume_filename; 48 | option_group.add_entry_filename(resume_entry, resume_filename); 49 | 50 | option_context.set_main_group(option_group); 51 | Gtk::Main kit(argc, argv, option_context); 52 | 53 | // Initialize the game objects. 54 | Configuration configuration(config_filename); 55 | 56 | // Start a logger. 57 | Logger logger(configuration.log_filename); 58 | configuration.dump(logger); 59 | 60 | // Construct the publishers. 61 | std::vector publishers; 62 | std::unique_ptr protobuf_publisher; 63 | if (!configuration.protobuf_port.empty()) { 64 | protobuf_publisher.reset(new ProtobufPublisher(configuration, logger)); 65 | publishers.push_back(protobuf_publisher.get()); 66 | } 67 | std::unique_ptr legacy_publisher; 68 | if (!configuration.legacy_port.empty()) { 69 | legacy_publisher.reset(new LegacyPublisher(configuration, logger)); 70 | publishers.push_back(legacy_publisher.get()); 71 | } 72 | 73 | // Construct the game controller that ties everything together. 74 | GameController controller(logger, configuration, publishers, resume_filename); 75 | 76 | // Create and display a main window. 77 | MainWindow main_window(controller); 78 | kit.run(main_window); 79 | 80 | // Shut down protobuf. 81 | google::protobuf::ShutdownProtobufLibrary(); 82 | 83 | return 0; 84 | } 85 | 86 | void print_exception(const Glib::Exception &exp) { 87 | std::cerr << "\nUnhandled exception:\n"; 88 | std::cerr << "Type: " << typeid(exp).name() << '\n'; 89 | std::cerr << "Detail: " << exp.what() << '\n'; 90 | } 91 | 92 | void print_exception(const std::exception &exp, bool first = true) { 93 | if (first) { 94 | std::cerr << "\nUnhandled exception:\n"; 95 | } else { 96 | std::cerr << "Caused by:\n"; 97 | } 98 | std::cerr << "Type: " << typeid(exp).name() << '\n'; 99 | std::cerr << "Detail: " << exp.what() << '\n'; 100 | try { 101 | std::rethrow_if_nested(exp); 102 | } catch (const std::exception &exp) { 103 | print_exception(exp, false); 104 | } 105 | } 106 | } 107 | 108 | int main(int argc, char **argv) { 109 | try { 110 | return main_impl(argc, argv); 111 | } catch (const Glib::Exception &exp) { 112 | print_exception(exp); 113 | } catch (const std::exception &exp) { 114 | print_exception(exp); 115 | } catch (...) { 116 | std::cerr << "\nUnhandled exception:\n"; 117 | std::cerr << "Type: Unknown\n"; 118 | std::cerr << "Detail: Unknown\n"; 119 | } 120 | return 1; 121 | } 122 | 123 | -------------------------------------------------------------------------------- /mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | class GameController; 20 | class RConServer; 21 | 22 | class MainWindow : public Gtk::Window { 23 | public: 24 | MainWindow(GameController &controller); 25 | ~MainWindow(); 26 | 27 | private: 28 | // Information about a game control button. 29 | struct GameControlButtonInfo { 30 | Gtk::Button MainWindow::*button; 31 | int new_stage; 32 | int new_command; 33 | }; 34 | 35 | void on_timeout_time_changed(); 36 | void on_game_clock_changed(); 37 | void on_yellow_card_time_changed(); 38 | void on_teamname_changed(); 39 | void on_other_changed(); 40 | 41 | void on_teamname_yellow_changed(); 42 | void on_teamname_blue_changed(); 43 | 44 | void on_goalie_yellow_changed(); 45 | void on_goalie_blue_changed(); 46 | 47 | bool on_goalie_yellow_commit(); 48 | bool on_goalie_blue_commit(); 49 | 50 | void on_game_control_button_clicked(const GameControlButtonInfo *button); 51 | void on_half_time_button_clicked(); 52 | 53 | void on_enable_rcon_clicked(); 54 | 55 | void on_switch_sides(); 56 | 57 | void on_toggle_pause_game(); 58 | 59 | void update_sensitivities(); 60 | 61 | // Information about the game control buttons. 62 | static const GameControlButtonInfo game_control_button_info[]; 63 | 64 | // The game controller. 65 | GameController &controller; 66 | 67 | // The remote control server. 68 | std::unique_ptr rcon_server; 69 | 70 | sigc::connection goalie_yellow_commit_connection, goalie_blue_commit_connection; 71 | 72 | // Elements 73 | Gtk::VBox big_vbox; 74 | 75 | Gtk::MenuBar menu_bar; 76 | Gtk::Menu config_menu; 77 | Gtk::CheckMenuItem ignore_rules_menu_item; 78 | Gtk::CheckMenuItem enable_rcon_menu_item; 79 | 80 | Gtk::HBox halt_stop_hbox; 81 | Gtk::Button halt_but; 82 | Gtk::Button stop_but; 83 | Gtk::HBox start_hbox; 84 | Gtk::Button force_start_but; 85 | Gtk::Button normal_start_but; 86 | 87 | Gtk::Frame goal_frame; 88 | Gtk::VBox goal_vbox; 89 | Gtk::HBox goal_hbox; 90 | Gtk::Label yellow_goal; 91 | Gtk::Label blue_goal; 92 | Gtk::Label vs; 93 | Gtk::HBox teamname_hbox; 94 | Gtk::ComboBoxText teamname_yellow; 95 | Gtk::ComboBoxText teamname_blue; 96 | Gtk::Button switch_colours_but; 97 | 98 | Gtk::HBox teamSide_hbox; 99 | Gtk::RadioButtonGroup teamSide_group; 100 | Gtk::RadioButton teamSide_yellow; 101 | Gtk::RadioButton teamSide_blue; 102 | 103 | Gtk::HBox game_status_hbox; 104 | Gtk::VBox game_status_vbox; 105 | 106 | Gtk::Frame game_control_frame; 107 | Gtk::Label stage_label; 108 | Gtk::Label time_label; 109 | Gtk::Label timeleft_label; 110 | Gtk::Label game_status_label; 111 | Gtk::VButtonBox game_control_box; 112 | Gtk::Button cancel_but; 113 | Gtk::HBox yellow_goal_box; 114 | Gtk::Button yellow_goal_but; 115 | Gtk::Button yellow_subgoal_but; 116 | Gtk::HBox blue_goal_box; 117 | Gtk::Button blue_goal_but; 118 | Gtk::Button blue_subgoal_but; 119 | Gtk::ToggleButton pause_game_but; 120 | 121 | Gtk::VBox game_stage_control_left_vbox; 122 | Gtk::VBox game_stage_control_right_vbox; 123 | Gtk::Label next_stage_label_left; 124 | Gtk::Label next_stage_label_right; 125 | Gtk::Button firsthalf_start_but; 126 | Gtk::Button halftime_start_but; 127 | Gtk::Button secondhalf_start_but; 128 | Gtk::Button overtime1_start_but; 129 | Gtk::Button overtime2_start_but; 130 | Gtk::Button penaltyshootout_start_but; 131 | Gtk::Button gameover_start_but; 132 | 133 | Gtk::HBox team_hbox; 134 | 135 | Gtk::Frame yellow_frame; 136 | Gtk::Table yellow_team_table; 137 | Gtk::Button yellow_timeout_start_but; 138 | Gtk::Label yellow_timeout_time_label; 139 | Gtk::Label yellow_timeout_time_text; 140 | Gtk::Label yellow_timeouts_left_label; 141 | Gtk::Label yellow_timeouts_left_text; 142 | Gtk::Label yellow_goalie_label; 143 | Gtk::SpinButton yellow_goalie_spin; 144 | Gtk::Button yellow_kickoff_but; 145 | Gtk::Button yellow_freekick_but; 146 | Gtk::Button yellow_penalty_but; 147 | Gtk::Button yellow_indirect_freekick_but; 148 | Gtk::Button yellow_yellowcard_but; 149 | Gtk::Button yellow_redcard_but; 150 | 151 | Gtk::Frame blue_frame; 152 | Gtk::Table blue_team_table; 153 | Gtk::Button blue_timeout_start_but; 154 | Gtk::Label blue_timeout_time_label; 155 | Gtk::Label blue_timeout_time_text; 156 | Gtk::Label blue_timeouts_left_label; 157 | Gtk::Label blue_timeouts_left_text; 158 | Gtk::Label blue_goalie_label; 159 | Gtk::SpinButton blue_goalie_spin; 160 | Gtk::Button blue_kickoff_but; 161 | Gtk::Button blue_freekick_but; 162 | Gtk::Button blue_penalty_but; 163 | Gtk::Button blue_indirect_freekick_but; 164 | Gtk::Button blue_yellowcard_but; 165 | Gtk::Button blue_redcard_but; 166 | }; 167 | 168 | #endif 169 | 170 | -------------------------------------------------------------------------------- /noncopyable.h: -------------------------------------------------------------------------------- 1 | #ifndef NONCOPYABLE_H 2 | #define NONCOPYABLE_H 3 | 4 | class NonCopyable { 5 | public: 6 | NonCopyable(const NonCopyable &) = delete; 7 | NonCopyable &operator=(const NonCopyable &) = delete; 8 | 9 | protected: 10 | NonCopyable(); 11 | }; 12 | 13 | 14 | 15 | inline NonCopyable::NonCopyable() { 16 | } 17 | 18 | #endif 19 | 20 | -------------------------------------------------------------------------------- /protobufpublisher.cc: -------------------------------------------------------------------------------- 1 | #include "protobufpublisher.h" 2 | #include "configuration.h" 3 | #include "referee.pb.h" 4 | #include "savestate.pb.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | ProtobufPublisher::ProtobufPublisher(const Configuration &configuration, Logger &logger) : bcast(logger, configuration.address, configuration.protobuf_port, configuration.interface) { 11 | } 12 | 13 | void ProtobufPublisher::publish(SaveState &state) { 14 | // Shove in the packet timestamp. 15 | std::chrono::microseconds diff = std::chrono::duration_cast(std::chrono::system_clock::now() - std::chrono::system_clock::from_time_t(0)); 16 | state.mutable_referee()->set_packet_timestamp(static_cast(diff.count())); 17 | 18 | // Serialize the packet. 19 | std::string packet; 20 | { 21 | google::protobuf::io::StringOutputStream sos(&packet); 22 | state.referee().SerializeToZeroCopyStream(&sos); 23 | } 24 | 25 | // Send the packet. 26 | bcast.send(packet.data(), packet.size()); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /protobufpublisher.h: -------------------------------------------------------------------------------- 1 | #ifndef PROTOBUF_PUBLISHER_H 2 | #define PROTOBUF_PUBLISHER_H 3 | 4 | #include "noncopyable.h" 5 | #include "publisher.h" 6 | #include "udpbroadcast.h" 7 | 8 | class Configuration; 9 | class Logger; 10 | 11 | class ProtobufPublisher : public NonCopyable, public Publisher { 12 | public: 13 | ProtobufPublisher(const Configuration &configuration, Logger &logger); 14 | void publish(SaveState &state); 15 | 16 | private: 17 | UDPBroadcast bcast; 18 | }; 19 | 20 | #endif 21 | 22 | -------------------------------------------------------------------------------- /publisher.h: -------------------------------------------------------------------------------- 1 | #ifndef PUBLISHER_H 2 | #define PUBLISHER_H 3 | 4 | class SaveState; 5 | 6 | class Publisher { 7 | public: 8 | virtual void publish(SaveState &state) = 0; 9 | }; 10 | 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /rcon-client/Makefile: -------------------------------------------------------------------------------- 1 | # Standard compiler and linker flags. 2 | PKG_CONFIG ?= pkg-config 3 | override CXXFLAGS := -std=gnu++0x -Wall -Wextra -Wold-style-cast -Wconversion -Wundef -O2 -g $(shell $(PKG_CONFIG) --cflags gtkmm-2.4 protobuf | sed 's/-I/-isystem /g') $(CXXFLAGS) 4 | override LDFLAGS := $(shell $(PKG_CONFIG) --libs-only-L --libs-only-other gtkmm-2.4 protobuf) 5 | override LDLIBS := $(shell $(PKG_CONFIG) --libs-only-l gtkmm-2.4 protobuf) 6 | 7 | # The default target. 8 | .PHONY : world 9 | world : rconclient 10 | 11 | # Gather lists of files of various types. 12 | protos := rcon.proto referee.proto game_event.proto 13 | proto_sources := $(patsubst %.proto,%.pb.cc,$(protos)) 14 | proto_headers := $(patsubst %.proto,%.pb.h,$(protos)) 15 | proto_objs := $(patsubst %.proto,%.pb.o,$(protos)) 16 | non_proto_sources := $(filter-out $(proto_sources),$(wildcard *.cc)) 17 | non_proto_headers := $(filter-out $(proto_headers),$(wildcard *.h)) 18 | non_proto_objs := $(patsubst %.cc,%.o,$(non_proto_sources)) 19 | all_sources := $(proto_sources) $(non_proto_sources) 20 | all_headers := $(proto_headers) $(non_proto_headers) 21 | all_objs := $(proto_objs) $(non_proto_objs) 22 | 23 | # Normal rule to link the final binary. 24 | rconclient : $(all_objs) 25 | @echo "LD $@" 26 | @$(CXX) $(LDFLAGS) -o $@ $+ $(LDLIBS) 27 | 28 | # Static pattern rule to compile a protobuf source file (with warnings disabled, as they make no sense here). 29 | $(proto_objs) : %.pb.o : %.pb.cc $(all_headers) 30 | @echo "CXX $@" 31 | @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -w -c $< 32 | 33 | # Static pattern rule to compile a non-protobuf source file. 34 | $(non_proto_objs) : %.o : %.cc $(all_headers) 35 | @echo "CXX $@" 36 | @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< 37 | 38 | # Pattern rule to run protoc on a message definition file. 39 | %.pb.cc %.pb.h : ../%.proto 40 | @echo "PROTO $(patsubst ../%.proto,%.pb.cc,$<)" 41 | @protoc --proto_path=.. --cpp_out=. $< 42 | 43 | # Rule to clean intermediates and outputs. 44 | .PHONY : clean 45 | clean : 46 | $(RM) rconclient *.o *.pb.cc *.pb.h 47 | -------------------------------------------------------------------------------- /rcon-client/rconclient.cc: -------------------------------------------------------------------------------- 1 | #include "rcon.pb.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #if defined(WIN32) 15 | #include 16 | #else 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #endif 24 | 25 | #define MAX_REPLY_LENGTH 4096 26 | 27 | namespace { 28 | void usage(const char *appName) { 29 | std::cerr << "Usage:\n" << appName << " refbox_address [refbox_rcon_port]\n"; 30 | std::exit(EXIT_FAILURE); 31 | } 32 | 33 | bool recvFully(int fd, void *buffer, std::size_t length) { 34 | char *ptr = static_cast(buffer); 35 | while (length) { 36 | ssize_t ret = recv(fd, ptr, length, 0); 37 | if (ret < 0) { 38 | std::cerr << std::strerror(errno) << '\n'; 39 | return false; 40 | } else if (!ret) { 41 | std::cerr << "Socket closed by remote peer.\n"; 42 | return false; 43 | } 44 | ptr += ret; 45 | length -= ret; 46 | } 47 | return true; 48 | } 49 | 50 | bool sendFully(int fd, const void *buffer, std::size_t length) { 51 | const char *ptr = static_cast(buffer); 52 | while (length) { 53 | ssize_t ret = send(fd, ptr, length, 0); 54 | if (ret < 0) { 55 | std::cerr << std::strerror(errno) << '\n'; 56 | return false; 57 | } 58 | ptr += ret; 59 | length -= ret; 60 | } 61 | return true; 62 | } 63 | 64 | uint32_t allocMessageID() { 65 | static uint32_t nextMessageID = 0; 66 | return nextMessageID++; 67 | } 68 | 69 | void doExit(int sock) { 70 | std::cout << "Goodbye.\n"; 71 | close(sock); 72 | std::exit(EXIT_SUCCESS); 73 | } 74 | 75 | void doRequest(int sock, const SSL_RefereeRemoteControlRequest &request) { 76 | { 77 | const std::string &message = request.SerializeAsString(); 78 | uint32_t messageLength = htonl(static_cast(message.size())); 79 | std::cout << "Send " << (sizeof(messageLength) + message.size()) << " bytes: "; 80 | std::cout.flush(); 81 | if (!sendFully(sock, &messageLength, sizeof(messageLength)) || !sendFully(sock, message.data(), message.size())) { 82 | doExit(sock); 83 | } 84 | std::cout << "OK\n"; 85 | } 86 | 87 | SSL_RefereeRemoteControlReply reply; 88 | { 89 | std::cout << "Receive reply: "; 90 | std::cout.flush(); 91 | uint32_t replyLength; 92 | if (!recvFully(sock, &replyLength, sizeof(replyLength))) { 93 | doExit(sock); 94 | } 95 | replyLength = ntohl(replyLength); 96 | if (replyLength > MAX_REPLY_LENGTH) { 97 | std::cerr << "Got reply length " << replyLength << " which is greater than limit " << MAX_REPLY_LENGTH << ".\n"; 98 | doExit(sock); 99 | } 100 | std::vector buffer(replyLength); 101 | if (!recvFully(sock, &buffer[0], replyLength)) { 102 | doExit(sock); 103 | } 104 | std::cout << (4 + replyLength) << " bytes OK.\n"; 105 | if (!reply.ParseFromArray(&buffer[0], replyLength)) { 106 | std::cerr << "Error in reply message structure.\n"; 107 | doExit(sock); 108 | } 109 | } 110 | if (reply.message_id() != request.message_id()) { 111 | std::cerr << "Reply message ID " << reply.message_id() << " does not match request message ID " << request.message_id() << ".\n"; 112 | doExit(sock); 113 | } 114 | std::cout << "Command result is: " << SSL_RefereeRemoteControlReply_Outcome_descriptor()->FindValueByNumber(reply.outcome())->name() << ".\n"; 115 | } 116 | 117 | void doHelp(const std::vector &commands) { 118 | std::cout << "Available commands are “help”, “exit”, “ping”, “card”, and the name of any game stage or\nreferee command converted to lowercase with underscores replaced by spaces.\nThe full list of commands is:\n"; 119 | for (const std::string &command : commands) { 120 | std::cout << "- " << command << '\n'; 121 | } 122 | } 123 | 124 | void doCard(int sock) { 125 | SSL_RefereeRemoteControlRequest request; 126 | request.set_message_id(allocMessageID()); 127 | for (;;) { 128 | std::cout << "[Y]ellow or [R]ed card> "; 129 | std::cout.flush(); 130 | std::string line; 131 | if (!std::getline(std::cin, line)) { 132 | doExit(sock); 133 | } 134 | if (line == "y" || line == "Y") { 135 | request.mutable_card()->set_type(SSL_RefereeRemoteControlRequest::CardInfo::CARD_YELLOW); 136 | break; 137 | } else if (line == "r" || line == "R") { 138 | request.mutable_card()->set_type(SSL_RefereeRemoteControlRequest::CardInfo::CARD_RED); 139 | break; 140 | } 141 | } 142 | for (;;) { 143 | std::cout << "[Y]ellow or [B]lue team> "; 144 | std::cout.flush(); 145 | std::string line; 146 | if (!std::getline(std::cin, line)) { 147 | doExit(sock); 148 | } 149 | if (line == "y" || line == "Y") { 150 | request.mutable_card()->set_team(SSL_RefereeRemoteControlRequest::CardInfo::TEAM_YELLOW); 151 | break; 152 | } else if (line == "b" || line == "B") { 153 | request.mutable_card()->set_team(SSL_RefereeRemoteControlRequest::CardInfo::TEAM_BLUE); 154 | break; 155 | } 156 | } 157 | doRequest(sock, request); 158 | } 159 | 160 | void doCommand(int sock, bool hasStage, SSL_Referee::Stage stage, bool hasCommand, SSL_Referee::Command command, bool hasDesignatedPosition) { 161 | SSL_RefereeRemoteControlRequest request; 162 | request.set_message_id(allocMessageID()); 163 | if (hasStage) { 164 | request.set_stage(stage); 165 | } 166 | if (hasCommand) { 167 | request.set_command(command); 168 | } 169 | 170 | if (hasDesignatedPosition) { 171 | static const char letters[] = "XY"; 172 | float pos[2]; 173 | for (unsigned int i = 0; i != 2; ++i) { 174 | for (;;) { 175 | std::cout << "Designated position " << letters[i] << "> "; 176 | std::cout.flush(); 177 | std::string line; 178 | if (!std::getline(std::cin, line)) { 179 | doExit(sock); 180 | } 181 | std::size_t endPos; 182 | try { 183 | pos[i] = std::stof(line, &endPos); 184 | if (endPos != line.size()) { 185 | std::cerr << "Invalid coordinate; must be a floating-point number.\n"; 186 | } else { 187 | break; 188 | } 189 | } catch (const std::invalid_argument &) { 190 | std::cerr << "Invalid coordinate; must be a floating-point number.\n"; 191 | } catch (const std::out_of_range &) { 192 | std::cerr << "Value is too large or too small.\n"; 193 | } 194 | } 195 | } 196 | request.mutable_designated_position()->set_x(pos[0]); 197 | request.mutable_designated_position()->set_y(pos[1]); 198 | } 199 | 200 | doRequest(sock, request); 201 | } 202 | } 203 | 204 | int main(int argc, char **argv) { 205 | if (argc < 2 || argc > 3) { 206 | usage(argv[0]); 207 | } 208 | 209 | // Parse target address. 210 | struct addrinfo *refboxAddresses; 211 | { 212 | struct addrinfo hints; 213 | hints.ai_flags = 0; 214 | hints.ai_family = AF_UNSPEC; 215 | hints.ai_socktype = SOCK_STREAM; 216 | hints.ai_protocol = 0; 217 | int err = getaddrinfo(argv[1], argc == 3 ? argv[2] : "10007", &hints, &refboxAddresses); 218 | if (err != 0) { 219 | std::cerr << gai_strerror(err) << '\n'; 220 | return EXIT_FAILURE; 221 | } 222 | } 223 | 224 | // Try to connect to the returned addresses until one of them succeeds. 225 | int sock = -1; 226 | for (const addrinfo *i = refboxAddresses; i; i = i->ai_next) { 227 | char host[256], port[32]; 228 | int err = getnameinfo(i->ai_addr, i->ai_addrlen, host, sizeof(host), port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV); 229 | if (err != 0) { 230 | std::cerr << gai_strerror(err) << '\n'; 231 | return EXIT_FAILURE; 232 | } 233 | std::cout << "Trying host " << host << " port " << port << ": "; 234 | std::cout.flush(); 235 | sock = socket(i->ai_family, i->ai_socktype, i->ai_protocol); 236 | if (sock < 0) { 237 | std::cout << std::strerror(errno) << '\n'; 238 | continue; 239 | } 240 | if (connect(sock, i->ai_addr, i->ai_addrlen) < 0) { 241 | std::cout << std::strerror(errno) << '\n'; 242 | close(sock); 243 | sock = -1; 244 | continue; 245 | } 246 | std::cout << "OK\n"; 247 | break; 248 | } 249 | if (sock < 0) { 250 | std::cerr << "Unable to connect to referee box.\n"; 251 | return EXIT_FAILURE; 252 | } 253 | 254 | // Free address info. 255 | freeaddrinfo(refboxAddresses); 256 | 257 | // Set up the command table. 258 | std::unordered_map> commands; 259 | std::vector commandsOrdered; 260 | commands["help"] = std::bind(&doHelp, std::ref(commandsOrdered)); 261 | commandsOrdered.push_back("help"); 262 | commands["exit"] = &doExit; 263 | commandsOrdered.push_back("exit"); 264 | commands["ping"] = std::bind(&doCommand, std::placeholders::_1, false, SSL_Referee::Stage_MIN, false, SSL_Referee::Command_MIN, false); 265 | commandsOrdered.push_back("ping"); 266 | commands["card"] = &doCard; 267 | commandsOrdered.push_back("card"); 268 | { 269 | const google::protobuf::EnumDescriptor *descriptor = SSL_Referee::Stage_descriptor(); 270 | for (int i = 0; i != descriptor->value_count(); ++i) { 271 | const google::protobuf::EnumValueDescriptor *valueDescriptor = descriptor->value(i); 272 | std::string name = valueDescriptor->name(); 273 | for (auto i = name.begin(), iEnd = name.end(); i != iEnd; ++i) { 274 | if (*i == '_') { 275 | *i = ' '; 276 | } else { 277 | *i = static_cast(std::tolower(*i)); 278 | } 279 | } 280 | commands[name] = std::bind(&doCommand, std::placeholders::_1, true, static_cast(valueDescriptor->number()), false, SSL_Referee::Command_MIN, false); 281 | commandsOrdered.push_back(name); 282 | } 283 | } 284 | { 285 | std::unordered_set hasDesignatedPosition { 286 | SSL_Referee::BALL_PLACEMENT_YELLOW, 287 | SSL_Referee::BALL_PLACEMENT_BLUE, 288 | }; 289 | 290 | const google::protobuf::EnumDescriptor *descriptor = SSL_Referee::Command_descriptor(); 291 | for (int i = 0; i != descriptor->value_count(); ++i) { 292 | const google::protobuf::EnumValueDescriptor *valueDescriptor = descriptor->value(i); 293 | std::string name = valueDescriptor->name(); 294 | for (auto i = name.begin(), iEnd = name.end(); i != iEnd; ++i) { 295 | if (*i == '_') { 296 | *i = ' '; 297 | } else { 298 | *i = static_cast(std::tolower(*i)); 299 | } 300 | } 301 | commands[name] = std::bind(&doCommand, std::placeholders::_1, false, SSL_Referee::Stage_MIN, true, static_cast(valueDescriptor->number()), hasDesignatedPosition.count(valueDescriptor->number()) != 0); 302 | commandsOrdered.push_back(name); 303 | } 304 | } 305 | 306 | // Run the remote control client. 307 | std::cout << "\nRemote control client up and running. Type “help” for help.\n"; 308 | for(;;) { 309 | std::cout << "> "; 310 | std::cout.flush(); 311 | std::string line; 312 | if (!std::getline(std::cin, line)) { 313 | line = "exit"; 314 | std::cout << '\n'; 315 | } 316 | auto i = commands.find(line); 317 | if (i == commands.end()) { 318 | std::cerr << "Unrecognized command. Type “help” for help.\n"; 319 | } else { 320 | i->second(sock); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /rcon.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | import "referee.proto"; 3 | import "game_event.proto"; 4 | 5 | // The TCP half-connection from controller to referee box carries a sequence of 6 | // these messages, sent on demand, each preceded by its length in bytes as a 7 | // 4-byte big-endian integer. 8 | // 9 | // There is no rule on how often requests can be, or need to be, sent. A remote 10 | // control client connected to the referee box over a reliable link will 11 | // typically send messages only when a change of game state is needed. However, 12 | // a remote control client connected over an unreliable link (e.g. wifi) may 13 | // wish to send a no-op request (a message with only message_id filled in) at a 14 | // fixed frequency to detect and report connection failures. Such no-op 15 | // requests cause no change to game state but solicit a reply from the referee 16 | // box with the OK outcome. 17 | // 18 | // Each request may contain at most one action. An action is a stage, a 19 | // command, or a card. Setting more than one action in a single request results 20 | // in the request being rejected. This simplifies understanding the order in 21 | // which actions occur. 22 | message SSL_RefereeRemoteControlRequest { 23 | // The message ID. This number may be selected arbitrarily by the client. 24 | // It is never interpreted by the referee box. It is returned unmodified in 25 | // the SSL_RefereeRemoteControlReply message to allow correlating replies 26 | // to requests. 27 | required uint32 message_id = 1; 28 | 29 | // The stage of the game to move to, which should be omitted if the stage 30 | // need not change. 31 | // 32 | // Do not use this field to enter the NORMAL_FIRST_HALF, 33 | // NORMAL_SECOND_HALF, EXTRA_FIRST_HALF, or EXTRA_SECOND_HALF stages. 34 | // Instead, prepare a kickoff and then issue the NORMAL_START command via 35 | // the command field. 36 | optional SSL_Referee.Stage stage = 2; 37 | 38 | // The command to be issued, which should be omitted if no command should 39 | // be issued at this time (i.e. the command currently in force may continue 40 | // to be used, or a stage change is occurring which comes with its own new 41 | // command). Sending a request with this field set always increments the 42 | // command counter in the broadcast referee box packet, which implies a 43 | // “new activity”. 44 | // 45 | // The TIMEOUT_YELLOW and TIMEOUT_BLUE commands must be used to report 46 | // entering timeouts. The timeout counter and clock are updated when those 47 | // commands are issued. STOP is used to end the timeout. 48 | // 49 | // HALT can be issued during a timeout to stop the timeout clock. The 50 | // appropriate choice of TIMEOUT_YELLOW or TIMEOUT_BLUE can be issued to 51 | // resume the timeout clock, or STOP can be issued to end the timeout. 52 | // 53 | // The GOAL_YELLOW and GOAL_BLUE commands must be used to report goals 54 | // scored. The goal counters in the TeamInfo messages are incremented when 55 | // those commands are issued. 56 | optional SSL_Referee.Command command = 3; 57 | 58 | // The coordinates of the Designated Position (whose purpose depends on the 59 | // current command). This is measured in millimetres and correspond to 60 | // SSL-Vision coordinates. this field must be present if and only if the 61 | // command field is present and is set to a ball placement command. If the 62 | // command field is absent, the designated position does not change. If the 63 | // command field is present and the designated position field is absent, no 64 | // designated position is included in the new command. 65 | optional SSL_Referee.Point designated_position = 4; 66 | 67 | // The card to issue. 68 | message CardInfo { 69 | // Which type of card to issue. 70 | enum CardType { 71 | CARD_YELLOW = 0; 72 | CARD_RED = 1; 73 | } 74 | required CardType type = 1; 75 | 76 | // Which team to issue the card to. 77 | enum CardTeam { 78 | TEAM_YELLOW = 0; 79 | TEAM_BLUE = 1; 80 | } 81 | required CardTeam team = 2; 82 | } 83 | optional CardInfo card = 5; 84 | 85 | // The command_counter of the most recent multicast referee packet observed 86 | // by the remote control. If this does not match the command_counter value 87 | // of the current referee packet, the request is rejected. 88 | // 89 | // The purpose of this field is to avoid race conditions between input from 90 | // a human and input from an autonomous software referee using the remote 91 | // control protocol. For example, consider the case where the game is in 92 | // play (Normal Start) and the human operator presses the Halt button while 93 | // the autonomous referee simultaneously sends the Stop command. Without 94 | // this field: 95 | // 1. The Halt button is pressed, resulting in a new command being started. 96 | // 2. The autonomous referee sends the Stop command to the remote control 97 | // port. 98 | // 3. The Stop command is accepted and takes precedence due to arriving 99 | // later. 100 | // In the worst case, this could result in the operator having to press 101 | // Halt multiple times before the field is made safe. 102 | // 103 | // By including the command_counter field in all requests on the remote 104 | // control port, and autonomous referee avoids this problem. Instead, the 105 | // situation would develop as follows: 106 | // 1. The Halt button is pressed, resulting in a new command being started. 107 | // 2. The autonomous referee sends the Stop command, but with the Normal 108 | // Start command’s command_counter. 109 | // 3. The referee box rejects the Stop command and remains halted, due to 110 | // the mismatch in command_counter value. 111 | // 4. The autonomous referee waits for the next multicast packet from the 112 | // referee box, to get the new command_counter value. 113 | // 5. The autonomous referee box observes that the game is halted and 114 | // decides it should not send the Stop command. 115 | // 116 | // If this field is omitted, the check for a matching counter value is 117 | // bypassed and requests are never rejected for this reason. This is 118 | // appropriate for remote control software managed by a human operator, 119 | // where no race condition is possible. 120 | optional uint32 last_command_counter = 6; 121 | 122 | // A unique static identifier for each implementation (like each auto-ref implementation, test clients, etc) 123 | // Used to identify the source of requests on the receiving side 124 | optional string implementation_id = 7; 125 | 126 | // The game event that caused the referee command 127 | optional SSL_Referee_Game_Event gameEvent = 8; 128 | } 129 | 130 | // The TCP half-connection from referee box to controller carries a sequence of 131 | // these messages, sent precisely once per received 132 | // SSL_RefereeRemoteControlRequest message, each preceded by its length in 133 | // bytes as as 4-byte big-endian integer. 134 | message SSL_RefereeRemoteControlReply { 135 | // The message ID of the request message to which this reply corresponds. 136 | required uint32 message_id = 1; 137 | 138 | // The outcome of the request. 139 | enum Outcome { 140 | // The request was accepted. 141 | OK = 0; 142 | // The request was rejected because it contained more than one action. 143 | MULTIPLE_ACTIONS = 1; 144 | // The request was rejected because the requested stage does not exist, 145 | // is not accessible from the current game state, or is a game half 146 | // (which must be entered by means of a NORMAL_START command, not a 147 | // stage change). 148 | BAD_STAGE = 2; 149 | // The request was rejected because the requested command does not 150 | // exist or is not accessible from the current game state. 151 | BAD_COMMAND = 3; 152 | // The request was rejected because a designated position was provided 153 | // with a command that does not need one or with the command field 154 | // absent, or the command field was set to a ball placement command but 155 | // a designated position was not provided. 156 | BAD_DESIGNATED_POSITION = 4; 157 | // The request was rejected because the command_counter field does not 158 | // match. 159 | BAD_COMMAND_COUNTER = 5; 160 | // The request was rejected because a card cannot be issued at this 161 | // time. 162 | BAD_CARD = 6; 163 | // The request was rejected because it found no majority 164 | NO_MAJORITY = 7; 165 | // The request was rejected because the communication to the ssl-refbox failed 166 | COMMUNICATION_FAILED = 8; 167 | } 168 | required Outcome outcome = 2; 169 | }; 170 | -------------------------------------------------------------------------------- /rconsrv.cc: -------------------------------------------------------------------------------- 1 | #include "rconsrv.h" 2 | #include "configuration.h" 3 | #include "gamecontroller.h" 4 | #include "logger.h" 5 | #include "rcon.pb.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #if defined(WIN32) 13 | #include 14 | #else 15 | #include 16 | #include 17 | #include 18 | #include 19 | #endif 20 | 21 | namespace { 22 | const unsigned int MAX_PACKET_SIZE = 4096; 23 | 24 | Glib::ustring format_address(const Glib::RefPtr &address) { 25 | const Glib::RefPtr &inet_address = Glib::RefPtr::cast_dynamic(address); 26 | if (inet_address) { 27 | return Glib::ustring::compose(u8"%1:%2", inet_address->get_address()->to_string(), inet_address->get_port()); 28 | } else { 29 | return u8""; 30 | } 31 | } 32 | } 33 | 34 | 35 | 36 | RConServer::RConServer(GameController &controller) : 37 | controller(controller), 38 | listener(Gio::SocketService::create()), 39 | connections(), 40 | logger(controller.logger) 41 | { 42 | listener->add_inet_port(controller.configuration.rcon_port); 43 | listener->signal_incoming().connect(sigc::mem_fun(this, &RConServer::on_incoming)); 44 | listener->start(); 45 | logger.write("Start listening for remote control commands"); 46 | } 47 | 48 | RConServer::~RConServer() { 49 | listener->stop(); 50 | listener->close(); 51 | logger.write("Stop listening for remote control commands"); 52 | } 53 | 54 | bool RConServer::on_incoming(const Glib::RefPtr &sock, const Glib::RefPtr &) { 55 | connections.emplace_back(*this, sock); 56 | std::list::iterator iterator = connections.end(); 57 | --iterator; 58 | iterator->set_connection_list_iterator(iterator); 59 | return false; 60 | } 61 | 62 | 63 | 64 | RConServer::Connection::Connection(RConServer &server, const Glib::RefPtr &sock) : 65 | server(server), 66 | sock(sock) 67 | { 68 | server.controller.logger.write(Glib::ustring::compose(u8"Accepted remote control connection from %1", format_address(sock->get_remote_address()))); 69 | if (!sock->get_socket()->set_option(IPPROTO_TCP, TCP_NODELAY, 1)) { 70 | server.controller.logger.write(u8"Warning: unable to set TCP_NODELAY option"); 71 | } 72 | paused = false; 73 | start_read_length(); 74 | } 75 | 76 | RConServer::Connection::~Connection() { 77 | server.controller.logger.write(Glib::ustring::compose(u8"End remote control connection from %1", format_address(sock->get_remote_address()))); 78 | sock->close(); 79 | } 80 | 81 | void RConServer::Connection::set_connection_list_iterator(std::list::iterator iter) { 82 | connection_list_iterator = iter; 83 | } 84 | 85 | void RConServer::Connection::start_read_length() { 86 | start_read_fully(&length, sizeof(length), sigc::mem_fun(this, &RConServer::Connection::finished_read_length)); 87 | } 88 | 89 | void RConServer::Connection::finished_read_length(bool ok) { 90 | if (ok) { 91 | length = ntohl(length); 92 | if (length <= MAX_PACKET_SIZE) { 93 | buffer.resize(length); 94 | start_read_fully(&buffer[0], buffer.size(), sigc::mem_fun(this, &RConServer::Connection::finished_read_data)); 95 | } else { 96 | server.controller.logger.write(Glib::ustring::compose(u8"Packet size %1 too large", length)); 97 | server.connections.erase(connection_list_iterator); 98 | } 99 | } else { 100 | server.connections.erase(connection_list_iterator); 101 | } 102 | } 103 | 104 | void RConServer::Connection::finished_read_data(bool ok) { 105 | if (ok) { 106 | SSL_RefereeRemoteControlRequest request; 107 | if (request.ParseFromArray(&buffer[0], static_cast(buffer.size()))) { 108 | SSL_RefereeRemoteControlReply reply; 109 | bool delayRequest; 110 | execute_request(request, reply, delayRequest); 111 | if(delayRequest) { 112 | paused = true; 113 | return; 114 | } 115 | length = reply.ByteSize(); 116 | buffer.resize(sizeof(length) + length); 117 | reply.SerializeWithCachedSizesToArray(&buffer[sizeof(length)]); 118 | length = htonl(length); 119 | std::memcpy(&buffer[0], &length, sizeof(length)); 120 | start_write_fully(&buffer[0], buffer.size(), sigc::mem_fun(this, &RConServer::Connection::finished_write_reply)); 121 | } else { 122 | server.controller.logger.write(u8"Protobuf parsing failed"); 123 | server.connections.erase(connection_list_iterator); 124 | } 125 | } else { 126 | server.connections.erase(connection_list_iterator); 127 | } 128 | } 129 | 130 | void RConServer::Connection::finished_write_reply(bool ok) { 131 | if (ok) { 132 | start_read_length(); 133 | } else { 134 | server.connections.erase(connection_list_iterator); 135 | } 136 | } 137 | 138 | void RConServer::Connection::execute_request(const SSL_RefereeRemoteControlRequest &request, SSL_RefereeRemoteControlReply &reply, bool &delayRequest) { 139 | reply.set_message_id(request.message_id()); 140 | reply.set_outcome(SSL_RefereeRemoteControlReply::OK); 141 | delayRequest = false; 142 | 143 | if (request.has_last_command_counter()) { 144 | if (request.last_command_counter() != server.controller.state.referee().command_counter()) { 145 | reply.set_outcome(SSL_RefereeRemoteControlReply::BAD_COMMAND_COUNTER); 146 | return; 147 | } 148 | } 149 | 150 | unsigned int actions = request.has_stage() + request.has_command() + request.has_card(); 151 | if (actions > 1) { 152 | reply.set_outcome(SSL_RefereeRemoteControlReply::MULTIPLE_ACTIONS); 153 | return; 154 | } 155 | 156 | if ((request.has_designated_position() && !request.has_command()) 157 | || (request.has_command() && (request.has_designated_position() != server.controller.command_needs_designated_position(request.command())))) { 158 | reply.set_outcome(SSL_RefereeRemoteControlReply::BAD_DESIGNATED_POSITION); 159 | return; 160 | } 161 | 162 | if (request.has_stage()) { 163 | if (server.controller.can_enter_stage(request.stage())) { 164 | server.controller.enter_stage(request.stage()); 165 | } else { 166 | reply.set_outcome(SSL_RefereeRemoteControlReply::BAD_STAGE); 167 | return; 168 | } 169 | } else if (request.has_command()) { 170 | if (server.controller.can_set_command(request.command())) { 171 | if(server.commands_on_hold.find(request.command()) != server.commands_on_hold.end()) { 172 | delayRequest = true; 173 | server.logger.write("Pause incoming command"); 174 | return; 175 | } 176 | server.controller.set_command(request.command(), request.designated_position().x(), request.designated_position().y(), false); 177 | } else { 178 | reply.set_outcome(SSL_RefereeRemoteControlReply::BAD_COMMAND); 179 | return; 180 | } 181 | } else if (request.has_card()) { 182 | if (server.controller.can_issue_card()) { 183 | SaveState::Team team = request.card().team() == SSL_RefereeRemoteControlRequest::CardInfo::TEAM_YELLOW ? SaveState::TEAM_YELLOW : SaveState::TEAM_BLUE; 184 | if (request.card().type() == SSL_RefereeRemoteControlRequest::CardInfo::CARD_YELLOW) { 185 | server.controller.yellow_card(team); 186 | } else { 187 | server.controller.red_card(team); 188 | } 189 | } else { 190 | reply.set_outcome(SSL_RefereeRemoteControlReply::BAD_CARD); 191 | return; 192 | } 193 | } 194 | 195 | if(request.has_gameevent()) { 196 | server.controller.set_game_event(&request.gameevent()); 197 | } 198 | } 199 | 200 | void RConServer::Connection::start_read_fully(void *buffer, std::size_t length, const sigc::slot &slot) { 201 | sock->get_input_stream()->read_async(buffer, length, sigc::bind(sigc::mem_fun(this, &RConServer::Connection::finished_read_fully_partial), buffer, length, slot)); 202 | } 203 | 204 | void RConServer::Connection::finished_read_fully_partial(Glib::RefPtr &result, void *rptr, std::size_t left, const sigc::slot &slot) { 205 | try { 206 | gssize bytes_read = sock->get_input_stream()->read_finish(result); 207 | if (bytes_read > 0) { 208 | unsigned char *rptr_ch = static_cast(rptr); 209 | rptr_ch += bytes_read; 210 | left -= bytes_read; 211 | if (left) { 212 | sock->get_input_stream()->read_async(rptr_ch, left, sigc::bind(sigc::mem_fun(this, &RConServer::Connection::finished_read_fully_partial), rptr_ch, left, slot)); 213 | } else { 214 | slot(true); 215 | } 216 | } else { 217 | slot(false); 218 | } 219 | } catch (const Gio::Error &) { 220 | slot(false); 221 | } 222 | } 223 | 224 | void RConServer::Connection::start_write_fully(const void *data, std::size_t length, const sigc::slot &slot) { 225 | sock->get_output_stream()->write_async(data, 1, sigc::bind(sigc::mem_fun(this, &RConServer::Connection::finished_write_fully_partial), data, length, slot)); 226 | } 227 | 228 | void RConServer::Connection::finished_write_fully_partial(Glib::RefPtr &result, const void *wptr, std::size_t left, const sigc::slot &slot) { 229 | try { 230 | gssize bytes_written = sock->get_output_stream()->write_finish(result); 231 | if (bytes_written > 0) { 232 | const unsigned char *wptr_ch = static_cast(wptr); 233 | wptr_ch += bytes_written; 234 | left -= bytes_written; 235 | if (left) { 236 | sock->get_output_stream()->write_async(wptr_ch, left, sigc::bind(sigc::mem_fun(this, &RConServer::Connection::finished_write_fully_partial), wptr_ch, left, slot)); 237 | } else { 238 | slot(true); 239 | } 240 | } else { 241 | slot(false); 242 | } 243 | } catch (const Gio::Error &) { 244 | slot(false); 245 | } 246 | } 247 | 248 | void RConServer::set_commands_on_hold(const std::set &commands) { 249 | if(commands.empty()) { 250 | logger.write("Unset commands on hold"); 251 | } else { 252 | logger.write("Set commands on hold"); 253 | } 254 | commands_on_hold = commands; 255 | std::list::iterator it; 256 | for (it = connections.begin(); it != connections.end(); ++it) { 257 | if (it->paused) { 258 | logger.write("Resume after unsetting commands on hold"); 259 | it->paused = false; 260 | it->finished_read_data(true); 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /rconsrv.h: -------------------------------------------------------------------------------- 1 | #ifndef RCONSRV_H 2 | #define RCONSRV_H 3 | 4 | #include "noncopyable.h" 5 | #include "logger.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class Configuration; 16 | class GameController; 17 | class SSL_RefereeRemoteControlRequest; 18 | class SSL_RefereeRemoteControlReply; 19 | 20 | class RConServer : public NonCopyable, public sigc::trackable { 21 | public: 22 | RConServer(GameController &controller); 23 | ~RConServer(); 24 | Logger &logger; 25 | std::set commands_on_hold; 26 | 27 | void set_commands_on_hold(const std::set &commands); 28 | 29 | private: 30 | class Connection : public NonCopyable, public sigc::trackable { 31 | public: 32 | Connection(RConServer &server, const Glib::RefPtr &sock); 33 | ~Connection(); 34 | bool paused; 35 | 36 | void set_connection_list_iterator(std::list::iterator iter); 37 | void finished_read_data(bool ok); 38 | 39 | private: 40 | RConServer &server; 41 | Glib::RefPtr sock; 42 | std::list::iterator connection_list_iterator; 43 | uint32_t length; 44 | std::vector buffer; 45 | 46 | void start_read_length(); 47 | void finished_read_length(bool ok); 48 | void finished_write_reply(bool ok); 49 | 50 | void execute_request(const SSL_RefereeRemoteControlRequest &request, SSL_RefereeRemoteControlReply &reply, bool &delayRequest); 51 | 52 | void start_read_fully(void *buffer, std::size_t length, const sigc::slot &slot); 53 | void finished_read_fully_partial(Glib::RefPtr &result, void *rptr, std::size_t left, const sigc::slot &slot); 54 | 55 | void start_write_fully(const void *data, std::size_t length, const sigc::slot &slot); 56 | void finished_write_fully_partial(Glib::RefPtr &result, const void *wptr, std::size_t left, const sigc::slot &slot); 57 | }; 58 | 59 | GameController &controller; 60 | Glib::RefPtr listener; 61 | std::list connections; 62 | 63 | bool on_incoming(const Glib::RefPtr &sock, const Glib::RefPtr &); 64 | }; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /referee.conf: -------------------------------------------------------------------------------- 1 | # These are the properties of normal time. 2 | [normal] 3 | # Length of a play half in seconds 4 | HALF = 300 5 | # Length of half time between halves in seconds 6 | HALF_TIME = 300 7 | # Total length of timeouts in seconds 8 | TIMEOUT_TIME = 300 9 | # Number of timeouts 10 | TIMEOUTS = 4 11 | 12 | 13 | # These are the properties of overtime. 14 | [overtime] 15 | # Length of the break between normal time and overtime in seconds 16 | BREAK = 300 17 | # Length of a play half in seconds 18 | HALF = 150 19 | # Length of half time between halves in seconds 20 | HALF_TIME = 120 21 | # Total length of timeouts in seconds 22 | TIMEOUT_TIME = 300 23 | # Number of timeouts 24 | TIMEOUTS = 2 25 | 26 | 27 | # These are the properties of the penalty shootout. 28 | [shootout] 29 | # Length of the break between overtime and penalty shootout in seconds 30 | BREAK = 120 31 | 32 | 33 | # These are other game control properties that are global and apply to all game stages equally. 34 | [global] 35 | # Length of a yellow card in seconds 36 | YELLOW_CARD_TIME = 120 37 | # Whether team names must be entered before starting a game 38 | TEAM_NAMES_REQUIRED = true 39 | # Whether remote connection is activated by default 40 | RCON_ENABLED_BY_DEFAULT = true 41 | 42 | 43 | # These are filenames used by the system. 44 | [files] 45 | # File into which state will be saved so game can be resumed (comment to not save); if %1 appears it will be replaced with a timestamp 46 | SAVE = referee.sav 47 | # File into which a game log will be recorded for later review (comment to not log) 48 | LOG = referee.log 49 | 50 | 51 | # These are the networking settings used to distribute data. 52 | [ip] 53 | # Address to send packets to (can be unicast, multicast, or broadcast) 54 | ADDRESS = 224.5.23.1 55 | # UDP port number to send old legacy packets to (comment to not send) 56 | LEGACY_PORT = 10001 57 | # UDP port number to send Protobuf packets to (comment to not send) 58 | PROTOBUF_PORT = 10003 59 | # Name of the network interface to send packets on (comment to send on all interfaces) 60 | #INTERFACE = eth0 61 | # TCP port number to accept remote control connections on (comment to disable remote control) 62 | RCON_PORT = 10007 63 | 64 | 65 | # These are the names of the teams that prepopulate the team name combo boxes. 66 | # The key for each team is ignored (but must be unique); the value is the team name. 67 | [teams] 68 | aces = ACES 69 | ais = AIS 70 | anorak = Anorak 71 | amc = AMC 72 | brocks = BRocks 73 | cmdragons = CMDragons 74 | cmus = CMμs 75 | cyrus = Cyrus 76 | eagleknights = Eagle Knights 77 | emenents = EMEnents 78 | erforce = ER-Force 79 | furgbot = FURGbot 80 | immortals = Immortals 81 | irssdeluxe = IRSS Deluxe 82 | kiks = KIKS 83 | kgpkubs = KgpKubs 84 | mctsusanologics = MCT Susano Logics 85 | mrl = MRL 86 | neuislanders = NEUIslanders 87 | odens = ODENS 88 | omid = OMID 89 | parsian = Parsian 90 | rfccambridge = RFC Cambridge 91 | robodragons = RoboDragons 92 | robofei = RoboFEI 93 | roboime = RoboIME 94 | robojackets = RoboJackets 95 | roboteamtwente = RoboTeam Twente 96 | skuba = Skuba 97 | src = SRC 98 | ssh = SSH 99 | stanfordroboticsclub = Stanford Robotics Club 100 | stoxs = STOx’s 101 | tigersmannheim = TIGERs Mannheim 102 | uaisoccer = UaiSoccer 103 | ultron = ULtron 104 | umass = UMass Minutebots 105 | thunderbots = UBC Thunderbots 106 | warthog = Warthog Robotics 107 | zjunlict = ZJUNlict 108 | -------------------------------------------------------------------------------- /referee.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "game_event.proto"; 4 | 5 | // Each UDP packet contains one of these messages. 6 | message SSL_Referee { 7 | // The UNIX timestamp when the packet was sent, in microseconds. 8 | // Divide by 1,000,000 to get a time_t. 9 | required uint64 packet_timestamp = 1; 10 | 11 | // These are the "coarse" stages of the game. 12 | enum Stage { 13 | // The first half is about to start. 14 | // A kickoff is called within this stage. 15 | // This stage ends with the NORMAL_START. 16 | NORMAL_FIRST_HALF_PRE = 0; 17 | // The first half of the normal game, before half time. 18 | NORMAL_FIRST_HALF = 1; 19 | // Half time between first and second halves. 20 | NORMAL_HALF_TIME = 2; 21 | // The second half is about to start. 22 | // A kickoff is called within this stage. 23 | // This stage ends with the NORMAL_START. 24 | NORMAL_SECOND_HALF_PRE = 3; 25 | // The second half of the normal game, after half time. 26 | NORMAL_SECOND_HALF = 4; 27 | // The break before extra time. 28 | EXTRA_TIME_BREAK = 5; 29 | // The first half of extra time is about to start. 30 | // A kickoff is called within this stage. 31 | // This stage ends with the NORMAL_START. 32 | EXTRA_FIRST_HALF_PRE = 6; 33 | // The first half of extra time. 34 | EXTRA_FIRST_HALF = 7; 35 | // Half time between first and second extra halves. 36 | EXTRA_HALF_TIME = 8; 37 | // The second half of extra time is about to start. 38 | // A kickoff is called within this stage. 39 | // This stage ends with the NORMAL_START. 40 | EXTRA_SECOND_HALF_PRE = 9; 41 | // The second half of extra time. 42 | EXTRA_SECOND_HALF = 10; 43 | // The break before penalty shootout. 44 | PENALTY_SHOOTOUT_BREAK = 11; 45 | // The penalty shootout. 46 | PENALTY_SHOOTOUT = 12; 47 | // The game is over. 48 | POST_GAME = 13; 49 | } 50 | required Stage stage = 2; 51 | 52 | // The number of microseconds left in the stage. 53 | // The following stages have this value; the rest do not: 54 | // NORMAL_FIRST_HALF 55 | // NORMAL_HALF_TIME 56 | // NORMAL_SECOND_HALF 57 | // EXTRA_TIME_BREAK 58 | // EXTRA_FIRST_HALF 59 | // EXTRA_HALF_TIME 60 | // EXTRA_SECOND_HALF 61 | // PENALTY_SHOOTOUT_BREAK 62 | // 63 | // If the stage runs over its specified time, this value 64 | // becomes negative. 65 | optional sint32 stage_time_left = 3; 66 | 67 | // These are the "fine" states of play on the field. 68 | enum Command { 69 | // All robots should completely stop moving. 70 | HALT = 0; 71 | // Robots must keep 50 cm from the ball. 72 | STOP = 1; 73 | // A prepared kickoff or penalty may now be taken. 74 | NORMAL_START = 2; 75 | // The ball is dropped and free for either team. 76 | FORCE_START = 3; 77 | // The yellow team may move into kickoff position. 78 | PREPARE_KICKOFF_YELLOW = 4; 79 | // The blue team may move into kickoff position. 80 | PREPARE_KICKOFF_BLUE = 5; 81 | // The yellow team may move into penalty position. 82 | PREPARE_PENALTY_YELLOW = 6; 83 | // The blue team may move into penalty position. 84 | PREPARE_PENALTY_BLUE = 7; 85 | // The yellow team may take a direct free kick. 86 | DIRECT_FREE_YELLOW = 8; 87 | // The blue team may take a direct free kick. 88 | DIRECT_FREE_BLUE = 9; 89 | // The yellow team may take an indirect free kick. 90 | INDIRECT_FREE_YELLOW = 10; 91 | // The blue team may take an indirect free kick. 92 | INDIRECT_FREE_BLUE = 11; 93 | // The yellow team is currently in a timeout. 94 | TIMEOUT_YELLOW = 12; 95 | // The blue team is currently in a timeout. 96 | TIMEOUT_BLUE = 13; 97 | // The yellow team just scored a goal. 98 | // For information only. 99 | // For rules compliance, teams must treat as STOP. 100 | GOAL_YELLOW = 14; 101 | // The blue team just scored a goal. 102 | GOAL_BLUE = 15; 103 | // Equivalent to STOP, but the yellow team must pick up the ball and 104 | // drop it in the Designated Position. 105 | BALL_PLACEMENT_YELLOW = 16; 106 | // Equivalent to STOP, but the blue team must pick up the ball and drop 107 | // it in the Designated Position. 108 | BALL_PLACEMENT_BLUE = 17; 109 | } 110 | required Command command = 4; 111 | 112 | // The number of commands issued since startup (mod 2^32). 113 | required uint32 command_counter = 5; 114 | 115 | // The UNIX timestamp when the command was issued, in microseconds. 116 | // This value changes only when a new command is issued, not on each packet. 117 | required uint64 command_timestamp = 6; 118 | 119 | // Information about a single team. 120 | message TeamInfo { 121 | // The team's name (empty string if operator has not typed anything). 122 | required string name = 1; 123 | // The number of goals scored by the team during normal play and overtime. 124 | required uint32 score = 2; 125 | // The number of red cards issued to the team since the beginning of the game. 126 | required uint32 red_cards = 3; 127 | // The amount of time (in microseconds) left on each yellow card issued to the team. 128 | // If no yellow cards are issued, this array has no elements. 129 | // Otherwise, times are ordered from smallest to largest. 130 | repeated uint32 yellow_card_times = 4 [packed=true]; 131 | // The total number of yellow cards ever issued to the team. 132 | required uint32 yellow_cards = 5; 133 | // The number of timeouts this team can still call. 134 | // If in a timeout right now, that timeout is excluded. 135 | required uint32 timeouts = 6; 136 | // The number of microseconds of timeout this team can use. 137 | required uint32 timeout_time = 7; 138 | // The pattern number of this team's goalie. 139 | required uint32 goalie = 8; 140 | } 141 | 142 | // Information about the two teams. 143 | required TeamInfo yellow = 7; 144 | required TeamInfo blue = 8; 145 | 146 | // The coordinates of the Designated Position. These are measured in 147 | // millimetres and correspond to SSL-Vision coordinates. These fields are 148 | // always either both present (in the case of a ball placement command) or 149 | // both absent (in the case of any other command). 150 | message Point { 151 | required float x = 1; 152 | required float y = 2; 153 | } 154 | optional Point designated_position = 9; 155 | 156 | // Information about the direction of play. 157 | // True, if the blue team will have it's goal on the positive x-axis of the ssl-vision coordinate system 158 | // Obviously, the yellow team will play on the opposide half 159 | // For compatibility, this field is optional 160 | optional bool blueTeamOnPositiveHalf = 10; 161 | 162 | // The game event that caused the referee command 163 | optional SSL_Referee_Game_Event gameEvent = 11; 164 | } 165 | -------------------------------------------------------------------------------- /savegame.cc: -------------------------------------------------------------------------------- 1 | #include "savegame.h" 2 | #include "noncopyable.h" 3 | #include "referee.pb.h" 4 | #include "savestate.pb.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #ifdef __linux__ 11 | #include "exception.h" 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace { 19 | class FD : public NonCopyable { 20 | public: 21 | FD(const std::string &filename, int options, int mode); 22 | ~FD(); 23 | void write(const void *data, std::size_t length); 24 | void fsync(); 25 | void close(); 26 | 27 | private: 28 | int fd; 29 | }; 30 | } 31 | 32 | FD::FD(const std::string &filename, int options, int mode) : fd(::open(filename.c_str(), options, mode)) { 33 | if (fd < 0) { 34 | int rc = errno; 35 | throw SystemError(Glib::locale_from_utf8(Glib::ustring::compose(u8"Error creating file %1", Glib::filename_to_utf8(filename))), rc); 36 | } 37 | } 38 | 39 | FD::~FD() { 40 | if (fd >= 0) { 41 | close(); 42 | } 43 | } 44 | 45 | void FD::write(const void *data, std::size_t length) { 46 | ssize_t ssz = ::write(fd, data, length); 47 | if (ssz != static_cast(length)) { 48 | throw SystemError("Error writing to saved state file"); 49 | } 50 | } 51 | 52 | void FD::fsync() { 53 | if (::fsync(fd) < 0) { 54 | throw SystemError("Error flushing saved state file"); 55 | } 56 | } 57 | 58 | void FD::close() { 59 | if (::close(fd) < 0) { 60 | throw SystemError("Error closing saved state file"); 61 | } 62 | fd = -1; 63 | } 64 | #endif 65 | 66 | void save_game(const SaveState &ss, const std::string &save_filename) { 67 | // We never save the current game state if we are in post-game. 68 | // It is better to leave the save file holding the last state just before we ended the game. 69 | // Thus, if the transition to post-game was accidental, the operator can recover. 70 | if (ss.referee().stage() == SSL_Referee::POST_GAME) { 71 | return; 72 | } 73 | 74 | // If there’s no filename provided, don’t try to save. 75 | if (save_filename.empty()) { 76 | return; 77 | } 78 | 79 | #ifdef __linux__ 80 | // On Linux, we can do the 100%-safe create-new-file, write-to-new-file, fsync-new-file, close-new-file, rename-over-old-file method. 81 | std::string data; 82 | if (!ss.SerializeToString(&data)) { 83 | throw std::runtime_error("Protobuf error serializing game state!"); 84 | } 85 | std::string temp_filename = save_filename + ".new"; 86 | FD fd(temp_filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); 87 | fd.write(data.data(), data.size()); 88 | fd.fsync(); 89 | fd.close(); 90 | if (::rename(temp_filename.c_str(), save_filename.c_str()) < 0) { 91 | int rc = errno; 92 | throw SystemError(Glib::locale_from_utf8(Glib::ustring::compose(u8"I/O error renaming \"%1\" to \"%2\"!", Glib::filename_to_utf8(temp_filename), Glib::filename_to_utf8(save_filename))), rc); 93 | } 94 | #else 95 | // On other platforms, just do an ordinary C++ file write and hope we don’t get a whole system crash or power loss that destroys the file. 96 | std::ofstream ofs; 97 | ofs.exceptions(std::ios_base::badbit | std::ios_base::failbit); 98 | ofs.open(save_filename, std::ios_base::out | std::ios_base::binary); 99 | if (!ss.SerializeToOstream(&ofs)) { 100 | throw std::runtime_error(Glib::locale_from_utf8(Glib::ustring::compose(u8"Protobuf error saving game state to file \"%1\"!", Glib::filename_to_utf8(save_filename)))); 101 | } 102 | ofs.close(); 103 | #endif 104 | } 105 | 106 | -------------------------------------------------------------------------------- /savegame.h: -------------------------------------------------------------------------------- 1 | #ifndef SAVEGAME_H 2 | #define SAVEGAME_H 3 | 4 | #include 5 | 6 | class SaveState; 7 | 8 | void save_game(const SaveState &ss, const std::string &save_filename); 9 | 10 | #endif 11 | 12 | -------------------------------------------------------------------------------- /savestate.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | import "referee.proto"; 3 | 4 | // The type of data saved in the saved state file. 5 | message SaveState { 6 | // The possible teams. 7 | // These must be numbered 0 and 1. 8 | enum Team { 9 | TEAM_YELLOW = 0; 10 | TEAM_BLUE = 1; 11 | } 12 | 13 | // The possible cards. 14 | enum Card { 15 | CARD_YELLOW = 0; 16 | CARD_RED = 1; 17 | } 18 | 19 | // Information about an issued card. 20 | message CardInfo { 21 | required Team team = 1; 22 | required Card card = 2; 23 | } 24 | 25 | // Information about a running timeout. 26 | message TimeoutInfo { 27 | required Team team = 1; 28 | required uint32 left_before = 2; 29 | } 30 | 31 | // The current packet. 32 | required SSL_Referee referee = 1; 33 | 34 | // The number of goals scored by each team during a penalty shootout. 35 | required uint32 yellow_penalty_goals = 2; 36 | required uint32 blue_penalty_goals = 3; 37 | 38 | // The amount of time, in microseconds, taken so far in a stage. 39 | required uint64 time_taken = 4; 40 | 41 | // The most recently awarded card. 42 | // Absent if no card has been awarded, if the most recent card has been cancelled, or if the most recent card was yellow and has expired. 43 | optional CardInfo last_card = 5; 44 | 45 | // The current timeout. 46 | // Only present if in a timeout. 47 | // However, this is still present if we are in the HALT state and that state was reached from inside a timeout, so we can pop back to the timeout if needed. 48 | optional TimeoutInfo timeout = 6; 49 | 50 | // The last timeout that was running. 51 | // Only present after the timeout ends with the Stop command up until the next command is issued. 52 | optional TimeoutInfo last_timeout = 7; 53 | } 54 | -------------------------------------------------------------------------------- /scoreboard/Makefile: -------------------------------------------------------------------------------- 1 | # Standard compiler and linker flags. 2 | PKG_CONFIG ?= pkg-config 3 | override CXXFLAGS := -std=gnu++0x -Wall -Wextra -Wold-style-cast -Wconversion -Wundef -O2 -g $(shell $(PKG_CONFIG) --cflags gtkmm-2.4 protobuf | sed 's/-I/-isystem /g') $(CXXFLAGS) 4 | override LDFLAGS := $(shell $(PKG_CONFIG) --libs-only-L --libs-only-other gtkmm-2.4 protobuf) 5 | override LDLIBS := $(shell $(PKG_CONFIG) --libs-only-l gtkmm-2.4 protobuf) 6 | 7 | # The default target. 8 | .PHONY : world 9 | world : scoreboard 10 | 11 | # Gather lists of files of various types. 12 | protos := referee.proto 13 | proto_sources := $(patsubst %.proto,%.pb.cc,$(protos)) 14 | proto_headers := $(patsubst %.proto,%.pb.h,$(protos)) 15 | proto_objs := $(patsubst %.proto,%.pb.o,$(protos)) 16 | non_proto_sources := $(filter-out $(proto_sources),$(wildcard *.cc)) 17 | non_proto_headers := $(filter-out $(proto_headers),$(wildcard *.h)) 18 | non_proto_objs := $(patsubst %.cc,%.o,$(non_proto_sources)) 19 | all_sources := $(proto_sources) $(non_proto_sources) 20 | all_headers := $(proto_headers) $(non_proto_headers) 21 | all_objs := $(proto_objs) $(non_proto_objs) 22 | 23 | # Normal rule to link the final binary. 24 | scoreboard : $(all_objs) 25 | @echo "LD $@" 26 | @$(CXX) $(LDFLAGS) -o $@ $+ $(LDLIBS) 27 | 28 | # Static pattern rule to compile a protobuf source file (with warnings disabled, as they make no sense here). 29 | $(proto_objs) : %.pb.o : %.pb.cc $(all_headers) 30 | @echo "CXX $@" 31 | @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -w -c $< 32 | 33 | # Static pattern rule to compile a non-protobuf source file. 34 | $(non_proto_objs) : %.o : %.cc $(all_headers) 35 | @echo "CXX $@" 36 | @$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< 37 | 38 | # Pattern rule to run protoc on a message definition file. 39 | %.pb.cc %.pb.h : ../%.proto 40 | @echo "PROTO $(patsubst ../%.proto,%.pb.cc,$<)" 41 | @protoc --proto_path=.. --cpp_out=. $< 42 | 43 | # Rule to clean intermediates and outputs. 44 | .PHONY : clean 45 | clean : 46 | $(RM) scoreboard *.o *.pb.cc *.pb.h 47 | -------------------------------------------------------------------------------- /scoreboard/addrinfolist.cc: -------------------------------------------------------------------------------- 1 | ../addrinfolist.cc -------------------------------------------------------------------------------- /scoreboard/addrinfolist.h: -------------------------------------------------------------------------------- 1 | ../addrinfolist.h -------------------------------------------------------------------------------- /scoreboard/exception.cc: -------------------------------------------------------------------------------- 1 | ../exception.cc -------------------------------------------------------------------------------- /scoreboard/exception.h: -------------------------------------------------------------------------------- 1 | ../exception.h -------------------------------------------------------------------------------- /scoreboard/flags/EMEnents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/EMEnents.png -------------------------------------------------------------------------------- /scoreboard/flags/ER-Force.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/ER-Force.png -------------------------------------------------------------------------------- /scoreboard/flags/IRSS Deluxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/IRSS Deluxe.png -------------------------------------------------------------------------------- /scoreboard/flags/Immortals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/Immortals.png -------------------------------------------------------------------------------- /scoreboard/flags/KIKS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/KIKS.png -------------------------------------------------------------------------------- /scoreboard/flags/MCT Susano Logics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/MCT Susano Logics.png -------------------------------------------------------------------------------- /scoreboard/flags/MRL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/MRL.png -------------------------------------------------------------------------------- /scoreboard/flags/NEUIslanders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/NEUIslanders.png -------------------------------------------------------------------------------- /scoreboard/flags/ODENS.png: -------------------------------------------------------------------------------- 1 | RoboDragons.png -------------------------------------------------------------------------------- /scoreboard/flags/RFC Cambridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/RFC Cambridge.png -------------------------------------------------------------------------------- /scoreboard/flags/RoboDragons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/RoboDragons.png -------------------------------------------------------------------------------- /scoreboard/flags/RoboFEI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/RoboFEI.png -------------------------------------------------------------------------------- /scoreboard/flags/RoboIME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/RoboIME.png -------------------------------------------------------------------------------- /scoreboard/flags/RoboJackets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/RoboJackets.png -------------------------------------------------------------------------------- /scoreboard/flags/STOx's.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/STOx's.png -------------------------------------------------------------------------------- /scoreboard/flags/Tigers Mannheim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/Tigers Mannheim.png -------------------------------------------------------------------------------- /scoreboard/flags/UBC Thunderbots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/UBC Thunderbots.png -------------------------------------------------------------------------------- /scoreboard/flags/Warthog Robotics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/flags/Warthog Robotics.png -------------------------------------------------------------------------------- /scoreboard/gamestate.cc: -------------------------------------------------------------------------------- 1 | #pragma GCC diagnostic ignored "-Wold-style-cast" 2 | 3 | #include "gamestate.h" 4 | #include "addrinfolist.h" 5 | #include "exception.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #ifdef WIN32 16 | #include 17 | #include 18 | #else 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #endif 26 | 27 | GameState::GameState(const std::string &interface, const std::string &group, const std::string &port) : ok(false) { 28 | // Get a list of addresses to bind to. 29 | addrinfo hints; 30 | hints.ai_flags = AI_PASSIVE; 31 | hints.ai_family = AF_UNSPEC; 32 | hints.ai_socktype = SOCK_DGRAM; 33 | hints.ai_protocol = 0; 34 | AddrInfoList bind_ail(0, port.c_str(), &hints); 35 | 36 | // Get the address of the multicast group to join. 37 | hints.ai_flags = 0; 38 | AddrInfoList mcgroup_ail(group.c_str(), 0, &hints); 39 | 40 | #ifdef WIN32 41 | #else 42 | // Look up the index of the multicast interface. 43 | unsigned int ifindex = if_nametoindex(interface.c_str()); 44 | if (!ifindex) { 45 | int err = errno; 46 | throw SystemError(Glib::ustring::compose(u8"Cannot look up index of network interface %1", interface), err); 47 | } 48 | #endif 49 | 50 | // Construct a socket for each bind address. 51 | for (const addrinfo *i = bind_ail.get(); i; i = i->ai_next) { 52 | // We only handle IPv4 and IPv6, because we do not know how to do multicast configuration sockopts for other families. 53 | if (i->ai_family == AF_INET || i->ai_family == AF_INET6) { 54 | // Do a reverse lookup to get the numeric host and port. 55 | char host[256], serv[256]; 56 | if (getnameinfo(i->ai_addr, i->ai_addrlen, host, sizeof(host), serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV) == 0) { 57 | try { 58 | // Create the socket. 59 | Socket sock(i->ai_family, i->ai_socktype, i->ai_protocol); 60 | 61 | // Allow reusing recently used ports. 62 | static const int ONE = 1; 63 | if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &ONE, sizeof(ONE)) < 0) { 64 | throw SystemError("Cannot set SO_REUSEADDR"); 65 | } 66 | 67 | // Bind to the listen address. 68 | if (bind(sock, i->ai_addr, i->ai_addrlen) < 0) { 69 | throw SystemError("Cannot bind socket"); 70 | } 71 | 72 | // Try to join the socket to a multicast group. 73 | bool ok = false; 74 | for (const addrinfo *j = mcgroup_ail.get(); j; j = j->ai_next) { 75 | if (j->ai_family == i->ai_family) { 76 | if (j->ai_family == AF_INET) { 77 | ip_mreqn req; 78 | req.imr_multiaddr = reinterpret_cast(j->ai_addr)->sin_addr; 79 | req.imr_address.s_addr = INADDR_ANY; 80 | req.imr_ifindex = ifindex; 81 | if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)) < 0) { 82 | throw SystemError("Cannot join multicast group"); 83 | } 84 | } else if (j->ai_family == AF_INET6) { 85 | ipv6_mreq req; 86 | req.ipv6mr_multiaddr = reinterpret_cast(j->ai_addr)->sin6_addr; 87 | req.ipv6mr_interface = ifindex; 88 | if (setsockopt(sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &req, sizeof(req)) < 0) { 89 | throw SystemError("Cannot join multicast group"); 90 | } 91 | } 92 | char mchost[256]; 93 | getnameinfo(j->ai_addr, j->ai_addrlen, mchost, sizeof(mchost), 0, 0, NI_NUMERICHOST); 94 | ok = true; 95 | break; 96 | } 97 | } 98 | if (!ok) { 99 | continue; 100 | } 101 | 102 | // Permit multicast loop to the local machine, but don’t worry if it fails (Windows/UNIX disagree on whether this happens on the send or the receive path). 103 | if (i->ai_family == AF_INET) { 104 | setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &ONE, sizeof(ONE)); 105 | } else { 106 | setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &ONE, sizeof(ONE)); 107 | } 108 | 109 | // Add a watch to get a notification on data arriving. 110 | Glib::signal_io().connect(sigc::mem_fun(this, &GameState::handle_io_ready), sock, Glib::IO_IN); 111 | 112 | // Drop the socket into the vector. 113 | sockets.push_back(std::move(sock)); 114 | } catch (const SystemError &exp) { 115 | std::cerr << Glib::ustring::compose(u8"Failed to create socket for bind address %1 and port %2: %3", Glib::locale_to_utf8(host), Glib::locale_to_utf8(serv), Glib::locale_to_utf8(exp.what())); 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | bool GameState::handle_io_ready(Glib::IOCondition) { 123 | uint8_t buffer[65536]; 124 | bool updated = false; 125 | for (const Socket &sock : sockets) { 126 | ssize_t rc = recv(sock, buffer, sizeof(buffer), MSG_DONTWAIT | MSG_NOSIGNAL); 127 | if (rc < 0) { 128 | if (errno != EWOULDBLOCK && errno != EAGAIN) { 129 | throw SystemError("Cannot receive data on socket"); 130 | } 131 | } 132 | if (!referee.ParseFromArray(buffer, static_cast(rc))) { 133 | throw std::runtime_error("Error parsing Protobuf packet"); 134 | } 135 | updated = true; 136 | } 137 | if (updated) { 138 | ok = true; 139 | timeout_connection.disconnect(); 140 | timeout_connection = Glib::signal_timeout().connect_seconds(sigc::mem_fun(this, &GameState::handle_timeout), 3); 141 | signal_updated.emit(); 142 | } 143 | return true; 144 | } 145 | 146 | bool GameState::handle_timeout() { 147 | ok = false; 148 | signal_updated.emit(); 149 | return false; 150 | } 151 | 152 | -------------------------------------------------------------------------------- /scoreboard/gamestate.h: -------------------------------------------------------------------------------- 1 | #ifndef GAMESTATE_H 2 | #define GAMESTATE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "referee.pb.h" 10 | #include "socket.h" 11 | 12 | class GameState { 13 | public: 14 | bool ok; 15 | SSL_Referee referee; 16 | sigc::signal signal_updated; 17 | 18 | GameState(const std::string &interface, const std::string &group, const std::string &port); 19 | 20 | private: 21 | std::vector sockets; 22 | sigc::connection timeout_connection; 23 | 24 | bool handle_io_ready(Glib::IOCondition cond); 25 | bool handle_timeout(); 26 | }; 27 | 28 | #endif 29 | 30 | -------------------------------------------------------------------------------- /scoreboard/imagedb.cc: -------------------------------------------------------------------------------- 1 | #include "imagedb.h" 2 | #include 3 | #include 4 | #include 5 | 6 | image_database_t load_image_database(const std::string &path) { 7 | image_database_t db; 8 | Glib::Dir dir(path); 9 | for (std::string file : dir) { 10 | const std::string &full_path = Glib::build_filename(path, file); 11 | if (full_path.size() > 4 && full_path[full_path.size() - 4] == '.' && full_path[full_path.size() - 3] == 'p' && full_path[full_path.size() - 2] == 'n' && full_path[full_path.size() - 1] == 'g') { 12 | Glib::RefPtr pb(Gdk::Pixbuf::create_from_file(full_path)); 13 | file.erase(file.end() - 4, file.end()); 14 | db[Glib::filename_to_utf8(file).casefold_collate_key()] = pb; 15 | } 16 | } 17 | return db; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /scoreboard/imagedb.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGEDB_H 2 | #define IMAGEDB_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | typedef std::unordered_map> image_database_t; 10 | 11 | image_database_t load_image_database(const std::string &path); 12 | 13 | #endif 14 | 15 | -------------------------------------------------------------------------------- /scoreboard/logos/CMDragons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/CMDragons.png -------------------------------------------------------------------------------- /scoreboard/logos/EMEnents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/EMEnents.png -------------------------------------------------------------------------------- /scoreboard/logos/ER-Force.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/ER-Force.png -------------------------------------------------------------------------------- /scoreboard/logos/IRSS Deluxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/IRSS Deluxe.png -------------------------------------------------------------------------------- /scoreboard/logos/Immortals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/Immortals.png -------------------------------------------------------------------------------- /scoreboard/logos/KIKS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/KIKS.png -------------------------------------------------------------------------------- /scoreboard/logos/MCT Susano Logics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/MCT Susano Logics.png -------------------------------------------------------------------------------- /scoreboard/logos/MRL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/MRL.png -------------------------------------------------------------------------------- /scoreboard/logos/NEUIslanders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/NEUIslanders.png -------------------------------------------------------------------------------- /scoreboard/logos/ODENS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/ODENS.png -------------------------------------------------------------------------------- /scoreboard/logos/Parsian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/Parsian.png -------------------------------------------------------------------------------- /scoreboard/logos/RFC Cambridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/RFC Cambridge.png -------------------------------------------------------------------------------- /scoreboard/logos/RoboDragons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/RoboDragons.png -------------------------------------------------------------------------------- /scoreboard/logos/RoboFEI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/RoboFEI.png -------------------------------------------------------------------------------- /scoreboard/logos/RoboIME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/RoboIME.png -------------------------------------------------------------------------------- /scoreboard/logos/RoboJackets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/RoboJackets.png -------------------------------------------------------------------------------- /scoreboard/logos/STOx's.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/STOx's.png -------------------------------------------------------------------------------- /scoreboard/logos/Tigers Mannheim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/Tigers Mannheim.png -------------------------------------------------------------------------------- /scoreboard/logos/UBC Thunderbots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/UBC Thunderbots.png -------------------------------------------------------------------------------- /scoreboard/logos/Warthog Robotics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboCup-SSL/ssl-refbox/9d6ad70b1a206f52ed18ad9ad59b39a7ebe0e80a/scoreboard/logos/Warthog Robotics.png -------------------------------------------------------------------------------- /scoreboard/main.cc: -------------------------------------------------------------------------------- 1 | #include "gamestate.h" 2 | #include "imagedb.h" 3 | #include "mainwindow.h" 4 | #include "socket.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace { 18 | int main_impl(int argc, char **argv) { 19 | // Set the current locale. 20 | std::locale::global(std::locale("")); 21 | 22 | // Parse the command-line arguments. 23 | Glib::OptionContext option_context; 24 | option_context.set_summary(u8"Runs the RoboCup Small Size League Scoreboard."); 25 | option_context.set_summary(u8"The Referee Box is © RoboCup Federation, 2013–2013."); 26 | 27 | Glib::OptionGroup option_group(u8"scoreboard", u8"Scoreboard Options", u8"Show Scoreboard Options"); 28 | 29 | Glib::OptionEntry mc_interface_entry; 30 | mc_interface_entry.set_long_name(u8"interface"); 31 | mc_interface_entry.set_short_name('i'); 32 | mc_interface_entry.set_description(u8"Sets the network interface name on which packets will be received."); 33 | mc_interface_entry.set_arg_description(u8"INTERFACE"); 34 | Glib::ustring mc_interface(u8"eth0"); 35 | option_group.add_entry(mc_interface_entry, mc_interface); 36 | 37 | Glib::OptionEntry mc_group_entry; 38 | mc_group_entry.set_long_name(u8"group"); 39 | mc_group_entry.set_short_name('g'); 40 | mc_group_entry.set_description(u8"Sets the multicast group to join."); 41 | mc_group_entry.set_arg_description(u8"ADDRESS"); 42 | Glib::ustring mc_group(u8"224.5.23.1"); 43 | option_group.add_entry(mc_group_entry, mc_group); 44 | 45 | Glib::OptionEntry mc_port_entry; 46 | mc_port_entry.set_long_name(u8"port"); 47 | mc_port_entry.set_short_name('p'); 48 | mc_port_entry.set_description(u8"Sets the UDP port on which packets will be received."); 49 | mc_port_entry.set_arg_description(u8"PORT"); 50 | Glib::ustring mc_port(u8"10003"); 51 | option_group.add_entry(mc_port_entry, mc_port); 52 | 53 | option_context.set_main_group(option_group); 54 | Gtk::Main kit(argc, argv, option_context); 55 | 56 | // Load configuration file. 57 | Glib::KeyFile kf; 58 | kf.load_from_file("scoreboard.conf"); 59 | 60 | // Load the flags and logos. 61 | const image_database_t &flags = load_image_database("flags"); 62 | const image_database_t &logos = load_image_database("logos"); 63 | 64 | // Start receiving and updating game state. 65 | Socket::init_system(); 66 | GameState state(mc_interface, mc_group, mc_port); 67 | 68 | // Create and display a main window. 69 | MainWindow main_window(state, flags, logos, kf); 70 | kit.run(main_window); 71 | 72 | // Shut down protobuf. 73 | google::protobuf::ShutdownProtobufLibrary(); 74 | 75 | return 0; 76 | } 77 | 78 | void print_exception(const Glib::Exception &exp) { 79 | std::cerr << "\nUnhandled exception:\n"; 80 | std::cerr << "Type: " << typeid(exp).name() << '\n'; 81 | std::cerr << "Detail: " << exp.what() << '\n'; 82 | } 83 | 84 | void print_exception(const std::exception &exp, bool first = true) { 85 | if (first) { 86 | std::cerr << "\nUnhandled exception:\n"; 87 | } else { 88 | std::cerr << "Caused by:\n"; 89 | } 90 | std::cerr << "Type: " << typeid(exp).name() << '\n'; 91 | std::cerr << "Detail: " << exp.what() << '\n'; 92 | try { 93 | std::rethrow_if_nested(exp); 94 | } catch (const std::exception &exp) { 95 | print_exception(exp, false); 96 | } 97 | } 98 | } 99 | 100 | int main(int argc, char **argv) { 101 | try { 102 | return main_impl(argc, argv); 103 | } catch (const Glib::Exception &exp) { 104 | print_exception(exp); 105 | } catch (const std::exception &exp) { 106 | print_exception(exp); 107 | } catch (...) { 108 | std::cerr << "\nUnhandled exception:\n"; 109 | std::cerr << "Type: Unknown\n"; 110 | std::cerr << "Detail: Unknown\n"; 111 | } 112 | return 1; 113 | } 114 | 115 | -------------------------------------------------------------------------------- /scoreboard/mainwindow.cc: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | // Set to 1 to show rectangles around layout elements. 17 | // Useful for debugging the rectangle calculation code. 18 | #define SHOW_LAYOUT 0 19 | 20 | namespace { 21 | Glib::ustring format_time_deciseconds(uint64_t micros) { 22 | uint64_t decis = micros / 100000U; 23 | uint64_t seconds = decis / 10; 24 | decis %= 10; 25 | uint64_t minutes = seconds / 60; 26 | seconds %= 60; 27 | return Glib::ustring::compose(u8"%1:%2.%3", minutes, Glib::ustring::format(std::setw(2), std::setfill(L'0'), seconds), decis); 28 | } 29 | 30 | Glib::RefPtr find_image(const image_database_t &db, const Glib::ustring &team) { 31 | Glib::RefPtr ret; 32 | const std::string &ckey = team.casefold_collate_key(); 33 | const image_database_t::const_iterator iter = db.find(ckey); 34 | if (iter != db.end()) { 35 | ret = iter->second; 36 | } 37 | return ret; 38 | } 39 | 40 | void split_rect_horizontal(const Pango::Rectangle &container, const std::initializer_list &rectangles, const std::initializer_list &fractions) { 41 | int x = container.get_x(); 42 | int y = container.get_y(); 43 | int w = container.get_width(); 44 | int h = container.get_height(); 45 | decltype(rectangles.begin()) i, iend; 46 | decltype(fractions.begin()) j; 47 | for (i = rectangles.begin(), iend = rectangles.end(), j = fractions.begin(); i != iend; ++i, ++j) { 48 | (*i)->set_x(x); 49 | (*i)->set_y(y); 50 | (*i)->set_width(static_cast(w * *j)); 51 | (*i)->set_height(h); 52 | x += static_cast(w * *j); 53 | } 54 | } 55 | 56 | void split_rect_vertical(const Pango::Rectangle &container, const std::initializer_list &rectangles, const std::initializer_list &fractions) { 57 | int x = container.get_x(); 58 | int y = container.get_y(); 59 | int w = container.get_width(); 60 | int h = container.get_height(); 61 | decltype(rectangles.begin()) i, iend; 62 | decltype(fractions.begin()) j; 63 | for (i = rectangles.begin(), iend = rectangles.end(), j = fractions.begin(); i != iend; ++i, ++j) { 64 | (*i)->set_x(x); 65 | (*i)->set_y(y); 66 | (*i)->set_width(w); 67 | (*i)->set_height(static_cast(h * *j)); 68 | y += static_cast(h * *j); 69 | } 70 | } 71 | 72 | void pad_rect(const Pango::Rectangle &outer, Pango::Rectangle &inner, int padding) { 73 | inner.set_x(outer.get_x() + padding); 74 | inner.set_y(outer.get_y() + padding); 75 | inner.set_width(outer.get_width() - 2 * padding); 76 | inner.set_height(outer.get_height() - 2 * padding); 77 | } 78 | 79 | void draw_text(Cairo::RefPtr ctx, const Pango::Rectangle &rect, int padding, const Glib::ustring &text) { 80 | Pango::FontDescription fd; 81 | fd.set_family(u8"monospace"); 82 | fd.set_style(Pango::STYLE_NORMAL); 83 | fd.set_variant(Pango::VARIANT_NORMAL); 84 | fd.set_size(24 * Pango::SCALE); 85 | 86 | Glib::RefPtr layout(Pango::Layout::create(ctx)); 87 | layout->set_text(text); 88 | layout->set_font_description(fd); 89 | Pango::Rectangle orig_pixel_extents = layout->get_pixel_logical_extents(); 90 | 91 | int target_width = rect.get_width() - 2 * padding; 92 | int target_height = rect.get_height() - 2 * padding; 93 | 94 | if (target_width <= 0 || target_height <= 0) { 95 | return; 96 | } 97 | 98 | double xscale = static_cast(target_width) / orig_pixel_extents.get_width(); 99 | double yscale = static_cast(target_height) / orig_pixel_extents.get_height(); 100 | double scale = std::min(xscale, yscale); 101 | 102 | fd.set_size(static_cast(24 * Pango::SCALE * scale)); 103 | layout->set_font_description(fd); 104 | Pango::Rectangle new_pixel_extents = layout->get_pixel_logical_extents(); 105 | 106 | ctx->move_to(rect.get_x() + padding + (target_width - new_pixel_extents.get_width()) / 2, rect.get_y() + padding + (target_height - new_pixel_extents.get_height()) / 2); 107 | layout->show_in_cairo_context(ctx); 108 | } 109 | 110 | void draw_image(Cairo::RefPtr ctx, const Pango::Rectangle &rect, int padding, Glib::RefPtr image) { 111 | Pango::Rectangle target; 112 | pad_rect(rect, target, padding); 113 | 114 | if (rect.get_width() <= 0 || rect.get_height() <= 0) { 115 | return; 116 | } 117 | 118 | ctx->save(); 119 | ctx->translate(target.get_x() + (target.get_width() - image->get_width()) / 2, target.get_y() + (target.get_height() - image->get_height()) / 2); 120 | Gdk::Cairo::set_source_pixbuf(ctx, image, 0, 0); 121 | ctx->paint(); 122 | ctx->restore(); 123 | } 124 | } 125 | 126 | MainWindow::MainWindow(GameState &state, const image_database_t &flags, const image_database_t &logos, const Glib::KeyFile &config) : 127 | state(state), 128 | flags(flags), 129 | logos(logos), 130 | config(config), 131 | is_fullscreen(false) { 132 | set_title(u8"Scoreboard (press F to toggle fullscreen"); 133 | 134 | Gtk::Main::signal_key_snooper().connect(sigc::mem_fun(this, &MainWindow::key_snoop)); 135 | 136 | state.signal_updated.connect(sigc::mem_fun(this, &MainWindow::handle_state_updated)); 137 | 138 | set_default_size(400, 400); 139 | show_all(); 140 | 141 | Gdk::Cursor cursor(Gdk::BLANK_CURSOR); 142 | get_window()->set_cursor(cursor); 143 | } 144 | 145 | bool MainWindow::on_expose_event(GdkEventExpose *evt) { 146 | // Pass to superclass. 147 | Gtk::Window::on_expose_event(evt); 148 | 149 | // Get dimensions and create context. 150 | int width, height; 151 | get_window()->get_size(width, height); 152 | Cairo::RefPtr ctx = get_window()->create_cairo_context(); 153 | 154 | // Fill background with black. 155 | ctx->set_source_rgb(0.0, 0.0, 0.0); 156 | ctx->paint(); 157 | 158 | // Compute how big a padding area should be (2% of whichever dimension is smaller). 159 | int padding = std::min(width, height) / 50; 160 | 161 | // If the current data is not valid, just show a big message and stop. 162 | if (!state.ok) { 163 | ctx->set_source_rgb(1.0, 1.0, 1.0); 164 | Pango::Rectangle window_rect(0, 0, width, height); 165 | draw_text(ctx, window_rect, padding, u8"No Signal"); 166 | return true; 167 | } 168 | 169 | // Compute a rectangle that will hold each part of the display. 170 | Pango::Rectangle window_rect(0, 0, width, height); 171 | // window_rect 172 | Pango::Rectangle top_rect, bottom_rect; 173 | split_rect_vertical(window_rect, {&top_rect, &bottom_rect}, {0.2, 0.8}); 174 | // top_rect 175 | Pango::Rectangle clock_rect, stages_rect; 176 | split_rect_horizontal(top_rect, {&clock_rect, &stages_rect}, {0.7, 0.3}); 177 | // clock_rect is terminal 178 | // stages_rect 179 | Pango::Rectangle stages_top_rect, stages_bottom_rect; 180 | split_rect_vertical(stages_rect, {&stages_top_rect, &stages_bottom_rect}, {0.5, 0.5}); 181 | // stages_top_rect 182 | Pango::Rectangle half_time_rect, first_half_rect, second_half_rect; 183 | split_rect_horizontal(stages_top_rect, {&half_time_rect, &first_half_rect, &second_half_rect}, {0.333, 0.333, 0.334}); 184 | // these are terminal 185 | // stages_bottom_rect 186 | Pango::Rectangle overtime_first_half_rect, overtime_second_half_rect, penalty_shootout_rect; 187 | split_rect_horizontal(stages_bottom_rect, {&overtime_first_half_rect, &overtime_second_half_rect, &penalty_shootout_rect}, {0.333, 0.333, 0.334}); 188 | // these are terminal 189 | // bottom_rect 190 | Pango::Rectangle yellow_rect, blue_rect; 191 | split_rect_horizontal(bottom_rect, {&yellow_rect, &blue_rect}, {0.5, 0.5}); 192 | // yellow_rect 193 | Pango::Rectangle yellow_inner_rect; 194 | pad_rect(yellow_rect, yellow_inner_rect, padding); 195 | // blue_rect 196 | Pango::Rectangle blue_inner_rect; 197 | pad_rect(blue_rect, blue_inner_rect, padding); 198 | 199 | #if SHOW_LAYOUT 200 | // Show the rectangles making up the layout. 201 | ctx->set_source_rgb(1.0, 1.0, 1.0); 202 | std::initializer_list rects{&clock_rect, &half_time_rect, &first_half_rect, &second_half_rect, &overtime_first_half_rect, &overtime_second_half_rect, &penalty_shootout_rect, &blue_rect, &blue_name_rect, &blue_flag_rect, &blue_logo_rect, &blue_score_rect}; 203 | for (auto i : rects) { 204 | ctx->rectangle(i->get_x(), i->get_y(), i->get_width(), i->get_height()); 205 | } 206 | ctx->stroke(); 207 | #endif 208 | 209 | // Draw the coloured outlines for the team info. 210 | ctx->set_line_width(padding); 211 | ctx->set_source_rgb(1.0, 1.0, 0.0); 212 | ctx->rectangle((yellow_rect.get_x() + yellow_inner_rect.get_x()) / 2, (yellow_rect.get_y() + yellow_inner_rect.get_y()) / 2, (yellow_rect.get_width() + yellow_inner_rect.get_width()) / 2, (yellow_rect.get_height() + yellow_inner_rect.get_height()) / 2); 213 | ctx->stroke(); 214 | ctx->set_source_rgb(0.0, 0.0, 1.0); 215 | ctx->rectangle((blue_rect.get_x() + blue_inner_rect.get_x()) / 2, (blue_rect.get_y() + blue_inner_rect.get_y()) / 2, (blue_rect.get_width() + blue_inner_rect.get_width()) / 2, (blue_rect.get_height() + blue_inner_rect.get_height()) / 2); 216 | ctx->stroke(); 217 | ctx->set_line_width(1); 218 | 219 | // Draw the common texts. 220 | ctx->set_source_rgb(1.0, 1.0, 1.0); 221 | draw_text(ctx, clock_rect, padding, state.referee.stage_time_left() < 0 ? u8"0:00.0" : format_time_deciseconds(state.referee.stage_time_left())); 222 | { 223 | static const Glib::ustring STAGE_TEXTS[6] = { u8"HT", u8"N1", u8"N2", u8"O1", u8"O2", u8"PS" }; 224 | static const int PROTOBUF_TO_STAGE_MAPPING[14] = { 1, 1, 0, 2, 2, 0, 3, 3, 0, 4, 4, 0, 5, -1 }; 225 | const Pango::Rectangle * const stage_rects[6] = { &half_time_rect, &first_half_rect, &second_half_rect, &overtime_first_half_rect, &overtime_second_half_rect, &penalty_shootout_rect }; 226 | for (int i = 0; i < 6; ++i) { 227 | if (PROTOBUF_TO_STAGE_MAPPING[state.referee.stage()] == i) { 228 | ctx->set_source_rgb(1.0, 1.0, 1.0); 229 | } else { 230 | ctx->set_source_rgb(0.2, 0.2, 0.2); 231 | } 232 | draw_text(ctx, *stage_rects[i], padding, STAGE_TEXTS[i]); 233 | } 234 | } 235 | 236 | // Draw the team information panels. 237 | draw_team_rectangle(state.referee.yellow().name(), yellow_logo_cache, yellow_flag_cache, yellow_inner_rect, ctx, padding, state.referee.yellow().score()); 238 | draw_team_rectangle(state.referee.blue().name(), blue_logo_cache, blue_flag_cache, blue_inner_rect, ctx, padding, state.referee.blue().score()); 239 | 240 | return true; 241 | } 242 | 243 | bool MainWindow::on_window_state_event(GdkEventWindowState *evt) { 244 | is_fullscreen = !!(evt->new_window_state & GDK_WINDOW_STATE_FULLSCREEN); 245 | return true; 246 | } 247 | 248 | void MainWindow::handle_state_updated() { 249 | const Glib::RefPtr win(get_window()); 250 | if (win) { 251 | win->invalidate(false); 252 | } 253 | } 254 | 255 | int MainWindow::key_snoop(Widget *, GdkEventKey *event) { 256 | if (event->type == GDK_KEY_PRESS && (event->keyval == GDK_F || event->keyval == GDK_f)) { 257 | if (is_fullscreen) { 258 | unfullscreen(); 259 | } else { 260 | fullscreen(); 261 | } 262 | } 263 | return 0; 264 | } 265 | 266 | void MainWindow::on_size_allocate(Gdk::Rectangle &) { 267 | const Glib::RefPtr win(get_window()); 268 | if (win) { 269 | win->invalidate(false); 270 | } 271 | } 272 | 273 | Glib::RefPtr MainWindow::resize_image(Glib::RefPtr image, int width, int height, ImageCache &cache, const Glib::ustring &team) { 274 | if (!cache.image || cache.image->get_width() != width || cache.image->get_height() != height || cache.team != team) { 275 | double xscale = static_cast(width) / image->get_width(); 276 | double yscale = static_cast(height) / image->get_height(); 277 | double scale = std::min(xscale, yscale); 278 | cache.image = image->scale_simple(std::min(width, static_cast(image->get_width() * scale)), std::min(height, static_cast(image->get_height() * scale)), Gdk::INTERP_BILINEAR); 279 | cache.team = team; 280 | } 281 | return cache.image; 282 | } 283 | 284 | void MainWindow::draw_team_rectangle(const Glib::ustring &name, ImageCache &logo_cache, ImageCache &flag_cache, Pango::Rectangle inner_rect, Cairo::RefPtr ctx, int padding, unsigned int score) { 285 | // Find the logo and flag images for the team. 286 | Glib::RefPtr logo = find_image(logos, name); 287 | Glib::RefPtr flag = find_image(flags, name); 288 | 289 | // Decide whether to show the team name. 290 | bool show_name = config.has_key(u8"shownames", name) ? config.get_boolean(u8"shownames", name) : !logo; 291 | if (!show_name && !flag && !logo) { 292 | show_name = true; 293 | } 294 | 295 | // inner_rect 296 | Pango::Rectangle top_rect, score_rect; 297 | split_rect_vertical(inner_rect, {&top_rect, &score_rect}, {0.6, 0.4}); 298 | // top_rect 299 | Pango::Rectangle name_rect, images_rect, logo_rect, flag_rect; 300 | if (show_name && (flag || logo)) { 301 | split_rect_vertical(top_rect, {&name_rect, &images_rect}, {0.35, 0.65}); 302 | } else if (flag || logo) { 303 | images_rect = top_rect; 304 | } else { 305 | name_rect = top_rect; 306 | } 307 | // name_rect is terminal 308 | // images_rect 309 | if (flag && logo) { 310 | int fwidth = flag->get_width(); 311 | int fheight = flag->get_height(); 312 | int lwidth = logo->get_width(); 313 | int lheight = logo->get_height(); 314 | int hwidth = fwidth + lwidth, hheight = std::max(fheight, lheight); 315 | int vwidth = std::max(fwidth, lwidth), vheight = fheight + lheight; 316 | double hscale = std::min(static_cast(images_rect.get_width()) / hwidth, static_cast(images_rect.get_height()) / hheight); 317 | double vscale = std::min(static_cast(images_rect.get_width()) / vwidth, static_cast(images_rect.get_height()) / vheight); 318 | if (hscale >= vscale) { 319 | split_rect_horizontal(images_rect, {&logo_rect, &flag_rect}, {0.5, 0.5}); 320 | } else { 321 | split_rect_vertical(images_rect, {&logo_rect, &flag_rect}, {0.5, 0.5}); 322 | } 323 | // logo_rect is terminal 324 | // flag_rect is terminal 325 | } else if (logo) { 326 | logo_rect = images_rect; 327 | // logo_rect is terminal 328 | } else if (flag) { 329 | flag_rect = images_rect; 330 | // flag_rect is terminal 331 | } 332 | // score_rect is terminal 333 | 334 | #if SHOW_LAYOUT 335 | // Show the rectangles making up the layout. 336 | ctx->set_source_rgb(1.0, 1.0, 1.0); 337 | std::initializer_list rects{&clock_rect, &half_time_rect, &first_half_rect, &second_half_rect, &overtime_first_half_rect, &overtime_second_half_rect, &penalty_shootout_rect, &rect, &name_rect, &flag_rect, &logo_rect, &score_rect, &blue_rect, &blue_name_rect, &blue_flag_rect, &blue_logo_rect, &blue_score_rect}; 338 | for (auto i : rects) { 339 | ctx->rectangle(i->get_x(), i->get_y(), i->get_width(), i->get_height()); 340 | } 341 | ctx->stroke(); 342 | #endif 343 | 344 | ctx->set_source_rgb(1.0, 1.0, 1.0); 345 | draw_text(ctx, name_rect, padding, name); 346 | if (logo) { 347 | draw_image(ctx, logo_rect, padding, resize_image(logo, logo_rect.get_width(), logo_rect.get_height(), logo_cache, name)); 348 | } 349 | if (flag) { 350 | draw_image(ctx, flag_rect, padding, resize_image(flag, flag_rect.get_width(), flag_rect.get_height(), flag_cache, name)); 351 | } 352 | ctx->set_source_rgb(1.0, 1.0, 1.0); 353 | draw_text(ctx, score_rect, padding, Glib::ustring::format(score)); 354 | } 355 | 356 | -------------------------------------------------------------------------------- /scoreboard/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include "gamestate.h" 5 | #include "imagedb.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | class MainWindow : public Gtk::Window { 18 | public: 19 | MainWindow(GameState &state, const image_database_t &flags, const image_database_t &logos, const Glib::KeyFile &config); 20 | 21 | protected: 22 | bool on_expose_event(GdkEventExpose *); 23 | bool on_window_state_event(GdkEventWindowState *); 24 | 25 | private: 26 | struct ImageCache { 27 | Glib::ustring team; 28 | Glib::RefPtr image; 29 | }; 30 | 31 | GameState &state; 32 | const image_database_t &flags; 33 | const image_database_t &logos; 34 | const Glib::KeyFile &config; 35 | 36 | bool is_fullscreen; 37 | 38 | ImageCache yellow_logo_cache, yellow_flag_cache, blue_logo_cache, blue_flag_cache; 39 | 40 | void handle_state_updated(); 41 | int key_snoop(Widget *, GdkEventKey *); 42 | void on_size_allocate(Gdk::Rectangle &); 43 | Glib::RefPtr resize_image(Glib::RefPtr image, int width, int height, ImageCache &cache, const Glib::ustring &team); 44 | void draw_team_rectangle(const Glib::ustring &name, ImageCache &logo_cache, ImageCache &flag_cache, Pango::Rectangle inner_rect, Cairo::RefPtr context, int padding, unsigned int score); 45 | }; 46 | 47 | #endif 48 | 49 | -------------------------------------------------------------------------------- /scoreboard/noncopyable.h: -------------------------------------------------------------------------------- 1 | ../noncopyable.h -------------------------------------------------------------------------------- /scoreboard/scoreboard.conf: -------------------------------------------------------------------------------- 1 | # This section specifies whether a team’s name should be shown on the scoreboard. 2 | # Each key is the name of a team as sent by the referee box, 3 | # The value can be true to show the name or false to hide it. 4 | # For most teams, this should be false because the team name is included in the logo. 5 | # If a team does not include its name in its logo, true should be specified here so the team name is shown. 6 | # If a team does not appear in this file, by default, its name is hidden if a logo is provided. 7 | [shownames] 8 | IRSS Deluxe = true 9 | TIGERS Mannheim = true 10 | -------------------------------------------------------------------------------- /scoreboard/socket.cc: -------------------------------------------------------------------------------- 1 | ../socket.cc -------------------------------------------------------------------------------- /scoreboard/socket.h: -------------------------------------------------------------------------------- 1 | ../socket.h -------------------------------------------------------------------------------- /socket.cc: -------------------------------------------------------------------------------- 1 | #include "socket.h" 2 | #include "exception.h" 3 | 4 | #ifdef WIN32 5 | #include 6 | #pragma comment(lib, "ws2_32.lib") 7 | #else 8 | #include 9 | #include 10 | #include 11 | #endif 12 | 13 | namespace { 14 | #ifdef WIN32 15 | class WinsockInitializer { 16 | public: 17 | WinsockInitializer() { 18 | WORD sockVersion; 19 | WSADATA wsaData; 20 | sockVersion = MAKEWORD(2, 2); 21 | WSAStartup(sockVersion, &wsaData); 22 | } 23 | 24 | ~WinsockInitializer() { 25 | WSACleanup(); 26 | } 27 | }; 28 | #endif 29 | } 30 | 31 | Socket::Socket(int domain, int type, int proto) { 32 | init_system(); 33 | sock = socket(domain, type, proto); 34 | if (sock < 0) { 35 | throw SystemError("Cannot create socket"); 36 | } 37 | } 38 | 39 | Socket::~Socket() { 40 | #ifdef WIN32 41 | closesocket(sock); 42 | #else 43 | close(sock); 44 | #endif 45 | } 46 | 47 | Socket &Socket::operator=(Socket &&moveref) { 48 | #ifdef WIN32 49 | closesocket(sock); 50 | #else 51 | close(sock); 52 | #endif 53 | sock = moveref.sock; 54 | moveref.sock = -1; 55 | return *this; 56 | } 57 | 58 | void Socket::init_system() { 59 | #ifdef WIN32 60 | static WinsockInitializer ws_init; 61 | #endif 62 | } 63 | 64 | -------------------------------------------------------------------------------- /socket.h: -------------------------------------------------------------------------------- 1 | #ifndef SOCKET_H 2 | #define SOCKET_H 3 | 4 | #include "noncopyable.h" 5 | 6 | class Socket : public NonCopyable { 7 | public: 8 | Socket(int domain, int type, int protocol); 9 | Socket(Socket &&moveref); 10 | ~Socket(); 11 | Socket &operator=(Socket &&moveref); 12 | operator int() const; 13 | 14 | static void init_system(); 15 | 16 | private: 17 | int sock; 18 | }; 19 | 20 | 21 | 22 | inline Socket::Socket(Socket &&moveref) { 23 | sock = moveref.sock; 24 | moveref.sock = -1; 25 | } 26 | 27 | inline Socket::operator int() const { 28 | return sock; 29 | } 30 | 31 | #endif 32 | 33 | -------------------------------------------------------------------------------- /sslrefbox.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name=SSLRefBox 5 | GenericName=Referee Box 6 | Comment=RoboCup Small Size League 7 | Exec=sslrefbox 8 | Terminal=true 9 | Categories=GTK;GTK;Game; 10 | -------------------------------------------------------------------------------- /teams.cc: -------------------------------------------------------------------------------- 1 | #include "teams.h" 2 | #include 3 | 4 | const TeamMeta TeamMeta::ALL[2]; 5 | 6 | SaveState::Team TeamMeta::command_team(SSL_Referee::Command command) { 7 | switch (command) { 8 | case SSL_Referee::PREPARE_KICKOFF_YELLOW: return SaveState::TEAM_YELLOW; 9 | case SSL_Referee::PREPARE_KICKOFF_BLUE: return SaveState::TEAM_BLUE; 10 | case SSL_Referee::PREPARE_PENALTY_YELLOW: return SaveState::TEAM_YELLOW; 11 | case SSL_Referee::PREPARE_PENALTY_BLUE: return SaveState::TEAM_BLUE; 12 | case SSL_Referee::DIRECT_FREE_YELLOW: return SaveState::TEAM_YELLOW; 13 | case SSL_Referee::DIRECT_FREE_BLUE: return SaveState::TEAM_BLUE; 14 | case SSL_Referee::INDIRECT_FREE_YELLOW: return SaveState::TEAM_YELLOW; 15 | case SSL_Referee::INDIRECT_FREE_BLUE: return SaveState::TEAM_BLUE; 16 | case SSL_Referee::TIMEOUT_YELLOW: return SaveState::TEAM_YELLOW; 17 | case SSL_Referee::TIMEOUT_BLUE: return SaveState::TEAM_BLUE; 18 | case SSL_Referee::GOAL_YELLOW: return SaveState::TEAM_YELLOW; 19 | case SSL_Referee::GOAL_BLUE: return SaveState::TEAM_BLUE; 20 | default: throw std::logic_error("Command is not team-specific!"); 21 | } 22 | } 23 | 24 | TeamMeta::TeamMeta() : COLOUR(team() == SaveState::TEAM_YELLOW ? u8"yellow" : u8"blue") { 25 | } 26 | 27 | SaveState::Team TeamMeta::team() const { 28 | return static_cast(this - ALL); 29 | } 30 | 31 | SaveState::Team TeamMeta::other() const { 32 | return static_cast((team() + 1U) % 2U); 33 | } 34 | 35 | SSL_Referee::TeamInfo &TeamMeta::team_info(SSL_Referee &ref) const { 36 | return *(team() == SaveState::TEAM_YELLOW ? ref.mutable_yellow() : ref.mutable_blue()); 37 | } 38 | 39 | const SSL_Referee::TeamInfo &TeamMeta::team_info(const SSL_Referee &ref) const { 40 | return team() == SaveState::TEAM_YELLOW ? ref.yellow() : ref.blue(); 41 | } 42 | 43 | uint32_t TeamMeta::penalty_goals(const SaveState &ss) const { 44 | return team() == SaveState::TEAM_YELLOW ? ss.yellow_penalty_goals() : ss.blue_penalty_goals(); 45 | } 46 | 47 | void TeamMeta::set_penalty_goals(SaveState &ss, uint32_t penalty_goals) const { 48 | if (team() == SaveState::TEAM_YELLOW) { 49 | ss.set_yellow_penalty_goals(penalty_goals); 50 | } else { 51 | ss.set_blue_penalty_goals(penalty_goals); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /teams.h: -------------------------------------------------------------------------------- 1 | #ifndef TEAMS_H 2 | #define TEAMS_H 3 | 4 | #include "referee.pb.h" 5 | #include "savestate.pb.h" 6 | #include 7 | 8 | class TeamMeta { 9 | public: 10 | static const TeamMeta ALL[2]; 11 | 12 | static SaveState::Team command_team(SSL_Referee::Command command); 13 | 14 | const Glib::ustring COLOUR; 15 | 16 | TeamMeta(); 17 | 18 | SaveState::Team team() const; 19 | SaveState::Team other() const; 20 | 21 | SSL_Referee::TeamInfo &team_info(SSL_Referee &ref) const; 22 | const SSL_Referee::TeamInfo &team_info(const SSL_Referee &ref) const; 23 | 24 | uint32_t penalty_goals(const SaveState &ss) const; 25 | void set_penalty_goals(SaveState &ss, uint32_t penalty_goals) const; 26 | }; 27 | 28 | #endif 29 | 30 | -------------------------------------------------------------------------------- /timing.cc: -------------------------------------------------------------------------------- 1 | #include "timing.h" 2 | 3 | MicrosecondCounter::MicrosecondCounter() : start_time(std::chrono::high_resolution_clock::now()) { 4 | } 5 | 6 | uint32_t MicrosecondCounter::read_and_reset() { 7 | // This computation is not exactly the same as just resetting start_time to now. 8 | // In the case of a clock with better-than-microsecond resolution, this computation leaves the fractional microseconds of difference in the new value of now - start_time. 9 | // This prevents error from accumulating. 10 | std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now(); 11 | std::chrono::microseconds diff = std::chrono::duration_cast(now - start_time); 12 | start_time += diff; 13 | return static_cast(diff.count()); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /timing.h: -------------------------------------------------------------------------------- 1 | #ifndef TIMING_H 2 | #define TIMING_H 3 | 4 | #include 5 | #include 6 | 7 | class MicrosecondCounter { 8 | public: 9 | MicrosecondCounter(); 10 | uint32_t read_and_reset(); 11 | 12 | private: 13 | std::chrono::high_resolution_clock::time_point start_time; 14 | }; 15 | 16 | #endif 17 | 18 | -------------------------------------------------------------------------------- /udpbroadcast.cc: -------------------------------------------------------------------------------- 1 | #include "udpbroadcast.h" 2 | #include "addrinfolist.h" 3 | #include "exception.h" 4 | #include "logger.h" 5 | #include "noncopyable.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #ifdef WIN32 14 | #include 15 | #include 16 | #else 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #endif 24 | 25 | namespace { 26 | class InterfaceList; 27 | 28 | class InterfaceInfo { 29 | public: 30 | bool configure_socket(const Socket &sock, Logger &logger) const; 31 | const std::string &name() const; 32 | int family() const; 33 | 34 | static std::vector all(); 35 | 36 | private: 37 | friend class InterfaceList; 38 | 39 | std::string name_; 40 | int family_; 41 | 42 | #ifdef WIN32 43 | InterfaceInfo(int family); 44 | #else 45 | unsigned int ifindex; 46 | 47 | InterfaceInfo(const std::string &name, int family, unsigned int ifindex); 48 | #endif 49 | }; 50 | } 51 | 52 | 53 | 54 | bool InterfaceInfo::configure_socket(const Socket &sock, Logger &logger) const { 55 | #ifdef WIN32 56 | return true; 57 | #else 58 | #ifdef __APPLE__ 59 | int set = 1; 60 | if (setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, reinterpret_cast(&set), sizeof(int)) < 0) { 61 | int rc = errno; 62 | logger.write(Glib::ustring::compose(u8"Cannot set NOSIGPIPE option for interface %1: %2", Glib::locale_to_utf8(name_), Glib::locale_to_utf8(std::strerror(rc)))); 63 | return false; 64 | } 65 | #endif 66 | if (family_ == AF_INET) { 67 | ip_mreqn mreq; 68 | mreq.imr_ifindex = static_cast(ifindex); 69 | if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) < 0) { 70 | int rc = errno; 71 | logger.write(Glib::ustring::compose(u8"Cannot set IPv4 socket to send multicast packet on interface %1: %2", Glib::locale_to_utf8(name_), Glib::locale_to_utf8(std::strerror(rc)))); 72 | return false; 73 | } 74 | return true; 75 | } else if (family_ == AF_INET6) { 76 | ipv6_mreq mreq; 77 | mreq.ipv6mr_interface = ifindex; 78 | if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, &mreq, sizeof(mreq)) < 0) { 79 | int rc = errno; 80 | logger.write(Glib::ustring::compose(u8"Cannot set IPv6 socket to send multicast packet on interface %1: %2", Glib::locale_to_utf8(name_), Glib::locale_to_utf8(std::strerror(rc)))); 81 | return false; 82 | } 83 | return true; 84 | } else { 85 | throw std::logic_error("InterfaceInfo::configure_socket() has unknown family!"); 86 | } 87 | #endif 88 | } 89 | 90 | const std::string &InterfaceInfo::name() const { 91 | return name_; 92 | } 93 | 94 | int InterfaceInfo::family() const { 95 | return family_; 96 | } 97 | 98 | std::vector InterfaceInfo::all() { 99 | std::vector vec; 100 | #ifdef WIN32 101 | vec.push_back(InterfaceInfo(AF_INET)); 102 | vec.push_back(InterfaceInfo(AF_INET6)); 103 | #else 104 | ifaddrs *ifs = 0; 105 | if (getifaddrs(&ifs) < 0) { 106 | throw SystemError("Cannot get network interface list"); 107 | } 108 | try { 109 | for (const ifaddrs *i = ifs; i; i = i->ifa_next) { 110 | if ((i->ifa_flags & IFF_UP) && (i->ifa_flags & IFF_MULTICAST) && i->ifa_addr) { 111 | if (i->ifa_addr->sa_family == AF_INET || i->ifa_addr->sa_family == AF_INET6) { 112 | unsigned int ifindex = if_nametoindex(i->ifa_name); 113 | if (ifindex) { 114 | vec.push_back(InterfaceInfo(i->ifa_name, i->ifa_addr->sa_family, ifindex)); 115 | } 116 | } 117 | } 118 | } 119 | freeifaddrs(ifs); 120 | } catch (...) { 121 | freeifaddrs(ifs); 122 | throw; 123 | } 124 | #endif 125 | return vec; 126 | } 127 | 128 | #ifdef WIN32 129 | InterfaceInfo::InterfaceInfo(int family) : name_(family == AF_INET ? "inet" : "inet6"), family_(family) { 130 | } 131 | #else 132 | InterfaceInfo::InterfaceInfo(const std::string &name, int family, unsigned int ifindex) : name_(name), family_(family), ifindex(ifindex) { 133 | } 134 | #endif 135 | 136 | 137 | 138 | UDPBroadcast::UDPBroadcast(Logger &logger, const std::string &host, const std::string &port, const std::string &interface) : logger(logger), interface(interface) { 139 | // Initialize the sockets subsystem. 140 | Socket::init_system(); 141 | 142 | // Look up the target host/IP and port. 143 | addrinfo hints; 144 | hints.ai_flags = 0; 145 | hints.ai_family = AF_UNSPEC; 146 | hints.ai_socktype = SOCK_DGRAM; 147 | hints.ai_protocol = 0; 148 | AddrInfoList ai(host.c_str(), port.c_str(), &hints); 149 | 150 | // Construct a socket for each destination. 151 | for (const addrinfo *i = ai.get(); i; i = i->ai_next) { 152 | // We only handle IPv4 and IPv6, because we do not know how to do multicast configuration sockopts for other families. 153 | if (i->ai_family == AF_INET || i->ai_family == AF_INET6) { 154 | // Do a reverse lookup to get the numeric host and port. 155 | char host[256], serv[256]; 156 | if (getnameinfo(i->ai_addr, i->ai_addrlen, host, sizeof(host), serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV) == 0) { 157 | try { 158 | // Create the socket. 159 | Socket sock(i->ai_family, i->ai_socktype, i->ai_protocol); 160 | 161 | // Permit broadcasts, but don’t worry if it fails. 162 | static const int one = 1; 163 | setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &one, sizeof(one)); 164 | 165 | // Permit multicast loop to the local machine, but don’t worry if it fails (Windows/UNIX disagree on whether this happens on the send or the receive path). 166 | if (i->ai_family == AF_INET) { 167 | setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &one, sizeof(one)); 168 | } else { 169 | setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &one, sizeof(one)); 170 | } 171 | 172 | // Lock in a default destination address. 173 | if (connect(sock, i->ai_addr, i->ai_addrlen) < 0) { 174 | throw SystemError("Cannot connect socket"); 175 | } 176 | 177 | // Drop the socket into the map keyed by family. 178 | sockets[i->ai_family].push_back(std::make_pair(std::make_pair(std::string(host), std::string(serv)), std::move(sock))); 179 | } catch (const SystemError &exp) { 180 | logger.write(Glib::ustring::compose(u8"Failed to create socket for destination address %1 and port %2: %3", Glib::locale_to_utf8(host), Glib::locale_to_utf8(serv), Glib::locale_to_utf8(exp.what()))); 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | void UDPBroadcast::send(const void *data, size_t length) { 188 | // Go through the interfaces. 189 | static std::vector interfaces = InterfaceInfo::all(); 190 | for (const InterfaceInfo &i : interfaces) { 191 | // If the interface name was provided in the configuration file, ignore any interface that does not match that name. 192 | if (!interface.empty() && i.name() != interface) { 193 | continue; 194 | } 195 | 196 | // Go through all the sockets whose families match the family of this interface. 197 | auto socks = sockets.find(i.family()); 198 | if (socks == sockets.end()) { 199 | continue; 200 | } 201 | for (const std::pair, Socket> &sock : socks->second) { 202 | if (i.configure_socket(sock.second, logger)) { 203 | // The socket was set up to send to this interface. 204 | // Now send data. 205 | #ifdef __APPLE__ 206 | ssize_t ssz = ::send(sock.second, data, length, 0); 207 | #else 208 | ssize_t ssz = ::send(sock.second, data, length, MSG_NOSIGNAL); 209 | #endif 210 | if (ssz < 0) { 211 | int rc = errno; 212 | logger.write(Glib::ustring::compose(u8"Failed to send on interface %1 to address %2 and port %3: %4", Glib::locale_to_utf8(i.name()), Glib::locale_to_utf8(sock.first.first), Glib::locale_to_utf8(sock.first.second), Glib::locale_to_utf8(std::strerror(rc)))); 213 | } else if (ssz != static_cast(length)) { 214 | logger.write(Glib::ustring::compose(u8"Short write sending on interface %1 to address %2 and port %3!", Glib::locale_to_utf8(i.name()), Glib::locale_to_utf8(sock.first.first), Glib::locale_to_utf8(sock.first.second))); 215 | } 216 | } 217 | } 218 | } 219 | } 220 | 221 | -------------------------------------------------------------------------------- /udpbroadcast.h: -------------------------------------------------------------------------------- 1 | #ifndef UDP_BROADCAST_H 2 | #define UDP_BROADCAST_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "socket.h" 9 | 10 | class Logger; 11 | 12 | class UDPBroadcast { 13 | public: 14 | UDPBroadcast(Logger &logger, const std::string &host, const std::string &port, const std::string &interface); 15 | void send(const void *data, std::size_t length); 16 | 17 | private: 18 | Logger &logger; 19 | std::string interface; 20 | std::unordered_map, Socket>>> sockets; 21 | }; 22 | 23 | #endif 24 | 25 | --------------------------------------------------------------------------------