├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── go.yml ├── .gitignore ├── .gitmodules ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── config └── config.go ├── db └── db.go ├── go.mod ├── go.sum ├── injctr └── injctr.go ├── main.go ├── mem ├── debug.go ├── linux.go ├── mem.go ├── read.go ├── scan.go └── windows.go ├── memory ├── functions.go ├── init.go ├── mods.go ├── read.go ├── tournament_linux.go ├── tournament_windows.go └── values.go ├── out.ico ├── pp ├── editor.go ├── functions.go ├── iffc.go ├── mania.go ├── max.go ├── oppai.c ├── oppai_impl.c └── pp.go ├── updater └── updater.go └── web ├── structure.go └── web.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.buymeacoffee.com/BlackShark 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Archive 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Set up Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.17 15 | 16 | - name: Check out code 17 | uses: actions/checkout@v2 18 | 19 | - name: Install build dependencies 20 | run: sudo apt-get update && sudo apt-get install -y build-essential && sudo apt-get install gcc-multilib 21 | 22 | - name: Install MinGW packages 23 | run: sudo apt-get update && sudo apt-get install -y gcc-mingw-w64-x86-64 gcc-mingw-w64-i686 24 | 25 | - name: Clone static repository 26 | run: git clone https://github.com/l3lackShark/static 27 | 28 | - name: Build for Windows amd64 29 | run: CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o gosumemory.exe 30 | 31 | - name: Archive Windows amd64 32 | uses: actions/upload-artifact@v2 33 | with: 34 | name: gosumemory_windows_amd64 35 | path: | 36 | gosumemory.exe 37 | static/ 38 | 39 | - name: Rename Windows amd64 executable 40 | run: mv gosumemory.exe gosumemory_windows_amd64.exe 41 | 42 | - name: Build for Windows 386 43 | run: CC=i686-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -o gosumemory.exe 44 | 45 | - name: Archive Windows 386 46 | uses: actions/upload-artifact@v2 47 | with: 48 | name: gosumemory_windows_386 49 | path: | 50 | gosumemory.exe 51 | static/ 52 | 53 | - name: Rename Windows 386 executable 54 | run: mv gosumemory.exe gosumemory_windows_386.exe 55 | 56 | - name: Build for Linux amd64 57 | run: CC=gcc go build -o gosumemory 58 | 59 | - name: Archive Linux amd64 60 | uses: actions/upload-artifact@v2 61 | with: 62 | name: gosumemory_linux_amd64 63 | path: | 64 | gosumemory 65 | static/ 66 | 67 | - name: Rename Linux amd64 executable 68 | run: mv gosumemory gosumemory_linux_amd64 69 | 70 | - name: Build for Linux 386 71 | run: CC=gcc CGO_ENABLED=1 GOARCH=386 go build -o gosumemory 72 | 73 | - name: Archive Linux 386 74 | uses: actions/upload-artifact@v2 75 | with: 76 | name: gosumemory_linux_386 77 | path: | 78 | gosumemory 79 | static/ 80 | 81 | - name: Rename Linux 386 executable 82 | run: mv gosumemory gosumemory_linux_386 83 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | 6 | build: 7 | name: Build 8 | runs-on: self-hosted 9 | steps: 10 | 11 | - name: Set up Go 1.17 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: 1.17 15 | id: go 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v2 19 | 20 | - name: Get dependencies 21 | run: | 22 | go get -v -t -d ./... 23 | if [ -f Gopkg.toml ]; then 24 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 25 | dep ensure 26 | fi 27 | - name: Build 28 | run: rsrc -arch=amd64 -ico ./out.ico -o gosumemory.syso && CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -v -o win64/gosumemory.exe && rm gosumemory.syso && rsrc -arch=386 -ico ./out.ico -o gosumemory.syso && CC=i686-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -v -o win32/gosumemory.exe && rm gosumemory.syso && CC=gcc go build -v -o lin64/gosumemory && CC=gcc CGO_ENABLED=1 GOARCH=386 go build -v -o lin32/gosumemory 29 | - name: Clone deps 30 | run: git clone https://github.com/l3lackShark/static && find win32 win64 lin32 lin64 -maxdepth 0 -exec cp -rf static {} \; 31 | - name: Upload win32 32 | uses: actions/upload-artifact@v1.0.0 33 | with: 34 | # Artifact name 35 | name: gosumemory_windows_386 36 | # Directory containing files to upload 37 | path: ./win32 38 | 39 | - name: Upload win64 40 | uses: actions/upload-artifact@v1.0.0 41 | with: 42 | # Artifact name 43 | name: gosumemory_windows_amd64 44 | # Directory containing files to upload 45 | path: ./win64 46 | 47 | - name: Upload lin32 48 | uses: actions/upload-artifact@v1.0.0 49 | with: 50 | # Artifact name 51 | name: gosumemory_linux_386 52 | # Directory containing files to upload 53 | path: ./lin32 54 | 55 | - name: Upload lin64 56 | uses: actions/upload-artifact@v1.0.0 57 | with: 58 | # Artifact name 59 | name: gosumemory_linux_amd64 60 | # Directory containing files to upload 61 | path: ./lin64 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | gosumemory 3 | gosumemory.exe 4 | gameoverlay 5 | static 6 | osu!.db 7 | config.ini 8 | __debug_bin 9 | gosumemory.exe.manifest 10 | icon.ico 11 | gosumemory.syso 12 | res.ico 13 | /.idea/gosumemory.iml 14 | /.idea/modules.xml 15 | /.idea/vcs.xml 16 | # Default ignored files 17 | /shelf/ 18 | /workspace.xml 19 | # Datasource local storage ignored files 20 | /../../../../../../../../../:\Users\BlackShark\go\src\github.com\l3lackShark\gosumemory\.idea/dataSources/ 21 | /dataSources.local.xml 22 | # Editor-based HTTP Client requests 23 | /httpRequests/ 24 | gameoverlay.zip 25 | *.exe -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l3lackShark/gosumemory/8b1915f594e218c3fbaa23d0dcddfadd4523c762/.gitmodules -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Launch", 10 | "type": "go", 11 | "request": "launch", 12 | "mode": "auto", 13 | "showGlobalVariables": true, 14 | "program": "${workspaceFolder}/main.go", 15 | "env": {}, 16 | "args": [] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Yet another memory reader for osu! Supports both Linux and Windows. (**requires sudo on Linux** though since only root can read /proc) 4 | 5 | Build custom pp counters with ease!\ 6 | You can contact us here: https://discord.gg/8enr4qD 7 | 8 | # Real-World examples: 9 | 10 | [![cpol](https://img.shields.io/badge/cpol%20v5.0---?style=for-the-badge&color=DFD895)](https://youtu.be/neHcOLycieE) 11 | [![FlyingTuna](https://img.shields.io/badge/FlyingTuna%20v1.5---?style=for-the-badge&color=527FD5)](https://www.twitch.tv/flyingtuna/clip/TransparentObliviousHawkAMPEnergyCherry) 12 | [![Alumetri](https://img.shields.io/badge/Alumetri%20v1.2---?style=for-the-badge&color=FF94B6)](https://mega.nz/file/QV1gTKoI#j1QRjDkrjnFvIhyb9JuGi3g_0XZCFzXEXz9PKWcxgmI) 13 | [![Sotarks](https://img.shields.io/badge/Sotarks%20v1.0---?style=for-the-badge&color=C63F55)](https://mega.nz/file/oAlmlQoY#8ABeJPGboMLgCiaY5vR21HX2Km--_jiwqRHOmUJvVmg) 14 | [![Mrekk](https://img.shields.io/badge/Mrekk%20v1.0---?style=for-the-badge&color=72a0d4)](https://mega.nz/file/UZsEUKDK#Ji3JAUr8_04Q7u0RG1BAJFGzZ2-CRhRZkEQqdXVrv60) 15 | [![Mathi](https://img.shields.io/badge/Mathi%20v1.5---?style=for-the-badge&color=4981CE)](https://mega.nz/file/5dsk1QJD#noUKykU5qJYv53I2DPZ7PY2CIQOftS1ufqzOh4rqOb8) 16 | [![RyuK](https://img.shields.io/badge/RyuK%20v1.0---?style=for-the-badge&color=f72f4d)](https://mega.nz/file/dY8k1YyZ#1Phdta1CzxXDotjtllUKsZunnCdliYlQ1VrZ_BNaNIs) 17 | 18 | 19 | # Usage 20 | 21 | 1. [Download the latest Release](https://github.com/l3lackShark/gosumemory/releases/latest) 22 | * Unzip files anywhere 23 | > In the root folder of the program, you can find the **static** directory. It contains all of the available counters. Those are getting streamed via HTTP-File server 24 | 25 | 2. Run gosumemory & osu! 26 | * Visit this link in your browser: http://localhost:24050 and choose the counter that you like. 27 | * Add a browser source in OBS (Width and Height could be found in the **Included counters** spoiler) 28 | 4. If using built-in counters covers all of your needs, then you are done here. 29 | > **Please note that auto-updates only cover the executable itself, however, if a new counter gets released, we will mention it in the Release Notes.**\ 30 | > If you want to make your own, just create a new directory in the *static* folder. 31 | 32 | # Does this work in-game? 33 | 34 | Yes! see [this](https://github.com/l3lackShark/gosumemory/wiki/GameOverlay) 35 | 36 | 37 | # Included counters: 38 |
39 | Click ME 40 | 41 | ### InGame1 42 | 43 | > Size: 240x133\ 44 | \ 45 | By: [Dartandr][1]
46 | 47 | ### InGame2 48 | 49 | > Size: 370x120\ 50 | \ 51 | By: [Dartandr][1]
52 | 53 | ### InGame3 54 | 55 | > Size: 150x70\ 56 | \ 57 | By: [Dartandr][1]
58 | 59 | 60 | ### reComfortaa 61 | 62 | > Size: 1152x245\ 63 | > *Song Selection*\ 64 | \ 65 | >*Gameplay*\ 66 | \ 67 | By: [Xynogen][6]
68 | 69 | ### MonokaiPane 70 | 71 | > Size: 512x150\ 72 | > *Song Selection*\ 73 | \ 74 | >*Gameplay 1*\ 75 | \ 76 | >*Gameplay 2*\ 77 | \ 78 | By: Xynogen
79 | 80 | ### WaveTournament 81 | 82 | > Size: 1920x1080\ 83 | > *Chat + Warmup (Team score disabled) view*\ 84 | \ 85 | > *Chat + Warmup (Team score disabled) view*\ 86 | \ 87 | > *Gameplay*\ 88 | \ 89 | By: [VictimCrasher][4]
90 | 91 | ### Classic 92 | 93 | > Size: 550x300\ 94 | \ 95 | By: [Dartandr][1]
96 | 97 | ### OldClassic 98 | 99 | > Size: 550x300\ 100 | \ 101 | By: [Dartandr][1]
102 | 103 | ### Simplistic 104 | 105 | > Size: 750x150\ 106 | > *Song Selection*\ 107 | \ 108 | >*Gameplay 1*\ 109 | \ 110 | By: [jassper0][5]
111 | 112 | ### DarkAndWhite 113 | 114 | > Size: 840x140\ 115 | \ 116 | By: [cyperdark][2]
117 | 118 | ### Kerli1 & Kerli2 119 | 120 | > Size (1)(2): 794x124 | 353x190\ 121 | \ 122 | By: [Dartandr][1]
123 | 124 | ### Luscent 125 | 126 | > Size: 1920x1080\ 127 | Open-Source Implementation of [Siae's Luscent][3] overlay. No elements were stolen. This is a remake. Please [consider buying](https://gumroad.com/l/Luscent) her version!\ 128 | \ 129 | Remake by: [Dartandr][1] 130 | 131 | ### VictimCrasherCompact 132 | 133 | > Size: 560x150\ 134 | > *Song Selection*\ 135 | \ 136 | > 137 | > *Gameplay*\ 138 | \ 139 | By: [VictimCrasher][4]
140 | 141 | ### VictimCrasherOverlay 142 | 143 | > Size: 1920x1080\ 144 | \ 145 | By: [VictimCrasher][4]
146 | 147 | ### UnstableRate 148 | 149 | > Size: 300x100\ 150 | Just a plain number that shows current UnstableRate, could be useful if you want to put it above your UR Bar.\ 151 | By: [Dartandr][1] 152 | 153 | ### MaximalLime 154 | 155 | > Size: 800x306\ 156 | \ 157 | By: [cyperdark][2]
158 | 159 | ### MinimalLime 160 | 161 | > Size: 640x130\ 162 | \ 163 | By: [cyperdark][2]
164 | 165 | ### TrafficLight 166 | 167 | > Size: 458x380\ 168 | \ 169 | By: [cyperdark][2]
170 | 171 | [1]: https://github.com/Dartandr 172 | 173 | [2]: https://github.com/cyperdark 174 | 175 | [3]: https://twitter.com/mk_cou/status/1464208290158501894 176 | 177 | [4]: https://github.com/VictimCrasher 178 | 179 | [5]: https://github.com/jassper0 180 | 181 | [6]: https://github.com/Xyn0gen 182 | 183 | 184 |
185 | 186 | # How does it work? 187 | 188 | gosumemory streams WebSocket data to **ws://localhost:24050/ws** that you can use in any programming language to develop a frontend. We recommend JavaScript though, as it's much easier to make something pretty with the Web framework. All of the included counters are good starting points. There is also http://localhost:24050/json that you can open in a web browser to see the available data. We strongly recommend against sending GET requests to that address in production, please **use WebSocket instead**. 189 | 190 | **[Example JSON and a little wiki of it's values](https://github.com/l3lackShark/gosumemory/wiki/JSON-values)** 191 | 192 | # What if I don't know any programming languages but still want to output data to OBS? 193 | https://www.youtube.com/watch?v=8ApXBEO5bes 194 | 195 | # How do I submit a pp counter? 196 | 197 | Head over to [static](https://github.com/l3lackShark/static) and create a pull request there. If it's good quality, then it will get approved and included in the next release. 198 | 199 | # Tournament Client 200 | 201 | When operating in tourney mode, real-time pp counters for each client, leaderboard and grades don't work. Each gameplay object is sorted by tournament client "id", "menu" object is a tournament manager (state 22). 202 | **Tournament mode works only with Cutting Edge tourney client and is not supported on Linux.** 203 | 204 | # Linux 205 | 206 | You have two options. Either run native, but with sudo privileges, or through WINE. 207 | Please note that Linux builds are not well tested and could contain crashes. Report them if you encounter any. 208 | 209 | # Consider supporting 210 | Buy Me A Coffee 211 | 212 | # This project depends on: 213 | 214 | * [cast](https://github.com/spf13/cast) 215 | * [gorilla-websocket](https://github.com/gorilla/websocket) 216 | * [open](https://github.com/skratchdot/open-golang) 217 | * [semver](https://github.com/blang/semver) 218 | * [selfupdate](https://github.com/rhysd/go-github-selfupdate) 219 | * [pretty-print](https://github.com/k0kubun/pp) 220 | * [mp3](https://github.com/tcolgate/mp3) 221 | * [go-windows](https://github.com/elastic/go-windows) 222 | 223 | # Special Thanks to: 224 | 225 | * [Piotrekol](https://github.com/Piotrekol/) and his [ProcessMemoryDataFinder](https://github.com/Piotrekol/ProcessMemoryDataFinder) for most of the memory signatures 226 | * [Francesco149](https://github.com/Francesco149) and his [oppai-ng](https://github.com/Francesco149/oppai-ng) for the pp calculator that we use 227 | * [tdeo](https://github.com/tadeokondrak) for the [Memory Signature Scanner](https://github.com/l3lackShark/gosumemory/tree/master/mem) package 228 | * [omkelderman](https://github.com/omkelderman) for helping out with the [db](https://github.com/l3lackShark/gosumemory/tree/master/db) package 229 | * [jamuwu](https://github.com/jamuwu/osu-strain) and his [osu-strain](https://github.com/jamuwu/osu-strain) for difficulty strain logic 230 | * [cyperdark](https://github.com/cyperdark) and [Dartandr](https://github.com/Dartandr) for frontend designs 231 | * [KotRik](https://github.com/KotRikD) for making an [OBS Script](https://github.com/l3lackShark/gosumemory-helpers/blob/master/gosumemory-reader.py) and porting legacy counters 232 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/l3lackShark/config" 11 | ) 12 | 13 | //Config file 14 | var Config map[string]string 15 | 16 | //Init the config file 17 | func Init() { 18 | ex, err := os.Executable() 19 | if err != nil { 20 | panic(err) 21 | } 22 | exPath := filepath.Dir(ex) 23 | 24 | cfg, err := config.SetFile(filepath.Join(exPath, "config.ini")) 25 | if err == config.ErrDoesNotExist { 26 | d := []byte(`[Main] 27 | update = 100 28 | path = auto 29 | cgodisable = false 30 | memdebug = false 31 | memcycletest = false 32 | wine = false 33 | 34 | [Web] 35 | serverip = 127.0.0.1:24050 36 | cors = false 37 | 38 | [GameOverlay] ; https://github.com/l3lackShark/gosumemory/wiki/GameOverlay 39 | enabled = false 40 | gameWidth = 1920 41 | gameHeight = 1080 42 | overlayURL = http://localhost:24050/InGame2 43 | overlayWidth = 380 44 | overlayHeight = 110 45 | overlayOffsetX = 0 46 | overlayOffsetY = 0 47 | overlayScale = 10 48 | 49 | `) 50 | if err := ioutil.WriteFile(filepath.Join(exPath, "config.ini"), d, 0644); err != nil { 51 | panic(err) 52 | } 53 | cfg, err = config.SetFile(filepath.Join(exPath, "config.ini")) 54 | if err != nil { 55 | panic(err) 56 | } 57 | } else if err != nil { 58 | log.Fatalln(err) 59 | } 60 | Config, err = cfg.Parse() 61 | if err != nil { 62 | panic(err) 63 | } 64 | if Config["overlayURL"] == "" { //Quck hack to append GameOverlay stuff to existing config, whole system needs revamp 65 | file, err := os.OpenFile(filepath.Join(exPath, "config.ini"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 66 | if err != nil { 67 | panic(err) 68 | } 69 | _, err = file.WriteString(fmt.Sprintf("\n[GameOverlay]; https://github.com/l3lackShark/gosumemory/wiki/GameOverlay\nenabled = false\ngameWidth = 1920\ngameHeight = 1080\noverlayURL = http://localhost:24050/InGame2\noverlayWidth = 380\noverlayHeight = 110\noverlayOffsetX = 0\noverlayOffsetY = 0\noverlayScale = 10")) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | Init() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "runtime/debug" 13 | "strings" 14 | "time" 15 | 16 | "github.com/l3lackShark/gosumemory/memory" 17 | 18 | "github.com/k0kubun/pp" 19 | ) 20 | 21 | type osudb struct { 22 | buildVer int32 23 | songsFolderSize int32 24 | isAccountUnlocked bool 25 | Nickname string 26 | BmInfo []beatmapInfo 27 | } 28 | 29 | type beatmapInfo struct { 30 | Artist string 31 | artistU string 32 | Title string 33 | titleU string 34 | Creator string 35 | Difficulty string 36 | audioName string 37 | md5 string 38 | Filename string 39 | rankedStatus int8 40 | NumHitCircles int16 41 | NumSliders int16 42 | NumSpinners int16 43 | dateTime int64 44 | approachRate float32 45 | circleSize float32 46 | hpDrain float32 47 | overallDifficulty float32 48 | sliderVelocity float64 //double 49 | starRatingOsu []starRating 50 | starRatingTaiko []starRating 51 | starRatingCtb []starRating 52 | StarRatingMania []starRating 53 | drainTime int32 54 | totalTime int32 55 | previewTime int32 56 | timingPoints []timingPoint 57 | beatmapID int32 58 | beatmapSetID int32 59 | threadID int32 60 | gradeOsu int8 61 | gradeTaiko int8 62 | gradeCtb int8 63 | gradeMania int8 64 | localOffset int16 65 | stackLeniency float32 66 | gameMode int8 67 | songSource string 68 | songTags string 69 | onlineOffset int16 70 | fontTitle string //? 71 | isUnplayed bool 72 | lastPlayed int64 73 | isOsz2 bool 74 | folderFromSongs string 75 | lastCheckedAgainstOsuRepo int64 76 | isBmSoundIgnored bool 77 | isBmSkinIgnored bool 78 | isBmStoryBoardDisabled bool 79 | isBmVideoDisabled bool 80 | isVisualOverride bool 81 | lastClosedEditor int32 82 | maniaScrollSpeed uint8 83 | } 84 | 85 | type starRating struct { 86 | BitMods int32 87 | StarRating float64 //double 88 | } 89 | 90 | type timingPoint struct { 91 | msPerBeat float64 //double 92 | songOffset float64 //double 93 | inheritedTimingPoint bool 94 | } 95 | 96 | //OsuDB is a structure representation of osu!.db file 97 | var OsuDB osudb 98 | 99 | var internalDB osudb 100 | 101 | //InitDB initializes osu database and gets data within it 102 | func InitDB() error { 103 | fmt.Println("[DB] Awaiting memory data...") 104 | for memory.DynamicAddresses.IsReady != true { 105 | time.Sleep(500 * time.Millisecond) 106 | } 107 | fmt.Println("[DB] Parsing osu!db...") 108 | dbpath := strings.TrimSuffix(memory.SongsFolderPath, "\\Songs") 109 | file, err := os.Open(filepath.Join(dbpath, "osu!.db")) 110 | if err != nil { 111 | pp.Println("Could not find osu!.db, mania related functionality will be unavailable") 112 | return nil 113 | } 114 | osuDB := bufio.NewReader(file) 115 | defer file.Close() 116 | binary.Read(osuDB, binary.LittleEndian, &internalDB.buildVer) 117 | binary.Read(osuDB, binary.LittleEndian, &internalDB.songsFolderSize) 118 | binary.Read(osuDB, binary.LittleEndian, &internalDB.isAccountUnlocked) 119 | var dateTime int64 120 | binary.Read(osuDB, binary.LittleEndian, &dateTime) 121 | internalDB.Nickname, err = readDBString(osuDB) 122 | if err != nil { 123 | log.Println("Database parse error: ", err) 124 | } 125 | internalDB.BmInfo, err = readDBArray(osuDB) 126 | if err != nil { 127 | panic(err) 128 | } 129 | OsuDB.BmInfo = make([]beatmapInfo, len(internalDB.BmInfo)) 130 | OsuDB.isAccountUnlocked = internalDB.isAccountUnlocked 131 | OsuDB.buildVer = internalDB.buildVer 132 | OsuDB.Nickname = internalDB.Nickname 133 | OsuDB.songsFolderSize = internalDB.songsFolderSize 134 | for i := 0; i < len(internalDB.BmInfo); i++ { 135 | OsuDB.BmInfo[i].StarRatingMania = make([]starRating, len(internalDB.BmInfo[i].StarRatingMania)) 136 | OsuDB.BmInfo[i].Filename = internalDB.BmInfo[i].Filename 137 | OsuDB.BmInfo[i].Artist = internalDB.BmInfo[i].Artist 138 | OsuDB.BmInfo[i].Title = internalDB.BmInfo[i].Title 139 | OsuDB.BmInfo[i].NumHitCircles = internalDB.BmInfo[i].NumHitCircles 140 | OsuDB.BmInfo[i].NumSliders = internalDB.BmInfo[i].NumSliders 141 | OsuDB.BmInfo[i].NumSpinners = internalDB.BmInfo[i].NumSpinners 142 | OsuDB.BmInfo[i].Creator = internalDB.BmInfo[i].Creator 143 | OsuDB.BmInfo[i].Difficulty = internalDB.BmInfo[i].Difficulty 144 | for j := 0; j < len(internalDB.BmInfo[i].StarRatingMania); j++ { 145 | if internalDB.BmInfo[i].StarRatingMania[j].BitMods == 0 || internalDB.BmInfo[i].StarRatingMania[j].BitMods == 64 || internalDB.BmInfo[i].StarRatingMania[j].BitMods == 256 { 146 | OsuDB.BmInfo[i].StarRatingMania[j].BitMods = internalDB.BmInfo[i].StarRatingMania[j].BitMods 147 | OsuDB.BmInfo[i].StarRatingMania[j].StarRating = internalDB.BmInfo[i].StarRatingMania[j].StarRating 148 | } 149 | } 150 | } 151 | internalDB = osudb{} 152 | debug.FreeOSMemory() 153 | fmt.Println("[DB] Done parsing osu!db") 154 | 155 | return nil 156 | } 157 | 158 | func readVarUint(r io.Reader, n uint) (uint64, error) { 159 | if n > 64 { 160 | panic(errors.New("leb128: n must <= 64")) 161 | } 162 | p := make([]byte, 1) 163 | var res uint64 164 | var shift uint 165 | for { 166 | _, err := io.ReadFull(r, p) 167 | if err != nil { 168 | return 0, err 169 | } 170 | b := uint64(p[0]) 171 | switch { 172 | // note: can not use b < 1<= 1<<7 && n > 7: 177 | res += (1 << shift) * (b - 1<<7) 178 | shift += 7 179 | n -= 7 180 | default: 181 | return 0, errors.New("leb128: invalid uint") 182 | } 183 | } 184 | } 185 | 186 | func readDBString(osuDB io.Reader) (string, error) { 187 | var checkByte byte 188 | err := binary.Read(osuDB, binary.LittleEndian, &checkByte) 189 | if err != nil { 190 | return "", err 191 | } 192 | switch checkByte { 193 | case 0x00: 194 | return "", nil 195 | case 0x0b: 196 | strlen, err := readVarUint(osuDB, 32) 197 | if err != nil { 198 | return "", err 199 | } 200 | stringBytes := make([]byte, int(strlen)) 201 | _, err = io.ReadFull(osuDB, stringBytes) 202 | if err != nil { 203 | return "", err 204 | } 205 | return string(stringBytes[:]), nil 206 | 207 | default: 208 | return "", errors.New("string parse error") 209 | } 210 | } 211 | func readDBArray(osuDB io.Reader) ([]beatmapInfo, error) { 212 | var arrLength int32 213 | err := binary.Read(osuDB, binary.LittleEndian, &arrLength) 214 | if err != nil { 215 | return nil, err 216 | } 217 | if arrLength == -1 { 218 | return nil, nil 219 | } 220 | beatmapsArray := make([]beatmapInfo, int(arrLength)) 221 | for i := 0; i < int(arrLength); i++ { 222 | beatmapsArray[i], err = readBeatmapInfo(osuDB) 223 | if err != nil { 224 | return nil, err 225 | } 226 | } 227 | return beatmapsArray, nil 228 | } 229 | func readBeatmapInfo(osuDB io.Reader) (beatmapInfo, error) { 230 | 231 | data := beatmapInfo{} 232 | var err error 233 | data.Artist, err = readDBString(osuDB) 234 | data.artistU, err = readDBString(osuDB) 235 | data.Title, err = readDBString(osuDB) 236 | data.titleU, err = readDBString(osuDB) 237 | data.Creator, err = readDBString(osuDB) 238 | data.Difficulty, err = readDBString(osuDB) 239 | data.audioName, err = readDBString(osuDB) 240 | data.md5, err = readDBString(osuDB) 241 | data.Filename, err = readDBString(osuDB) 242 | err = binary.Read(osuDB, binary.LittleEndian, &data.rankedStatus) 243 | err = binary.Read(osuDB, binary.LittleEndian, &data.NumHitCircles) 244 | err = binary.Read(osuDB, binary.LittleEndian, &data.NumSliders) 245 | err = binary.Read(osuDB, binary.LittleEndian, &data.NumSpinners) 246 | err = binary.Read(osuDB, binary.LittleEndian, &data.dateTime) 247 | err = binary.Read(osuDB, binary.LittleEndian, &data.approachRate) 248 | err = binary.Read(osuDB, binary.LittleEndian, &data.circleSize) 249 | err = binary.Read(osuDB, binary.LittleEndian, &data.hpDrain) 250 | err = binary.Read(osuDB, binary.LittleEndian, &data.overallDifficulty) 251 | err = binary.Read(osuDB, binary.LittleEndian, &data.sliderVelocity) 252 | var lengthList int32 // should move this into a separate functuion and use reflections to set values 253 | err = binary.Read(osuDB, binary.LittleEndian, &lengthList) 254 | if lengthList >= 1 { 255 | var zeroXeight uint8 256 | var zeroXzerod uint8 257 | data.starRatingOsu = make([]starRating, int(lengthList)) 258 | for i := 0; i < int(lengthList); i++ { 259 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXeight) 260 | err = binary.Read(osuDB, binary.LittleEndian, &data.starRatingOsu[i].BitMods) 261 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXzerod) 262 | if zeroXzerod != 0x0d || zeroXeight != 0x08 { 263 | pp.Println("Star rating parse err.") 264 | data := beatmapInfo{} 265 | return data, errors.New("Star rating parse err") 266 | } 267 | err = binary.Read(osuDB, binary.LittleEndian, &data.starRatingOsu[i].StarRating) 268 | if err != nil { 269 | data := beatmapInfo{} 270 | return data, err 271 | } 272 | } 273 | } 274 | 275 | var lengthListTaiko int32 // should move this into a separate functuion and use reflections to set values 276 | err = binary.Read(osuDB, binary.LittleEndian, &lengthListTaiko) 277 | if lengthListTaiko >= 1 { 278 | var zeroXeight uint8 279 | var zeroXzerod uint8 280 | data.starRatingTaiko = make([]starRating, int(lengthListTaiko)) 281 | for i := 0; i < int(lengthListTaiko); i++ { 282 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXeight) 283 | err = binary.Read(osuDB, binary.LittleEndian, &data.starRatingTaiko[i].BitMods) 284 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXzerod) 285 | if zeroXzerod != 0x0d || zeroXeight != 0x08 { 286 | pp.Println("Star rating parse err. (taiko)") 287 | data := beatmapInfo{} 288 | return data, errors.New("Star rating parse err (taiko)") 289 | } 290 | err = binary.Read(osuDB, binary.LittleEndian, &data.starRatingTaiko[i].StarRating) 291 | if err != nil { 292 | data := beatmapInfo{} 293 | return data, err 294 | } 295 | } 296 | } 297 | 298 | var lengthListCtb int32 // should move this into a separate functuion and use reflections to set values 299 | err = binary.Read(osuDB, binary.LittleEndian, &lengthListCtb) 300 | if lengthListCtb >= 1 { 301 | var zeroXeight uint8 302 | var zeroXzerod uint8 303 | data.starRatingCtb = make([]starRating, int(lengthListCtb)) 304 | for i := 0; i < int(lengthListCtb); i++ { 305 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXeight) 306 | err = binary.Read(osuDB, binary.LittleEndian, &data.starRatingCtb[i].BitMods) 307 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXzerod) 308 | if zeroXzerod != 0x0d || zeroXeight != 0x08 { 309 | pp.Println("Star rating parse err. (ctb)") 310 | data := beatmapInfo{} 311 | return data, errors.New("Star rating parse err (ctb)") 312 | } 313 | err = binary.Read(osuDB, binary.LittleEndian, &data.starRatingCtb[i].StarRating) 314 | if err != nil { 315 | data := beatmapInfo{} 316 | return data, err 317 | } 318 | } 319 | } 320 | var lengthListMania int32 // should move this into a separate functuion and use reflections to set values 321 | err = binary.Read(osuDB, binary.LittleEndian, &lengthListMania) 322 | if lengthListMania >= 1 { 323 | var zeroXeight uint8 324 | var zeroXzerod uint8 325 | data.StarRatingMania = make([]starRating, int(lengthListMania)) 326 | for i := 0; i < int(lengthListMania); i++ { 327 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXeight) 328 | err = binary.Read(osuDB, binary.LittleEndian, &data.StarRatingMania[i].BitMods) 329 | err = binary.Read(osuDB, binary.LittleEndian, &zeroXzerod) 330 | if zeroXzerod != 0x0d || zeroXeight != 0x08 { 331 | pp.Println("Star rating parse err. (Mania)") 332 | data := beatmapInfo{} 333 | return data, errors.New("Star rating parse err (Mania)") 334 | } 335 | err = binary.Read(osuDB, binary.LittleEndian, &data.StarRatingMania[i].StarRating) 336 | if err != nil { 337 | data := beatmapInfo{} 338 | return data, err 339 | } 340 | } 341 | } 342 | err = binary.Read(osuDB, binary.LittleEndian, &data.drainTime) 343 | err = binary.Read(osuDB, binary.LittleEndian, &data.totalTime) 344 | err = binary.Read(osuDB, binary.LittleEndian, &data.previewTime) 345 | 346 | var lengthTimingPoints int32 347 | err = binary.Read(osuDB, binary.LittleEndian, &lengthTimingPoints) 348 | if lengthTimingPoints >= 1 { 349 | data.timingPoints = make([]timingPoint, int(lengthTimingPoints)) 350 | for i := 0; i < int(lengthTimingPoints); i++ { 351 | err = binary.Read(osuDB, binary.LittleEndian, &data.timingPoints[i].msPerBeat) 352 | err = binary.Read(osuDB, binary.LittleEndian, &data.timingPoints[i].songOffset) 353 | err = binary.Read(osuDB, binary.LittleEndian, &data.timingPoints[i].inheritedTimingPoint) 354 | if err != nil { 355 | data := beatmapInfo{} 356 | return data, err 357 | } 358 | } 359 | } 360 | err = binary.Read(osuDB, binary.LittleEndian, &data.beatmapID) 361 | err = binary.Read(osuDB, binary.LittleEndian, &data.beatmapSetID) 362 | err = binary.Read(osuDB, binary.LittleEndian, &data.threadID) 363 | err = binary.Read(osuDB, binary.LittleEndian, &data.gradeOsu) 364 | err = binary.Read(osuDB, binary.LittleEndian, &data.gradeTaiko) 365 | err = binary.Read(osuDB, binary.LittleEndian, &data.gradeCtb) 366 | err = binary.Read(osuDB, binary.LittleEndian, &data.gradeMania) 367 | err = binary.Read(osuDB, binary.LittleEndian, &data.localOffset) 368 | err = binary.Read(osuDB, binary.LittleEndian, &data.stackLeniency) 369 | err = binary.Read(osuDB, binary.LittleEndian, &data.gameMode) 370 | data.songSource, err = readDBString(osuDB) 371 | data.songTags, err = readDBString(osuDB) 372 | err = binary.Read(osuDB, binary.LittleEndian, &data.onlineOffset) 373 | data.fontTitle, err = readDBString(osuDB) 374 | err = binary.Read(osuDB, binary.LittleEndian, &data.isUnplayed) 375 | err = binary.Read(osuDB, binary.LittleEndian, &data.lastPlayed) 376 | err = binary.Read(osuDB, binary.LittleEndian, &data.isOsz2) 377 | data.folderFromSongs, err = readDBString(osuDB) 378 | err = binary.Read(osuDB, binary.LittleEndian, &data.lastCheckedAgainstOsuRepo) 379 | err = binary.Read(osuDB, binary.LittleEndian, &data.isBmSoundIgnored) 380 | err = binary.Read(osuDB, binary.LittleEndian, &data.isBmSkinIgnored) 381 | err = binary.Read(osuDB, binary.LittleEndian, &data.isBmStoryBoardDisabled) 382 | err = binary.Read(osuDB, binary.LittleEndian, &data.isBmVideoDisabled) 383 | err = binary.Read(osuDB, binary.LittleEndian, &data.isVisualOverride) 384 | err = binary.Read(osuDB, binary.LittleEndian, &data.lastClosedEditor) 385 | err = binary.Read(osuDB, binary.LittleEndian, &data.maniaScrollSpeed) 386 | 387 | if err != nil { 388 | data := beatmapInfo{} 389 | return data, err 390 | } 391 | return data, nil 392 | } 393 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/l3lackShark/gosumemory 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Wieku/gosu-pp v0.0.0-20211202005932-7ae98709eece // indirect 7 | github.com/blang/semver v3.5.1+incompatible 8 | github.com/elastic/go-windows v1.0.1 9 | github.com/gorilla/websocket v1.4.2 10 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect 11 | github.com/k0kubun/pp v3.0.1+incompatible 12 | github.com/l3lackShark/config v0.0.0-20201023014929-236482b96fde 13 | github.com/mattn/go-colorable v0.1.11 // indirect 14 | github.com/rhysd/go-github-selfupdate v1.2.3 15 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 16 | github.com/spf13/cast v1.4.1 17 | github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 18 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c 19 | golang.org/x/text v0.3.7 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Wieku/gosu-pp v0.0.0-20211202005932-7ae98709eece h1:bwCUYu9idzXTf5qYGHJtgbDLCaL7wJ9aIxyTjWR4EOg= 2 | github.com/Wieku/gosu-pp v0.0.0-20211202005932-7ae98709eece/go.mod h1:1Bj2dQOyHt5BCgltsCoiWdRCl7vDb1OgIEiB2V1pmz0= 3 | github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= 4 | github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= 9 | github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= 10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 13 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= 15 | github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= 16 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 17 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 18 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 19 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 20 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 21 | github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= 22 | github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= 23 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= 24 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 25 | github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= 26 | github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= 27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 28 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 29 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 30 | github.com/l3lackShark/config v0.0.0-20201023014929-236482b96fde h1:Z2oPYkaxn/g/xJ6cy1nZHSpgRuKGdkUC5xDT3JJOy2k= 31 | github.com/l3lackShark/config v0.0.0-20201023014929-236482b96fde/go.mod h1:ga9kHvMOqz3nFftshR6URgGs1C2fXFX2QBruGbgT1wg= 32 | github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= 33 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 34 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 35 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 36 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 37 | github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= 38 | github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 39 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 40 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag= 44 | github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg= 45 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= 46 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 47 | github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= 48 | github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 51 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 52 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 53 | github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= 54 | github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= 55 | github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI= 56 | github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI= 57 | github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= 58 | github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 60 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 61 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 62 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 63 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 64 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 66 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 67 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 68 | golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= 69 | golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 70 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= 78 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 80 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 81 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 82 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 83 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 84 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 85 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 86 | google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= 87 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 88 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 91 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 92 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 93 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 94 | -------------------------------------------------------------------------------- /injctr/injctr.go: -------------------------------------------------------------------------------- 1 | package injctr 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | //Injct dll into osu's process 18 | func Injct(pid int) error { 19 | if runtime.GOOS != "windows" { 20 | return errors.New("Gameoverlay only works under windows") 21 | } 22 | ex, err := os.Executable() 23 | if err != nil { 24 | panic(err) 25 | } 26 | exPath := filepath.Dir(ex) 27 | _, err = os.Stat("gameoverlay") 28 | if err != nil { 29 | fmt.Println("[GAMEOVERLAY] Downloading gameoverlay... (can take a while, filesize is around 60MB)") 30 | err = downloadFile("https://omk.pics/12/wVugx", "gameoverlay.zip") 31 | if err != nil { 32 | return err 33 | } 34 | unzip("gameoverlay.zip", "gameoverlay") 35 | err = os.Remove("gameoverlay.zip") 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | 41 | _, err = os.Stat("gameoverlay\\gosumemoryoverlay.dll") 42 | if err != nil { 43 | return err 44 | } 45 | _, err = exec.Command("gameoverlay\\a.exe", strconv.Itoa(pid), filepath.Join(exPath, "gameoverlay", "gosumemoryoverlay.dll")).Output() 46 | if err != nil { 47 | return err 48 | } 49 | fmt.Println("[GAMEOVERLAY] Initialized successfully, see https://github.com/l3lackShark/gosumemory/wiki/GameOverlay for tutorial") 50 | return nil 51 | } 52 | 53 | func downloadFile(URL, fileName string) error { 54 | //Get the response bytes from the url 55 | response, err := http.Get(URL) 56 | if err != nil { 57 | return err 58 | } 59 | defer response.Body.Close() 60 | 61 | if response.StatusCode != 200 { 62 | return errors.New("Received non 200 response code") 63 | } 64 | //Create a empty file 65 | file, err := os.Create(fileName) 66 | if err != nil { 67 | return err 68 | } 69 | defer file.Close() 70 | 71 | //Write the bytes to the fiel 72 | _, err = io.Copy(file, response.Body) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func unzip(src, dest string) error { 81 | r, err := zip.OpenReader(src) 82 | if err != nil { 83 | return err 84 | } 85 | defer func() { 86 | if err := r.Close(); err != nil { 87 | panic(err) 88 | } 89 | }() 90 | 91 | // Closure to address file descriptors issue with all the deferred .Close() methods 92 | extractAndWriteFile := func(f *zip.File) error { 93 | rc, err := f.Open() 94 | if err != nil { 95 | return err 96 | } 97 | defer func() { 98 | if err := rc.Close(); err != nil { 99 | panic(err) 100 | } 101 | }() 102 | 103 | path := filepath.Join(dest, f.Name) 104 | 105 | // Check for ZipSlip (Directory traversal) 106 | if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) { 107 | return fmt.Errorf("illegal file path: %s", path) 108 | } 109 | 110 | if f.FileInfo().IsDir() { 111 | os.MkdirAll(path, f.Mode()) 112 | } else { 113 | os.MkdirAll(filepath.Dir(path), f.Mode()) 114 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 115 | if err != nil { 116 | return err 117 | } 118 | defer func() { 119 | if err := f.Close(); err != nil { 120 | panic(err) 121 | } 122 | }() 123 | 124 | _, err = io.Copy(f, rc) 125 | if err != nil { 126 | return err 127 | } 128 | } 129 | return nil 130 | } 131 | 132 | for _, f := range r.File { 133 | err := extractAndWriteFile(f) 134 | if err != nil { 135 | return err 136 | } 137 | } 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/spf13/cast" 10 | 11 | "github.com/l3lackShark/gosumemory/config" 12 | 13 | "github.com/l3lackShark/gosumemory/mem" 14 | "github.com/l3lackShark/gosumemory/memory" 15 | "github.com/l3lackShark/gosumemory/pp" 16 | "github.com/l3lackShark/gosumemory/updater" 17 | "github.com/l3lackShark/gosumemory/web" 18 | ) 19 | 20 | func main() { 21 | config.Init() 22 | updateTimeFlag := flag.Int("update", cast.ToInt(config.Config["update"]), "How fast should we update the values? (in milliseconds)") 23 | shouldWeUpdate := flag.Bool("autoupdate", true, "Should we auto update the application?") 24 | isRunningInWINE := flag.Bool("wine", cast.ToBool(config.Config["wine"]), "Running under WINE?") 25 | songsFolderFlag := flag.String("path", config.Config["path"], `Path to osu! Songs directory ex: /mnt/ps3drive/osu\!/Songs`) 26 | memDebugFlag := flag.Bool("memdebug", cast.ToBool(config.Config["memdebug"]), `Enable verbose memory debugging?`) 27 | memCycleTestFlag := flag.Bool("memcycletest", cast.ToBool(config.Config["memcycletest"]), `Enable memory cycle time measure?`) 28 | disablecgo := flag.Bool("cgodisable", cast.ToBool(config.Config["cgodisable"]), `Disable everything non memory-reader related? (pp counters)`) 29 | flag.Parse() 30 | cgo := *disablecgo 31 | mem.Debug = *memDebugFlag 32 | memory.MemCycle = *memCycleTestFlag 33 | memory.UpdateTime = *updateTimeFlag 34 | memory.SongsFolderPath = *songsFolderFlag 35 | memory.UnderWine = *isRunningInWINE 36 | if runtime.GOOS != "windows" && memory.SongsFolderPath == "auto" { 37 | log.Fatalln("Please specify path to osu!Songs (see --help)") 38 | } 39 | if memory.SongsFolderPath != "auto" { 40 | if _, err := os.Stat(memory.SongsFolderPath); os.IsNotExist(err) { 41 | log.Fatalln(`Specified Songs directory does not exist on the system! (try setting to "auto" if you are on Windows or make sure that the path is correct)`) 42 | } 43 | } 44 | if *shouldWeUpdate == true { 45 | updater.DoSelfUpdate() 46 | } 47 | 48 | go memory.Init() 49 | // err := db.InitDB() 50 | // if err != nil { 51 | // log.Println(err) 52 | // time.Sleep(5 * time.Second) 53 | // os.Exit(1) 54 | // } 55 | go web.SetupStructure() 56 | go web.SetupRoutes() 57 | if !cgo { 58 | go pp.GetData() 59 | go pp.GetFCData() 60 | go pp.GetMaxData() 61 | go pp.GetEditorData() 62 | } 63 | web.HTTPServer() 64 | 65 | } 66 | -------------------------------------------------------------------------------- /mem/debug.go: -------------------------------------------------------------------------------- 1 | package mem 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "sync/atomic" 8 | ) 9 | 10 | var Debug = false 11 | var indent int32 = 0 12 | 13 | func beginDebug() { 14 | if !Debug { 15 | return 16 | } 17 | 18 | var callers [8]uintptr 19 | n := runtime.Callers(2, callers[:]) 20 | 21 | frames := runtime.CallersFrames(callers[:n]) 22 | var stack []string 23 | for { 24 | frame, more := frames.Next() 25 | if strings.HasPrefix(frame.Function, "runtime") { 26 | break 27 | } 28 | stack = append(stack, 29 | fmt.Sprintf("%s:%d", frame.Function, frame.Line)) 30 | if !more { 31 | break 32 | } 33 | } 34 | for i, j := 0, len(stack)-1; i < j; i, j = i+1, j-1 { 35 | stack[i], stack[j] = stack[j], stack[i] 36 | } 37 | for _, s := range stack { 38 | log("%s\n", s) 39 | atomic.AddInt32(&indent, 1) 40 | } 41 | } 42 | 43 | func pushDebug() int32 { 44 | if !Debug { 45 | return 0 46 | } 47 | 48 | return atomic.AddInt32(&indent, 1) - 1 49 | } 50 | 51 | func popDebug(i int32) { 52 | if !Debug { 53 | return 54 | } 55 | 56 | atomic.StoreInt32(&indent, i) 57 | } 58 | 59 | func endDebug() { 60 | if !Debug { 61 | return 62 | } 63 | 64 | atomic.StoreInt32(&indent, 0) 65 | } 66 | 67 | func log(format string, args ...interface{}) { 68 | if !Debug { 69 | return 70 | } 71 | 72 | for i := int32(0); i < atomic.LoadInt32(&indent)*4; i++ { 73 | fmt.Printf(" ") 74 | } 75 | fmt.Printf(format, args...) 76 | } 77 | 78 | func logRead(b []byte, n int, off int64, err error) { 79 | if !Debug { 80 | return 81 | } 82 | 83 | if err == nil { 84 | var arr string 85 | if n < 16 { 86 | arr = fmt.Sprintf("%v", b[:n]) 87 | } else { 88 | arr = "[...]" 89 | } 90 | log("Read(0x%x, %d): %s\n", uint64(off), n, arr) 91 | } else { 92 | log("Read(0x%x, %d): %v\n", uint64(off), n, err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /mem/linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package mem 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "strconv" 15 | 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | func FindProcess(re *regexp.Regexp, blacklistedTitles ...string) ([]Process, error) { //blacklistedTitles not implemented yet 20 | dirs, err := ioutil.ReadDir("/proc") 21 | if err != nil { 22 | return nil, err 23 | } 24 | var pids []int 25 | for _, dir := range dirs { 26 | if pid, err := strconv.Atoi(dir.Name()); err == nil { 27 | pids = append(pids, pid) 28 | } 29 | } 30 | var procs []Process 31 | for _, pid := range pids { 32 | path := fmt.Sprintf("/proc/%d/cmdline", pid) 33 | f, err := os.Open(path) 34 | if err != nil { 35 | continue 36 | } 37 | defer f.Close() 38 | 39 | content, err := ioutil.ReadAll(f) 40 | if err != nil { 41 | continue 42 | } 43 | 44 | slices := bytes.SplitN(content, []byte{'\x00'}, 2) 45 | if !re.Match(slices[0]) { 46 | continue 47 | } 48 | 49 | procs = append(procs, process{pid}) 50 | } 51 | if len(procs) < 1 { 52 | return nil, ErrNoProcess 53 | } 54 | return procs, nil 55 | } 56 | 57 | type process struct { 58 | pid int 59 | } 60 | 61 | func (p process) ExecutablePath() (string, error) { 62 | path := fmt.Sprintf("/proc/%d/exe", p.pid) 63 | path, err := filepath.EvalSymlinks(path) 64 | if err != nil { 65 | return "", err 66 | } 67 | return filepath.Abs(path) 68 | } 69 | 70 | func (p process) Close() error { 71 | return nil 72 | } 73 | 74 | func (p process) Pid() int { 75 | return p.pid 76 | } 77 | 78 | func (p process) ReadAt(b []byte, off int64) (n int, err error) { 79 | localIov := [1]unix.Iovec{ 80 | {Base: &b[0]}, 81 | } 82 | localIov[0].SetLen(len(b)) 83 | remoteIov := [1]unix.RemoteIovec{ 84 | {Base: uintptr(off), Len: len(b)}, 85 | } 86 | n, err = unix.ProcessVMReadv(p.pid, localIov[:], remoteIov[:], 0) 87 | logRead(b, n, off, err) 88 | return n, err 89 | } 90 | 91 | func (p process) Maps() ([]Map, error) { 92 | path := fmt.Sprintf("/proc/%d/maps", p.pid) 93 | f, err := os.Open(path) 94 | if err != nil { 95 | return nil, err 96 | } 97 | defer f.Close() 98 | 99 | var maps []Map 100 | s := bufio.NewScanner(f) 101 | for s.Scan() { 102 | var reg region 103 | _, err := fmt.Sscanf(s.Text(), "%x-%x", 104 | ®.start, ®.end) 105 | if err != nil && err != io.EOF { 106 | return nil, err 107 | } 108 | maps = append(maps, reg) 109 | } 110 | return maps, nil 111 | } 112 | 113 | type region struct { 114 | start int64 115 | end int64 116 | } 117 | 118 | func (r region) Start() int64 { 119 | return r.start 120 | } 121 | 122 | func (r region) Size() int64 { 123 | return r.end - r.start 124 | } 125 | -------------------------------------------------------------------------------- /mem/mem.go: -------------------------------------------------------------------------------- 1 | package mem 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | var ( 9 | ErrNoProcess = errors.New("no process matching the criteria was found") 10 | ErrPatternNotFound = errors.New("no memory matched the pattern") 11 | ) 12 | 13 | type ( 14 | Process interface { 15 | io.Closer 16 | io.ReaderAt 17 | Pid() int 18 | Maps() ([]Map, error) 19 | ExecutablePath() (string, error) 20 | } 21 | 22 | Map interface { 23 | Start() int64 24 | Size() int64 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /mem/read.go: -------------------------------------------------------------------------------- 1 | package mem 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "math" 8 | "unicode/utf16" 9 | ) 10 | 11 | const ( 12 | MaxStringLength = 4096 13 | MaxArrayLength = 65536 14 | ) 15 | 16 | var ( 17 | ErrStringTooLong = errors.New("read failed, string too long") 18 | ErrArrayTooLong = errors.New("read failed, array too long") 19 | 20 | ErrInvalidStringLength = errors.New("read failed, string length < 0") 21 | ErrInvalidArrayLength = errors.New("read failed, array length < 0") 22 | ) 23 | 24 | func readFullAt(r io.ReaderAt, buf []byte, off int64) (n int, err error) { 25 | for n < len(buf) && err == nil { 26 | var nn int 27 | nn, err = r.ReadAt(buf[n:], off+int64(n)) 28 | n += nn 29 | } 30 | 31 | if n >= len(buf) { 32 | err = nil 33 | } else if n > 0 && err == io.EOF { 34 | err = io.ErrUnexpectedEOF 35 | } 36 | 37 | return 38 | } 39 | 40 | func bytesToInt(buf []byte) uint64 { 41 | var num uint64 42 | 43 | for i := range buf { 44 | num |= uint64(buf[i]) << (8 * i) 45 | } 46 | 47 | return num 48 | } 49 | 50 | func removeLast(slice []int64) ([]int64, *int64) { 51 | if len(slice) == 0 { 52 | return nil, nil 53 | } 54 | 55 | return slice[:len(slice)-1], &slice[len(slice)-1] 56 | } 57 | 58 | func followOffsets(r io.ReaderAt, addr int64, offsets ...int64) (int64, error) { 59 | start, last := removeLast(offsets) 60 | 61 | for _, offset := range start { 62 | newaddr, err := ReadPtr(r, addr+offset, 0) 63 | if err != nil { 64 | return 0, err 65 | } 66 | addr = newaddr 67 | } 68 | 69 | if last != nil { 70 | addr += *last 71 | } 72 | 73 | return addr, nil 74 | } 75 | 76 | func readUintRaw(r io.ReaderAt, addr int64, size int) (uint64, error) { 77 | var buf [8]byte 78 | 79 | if _, err := readFullAt(r, buf[:size], addr); err != nil { 80 | return 0, err 81 | } 82 | 83 | return bytesToInt(buf[:size]), nil 84 | } 85 | 86 | func readUint(r io.ReaderAt, addr int64, size int, offsets ...int64) (uint64, error) { 87 | addr, err := followOffsets(r, addr, offsets...) 88 | if err != nil { 89 | return 0, err 90 | } 91 | 92 | return readUintRaw(r, addr, size) 93 | } 94 | 95 | func readUintArray(r io.ReaderAt, addr int64, size int, 96 | offsets ...int64) ([]uint64, error) { 97 | base, err := followOffsets(r, addr, offsets...) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | length, err := ReadInt32(r, base, 12) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | if length < 0 { 108 | return nil, ErrInvalidArrayLength 109 | } 110 | 111 | if length > MaxArrayLength { 112 | return nil, ErrArrayTooLong 113 | } 114 | 115 | data, err := ReadPtr(r, base, 4) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | buf := make([]byte, int(length)*size) 121 | _, err = readFullAt(r, buf, data+8) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | buf64 := make([]uint64, length) 127 | for i := 0; i < int(length); i++ { 128 | buf64[i] = bytesToInt(buf[i*size : i*size+size]) 129 | } 130 | 131 | return buf64, nil 132 | } 133 | 134 | func ReadString(r io.ReaderAt, addr int64, offsets ...int64) (string, error) { 135 | base, err := followOffsets(r, addr, offsets...) 136 | if err != nil { 137 | return "", err 138 | } 139 | 140 | length, err := ReadUint32(r, base, 4) 141 | if err != nil { 142 | return "", err 143 | } 144 | 145 | if length < 0 { 146 | return "", ErrInvalidStringLength 147 | } 148 | 149 | if length > MaxStringLength { 150 | return "", ErrStringTooLong 151 | } 152 | 153 | buf := make([]byte, length*2) 154 | 155 | _, err = readFullAt(r, buf, base+8) 156 | if err != nil { 157 | return "", err 158 | } 159 | 160 | buf16 := make([]uint16, length) 161 | for i := uint32(0); i < length; i += 1 { 162 | buf16[i] = binary.LittleEndian.Uint16(buf[i*2 : i*2+2]) 163 | } 164 | 165 | return string(utf16.Decode(buf16)), nil 166 | } 167 | 168 | func ReadInt8(r io.ReaderAt, addr int64, offsets ...int64) (int8, error) { 169 | num, err := readUint(r, addr, 1, offsets...) 170 | return int8(int64(num)), err 171 | } 172 | 173 | func ReadInt8Array(r io.ReaderAt, addr int64, offsets ...int64) ([]int8, error) { 174 | array, err := readUintArray(r, addr, 1, offsets...) 175 | array64 := make([]int8, len(array)) 176 | for i, v := range array { 177 | array64[i] = int8(int64(v)) 178 | } 179 | return array64, err 180 | } 181 | 182 | func ReadUint8(r io.ReaderAt, addr int64, offsets ...int64) (uint8, error) { 183 | num, err := readUint(r, addr, 1, offsets...) 184 | return uint8(num), err 185 | } 186 | 187 | func ReadUint8Array(r io.ReaderAt, addr int64, offsets ...int64) ([]uint8, error) { 188 | array, err := readUintArray(r, addr, 1, offsets...) 189 | array8 := make([]uint8, len(array)) 190 | for i, v := range array { 191 | array8[i] = uint8(v) 192 | } 193 | return array8, err 194 | } 195 | 196 | func ReadInt16(r io.ReaderAt, addr int64, offsets ...int64) (int16, error) { 197 | num, err := readUint(r, addr, 2, offsets...) 198 | return int16(int64(num)), err 199 | } 200 | 201 | func ReadInt16Array(r io.ReaderAt, addr int64, offsets ...int64) ([]int16, error) { 202 | array, err := readUintArray(r, addr, 2, offsets...) 203 | array16 := make([]int16, len(array)) 204 | for i, v := range array { 205 | array16[i] = int16(int64(v)) 206 | } 207 | return array16, err 208 | } 209 | 210 | func ReadUint16(r io.ReaderAt, addr int64, offsets ...int64) (uint16, error) { 211 | num, err := readUint(r, addr, 2, offsets...) 212 | return uint16(num), err 213 | } 214 | 215 | func ReadUint16Array(r io.ReaderAt, addr int64, offsets ...int64) ([]uint16, error) { 216 | array, err := readUintArray(r, addr, 2, offsets...) 217 | array16 := make([]uint16, len(array)) 218 | for i, v := range array { 219 | array16[i] = uint16(v) 220 | } 221 | return array16, err 222 | } 223 | 224 | func ReadInt32(r io.ReaderAt, addr int64, offsets ...int64) (int32, error) { 225 | num, err := readUint(r, addr, 4, offsets...) 226 | return int32(int64(num)), err 227 | } 228 | 229 | func ReadInt32Array(r io.ReaderAt, addr int64, offsets ...int64) ([]int32, error) { 230 | array, err := readUintArray(r, addr, 4, offsets...) 231 | array32 := make([]int32, len(array)) 232 | for i, v := range array { 233 | array32[i] = int32(int64(v)) 234 | } 235 | return array32, err 236 | } 237 | 238 | func ReadUint32(r io.ReaderAt, addr int64, offsets ...int64) (uint32, error) { 239 | num, err := readUint(r, addr, 4, offsets...) 240 | return uint32(num), err 241 | } 242 | 243 | func ReadUint32Array(r io.ReaderAt, addr int64, offsets ...int64) ([]uint32, error) { 244 | array, err := readUintArray(r, addr, 4, offsets...) 245 | array32 := make([]uint32, len(array)) 246 | for i, v := range array { 247 | array32[i] = uint32(v) 248 | } 249 | return array32, err 250 | } 251 | 252 | func ReadInt64(r io.ReaderAt, addr int64, offsets ...int64) (int64, error) { 253 | num, err := readUint(r, addr, 8, offsets...) 254 | return int64(num), err 255 | } 256 | 257 | func ReadInt64Array(r io.ReaderAt, addr int64, offsets ...int64) ([]int64, error) { 258 | array, err := readUintArray(r, addr, 8, offsets...) 259 | array64 := make([]int64, len(array)) 260 | for i, v := range array { 261 | array64[i] = int64(v) 262 | } 263 | return array64, err 264 | } 265 | 266 | func ReadUint64(r io.ReaderAt, addr int64, offsets ...int64) (uint64, error) { 267 | num, err := readUint(r, addr, 8, offsets...) 268 | return uint64(num), err 269 | } 270 | 271 | func ReadUint64Array(r io.ReaderAt, addr int64, offsets ...int64) ([]uint64, error) { 272 | array, err := readUintArray(r, addr, 8, offsets...) 273 | array64 := make([]uint64, len(array)) 274 | for i, v := range array { 275 | array64[i] = v 276 | } 277 | return array64, err 278 | } 279 | 280 | func ReadFloat32(r io.ReaderAt, addr int64, offsets ...int64) (float32, error) { 281 | num, err := readUint(r, addr, 4, offsets...) 282 | return math.Float32frombits(uint32(num)), err 283 | } 284 | 285 | func ReadFloat32Array(r io.ReaderAt, addr int64, offsets ...int64) ([]float32, error) { 286 | array, err := readUintArray(r, addr, 4, offsets...) 287 | array32 := make([]float32, len(array)) 288 | for i, v := range array { 289 | array32[i] = math.Float32frombits(uint32(v)) 290 | } 291 | return array32, err 292 | } 293 | 294 | func ReadFloat64(r io.ReaderAt, addr int64, offsets ...int64) (float64, error) { 295 | num, err := readUint(r, addr, 8, offsets...) 296 | return math.Float64frombits(num), err 297 | } 298 | 299 | func ReadFloat64Array(r io.ReaderAt, addr int64, offsets ...int64) ([]float64, error) { 300 | array, err := readUintArray(r, addr, 8, offsets...) 301 | array64 := make([]float64, len(array)) 302 | for i, v := range array { 303 | array64[i] = math.Float64frombits(v) 304 | } 305 | return array64, err 306 | } 307 | 308 | func ReadPtr(r io.ReaderAt, addr int64, offsets ...int64) (int64, error) { 309 | num, err := ReadUint32(r, addr, offsets...) 310 | return int64(num), err 311 | } 312 | -------------------------------------------------------------------------------- /mem/scan.go: -------------------------------------------------------------------------------- 1 | package mem 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "text/scanner" 12 | ) 13 | 14 | type pattern struct { 15 | Bytes []uint32 16 | Mask []uint32 17 | } 18 | 19 | func parsePattern(s string) (pattern, error) { 20 | var bytes, mask []byte 21 | for _, bytestr := range strings.Split(s, " ") { 22 | if bytestr == "??" { 23 | bytes = append(bytes, 0x00) 24 | mask = append(mask, 0x00) 25 | continue 26 | } 27 | b, err := strconv.ParseUint(bytestr, 16, 8) 28 | if err != nil { 29 | return pattern{}, err 30 | } 31 | bytes = append(bytes, byte(b)) 32 | mask = append(mask, 0xFF) 33 | } 34 | 35 | var p pattern 36 | for i := 0; i < len(bytes); i += 4 { 37 | var byt uint32 38 | for i, b := range bytes[i : i+4] { 39 | byt |= uint32(b) << (i * 8) 40 | } 41 | p.Bytes = append(p.Bytes, byt) 42 | var mas uint32 43 | for i, m := range mask[i : i+4] { 44 | mas |= uint32(m) << (i * 8) 45 | } 46 | p.Mask = append(p.Mask, mas) 47 | } 48 | 49 | return p, nil 50 | } 51 | 52 | func maskbyte(needle, mask uint32) (search byte, offset int) { 53 | for offset := 0; offset < 4; offset++ { 54 | maskByte := byte(mask >> (offset * 8) & 0xFF) 55 | needleByte := byte(needle >> (offset * 8) & 0xFF) 56 | if maskByte != 0x00 && needleByte != 0x00 { 57 | return needleByte, offset 58 | } 59 | } 60 | panic("empty mask (bad pattern)") 61 | } 62 | 63 | func search(buf []byte, pat pattern) (int, bool) { 64 | progress := 0 65 | maskbyte, byteoffset := maskbyte(pat.Bytes[0], pat.Mask[0]) 66 | for { 67 | i := bytes.IndexByte(buf, maskbyte) 68 | if i == -1 { 69 | return 0, false 70 | } 71 | begin := i - byteoffset 72 | end := begin + len(pat.Bytes)*4 73 | if begin < 0 || end > len(buf) { 74 | progress += i + 1 75 | buf = buf[i+1:] 76 | continue 77 | } 78 | success := true 79 | for j := range pat.Bytes { 80 | needle, mask := pat.Bytes[j], pat.Mask[j] 81 | slice := buf[begin+(j*4) : begin+(j*4)+4] 82 | haystack := binary.LittleEndian.Uint32(slice) 83 | if needle^haystack&mask != 0 { 84 | success = false 85 | break 86 | } 87 | } 88 | if success { 89 | return begin + progress, true 90 | } 91 | progress += i + 1 92 | buf = buf[i+1:] 93 | } 94 | } 95 | 96 | func find(p Process, pat pattern, reg Map) (int64, error) { 97 | const bufsize = 65536 98 | var buf [bufsize]byte 99 | for i := int64(0); i < reg.Size(); { 100 | ntoread := reg.Size() - i 101 | if ntoread >= bufsize { 102 | ntoread = bufsize - 1 103 | } 104 | n, err := p.ReadAt(buf[:ntoread], int64(reg.Start()+i)) 105 | if err != nil { 106 | return 0, err 107 | } 108 | if at, ok := search(buf[:n], pat); ok { 109 | return int64(reg.Start() + i + int64(at)), nil 110 | } 111 | diff := n - len(pat.Bytes)*8 112 | if diff <= 1 { 113 | diff = 1 114 | } 115 | i += int64(diff) 116 | } 117 | return 0, ErrPatternNotFound 118 | } 119 | 120 | func Scan(p Process, pattern string) (int64, error) { 121 | maps, err := p.Maps() 122 | if err != nil { 123 | return 0, err 124 | } 125 | 126 | pat, err := parsePattern(pattern) 127 | if err != nil { 128 | return 0, err 129 | } 130 | 131 | for _, reg := range maps { 132 | if i, err := find(p, pat, reg); err == nil { 133 | return i, nil 134 | } 135 | } 136 | 137 | return 0, fmt.Errorf("no memory matched the pattern: %s", pattern) 138 | } 139 | 140 | func ResolvePatterns(p Process, offsets interface{}) error { 141 | pval := reflect.ValueOf(offsets) 142 | val := reflect.Indirect(pval) 143 | valt := val.Type() 144 | if pval.Kind() != reflect.Ptr || val.Kind() != reflect.Struct { 145 | panic("offsets must be a pointer to a struct") 146 | } 147 | var anyErr error 148 | for i := 0; i < val.NumField(); i++ { 149 | field := valt.Field(i) 150 | sig, ok := field.Tag.Lookup("sig") 151 | if !ok { 152 | continue 153 | } 154 | offset, err := Scan(p, sig) 155 | if err != nil { 156 | anyErr = err 157 | continue 158 | } 159 | val.Field(i).Set(reflect.ValueOf(offset)) 160 | } 161 | 162 | return anyErr 163 | } 164 | 165 | type ReadError []error 166 | 167 | func (r ReadError) Error() string { 168 | var strs []string 169 | for _, err := range r { 170 | strs = append(strs, err.Error()) 171 | } 172 | return strings.Join(strs, ", ") 173 | } 174 | 175 | func Read(r io.ReaderAt, addresses interface{}, p interface{}) error { 176 | beginDebug() 177 | defer endDebug() 178 | 179 | addrpval := reflect.ValueOf(addresses) 180 | addrval := addrpval.Elem() 181 | 182 | if addrval.Kind() != reflect.Struct { 183 | panic("addresses must be a pointer to a struct") 184 | } 185 | 186 | pval := reflect.ValueOf(p) 187 | val := pval.Elem() 188 | valt := val.Type() 189 | 190 | if val.Kind() != reflect.Struct { 191 | panic("p must be a pointer to a struct") 192 | } 193 | 194 | var errs ReadError 195 | for i := 0; i < val.NumField(); i++ { 196 | field := val.Field(i) 197 | fieldt := valt.Field(i) 198 | tag, ok := fieldt.Tag.Lookup("mem") 199 | if !ok { 200 | continue 201 | } 202 | evalFunc := func(addr int64) (int64, error) { 203 | return ReadPtr(r, addr, 0) 204 | } 205 | var varFunc func(name string) (int64, error) 206 | varFunc = func(name string) (int64, error) { 207 | field := addrval.FieldByName(name) 208 | if field.IsValid() { 209 | addr := field.Interface().(int64) 210 | log("%s: 0x%x\n", name, addr) 211 | return addr, nil 212 | } 213 | method := addrval.Addr().MethodByName(name) 214 | if method.IsValid() { 215 | ret := method.Call([]reflect.Value{}) 216 | exprStr := ret[0].Interface().(string) 217 | expr, err := parseMem(exprStr, varFunc) 218 | if err != nil { 219 | log("Failed to parse variable %s: %v\n", 220 | name, err) 221 | return 0, err 222 | } 223 | log("%s(): %#v\n", name, exprStr) 224 | val, err := expr.eval(evalFunc) 225 | if err == nil { 226 | log("%s() = 0x%x\n", 227 | name, val) 228 | } else { 229 | log("Failed to resolve variable %s: %v\n", 230 | name, err) 231 | } 232 | return val, err 233 | } 234 | return 0, fmt.Errorf("undefined variable %s", name) 235 | } 236 | log("%v: %#v\n", fieldt.Name, tag) 237 | dbg := pushDebug() 238 | expr, err := parseMem(tag, varFunc) 239 | if err != nil { 240 | return fmt.Errorf( 241 | "failed to parse mem tag for %s.%s: %w", 242 | valt.Name(), fieldt.Name, err) 243 | } 244 | addr, err := expr.eval(evalFunc) 245 | if err != nil { 246 | return fmt.Errorf("failed to read %s.%s: %w", 247 | valt.Name(), fieldt.Name, err) 248 | } 249 | if err := readPrimitive(r, field.Addr().Interface(), 250 | addr, 0); err != nil { 251 | err = fmt.Errorf("failed to read %s.%s: %w", 252 | valt.Name(), fieldt.Name, err) 253 | errs = append(errs, err) 254 | } 255 | popDebug(dbg) 256 | } 257 | 258 | if len(errs) != 0 { 259 | return errs 260 | } 261 | 262 | return nil 263 | } 264 | 265 | func readPrimitive(r io.ReaderAt, p interface{}, 266 | addr int64, offsets ...int64) error { 267 | var err error 268 | switch p := p.(type) { 269 | case *int8: 270 | *p, err = ReadInt8(r, addr, offsets...) 271 | case *int16: 272 | *p, err = ReadInt16(r, addr, offsets...) 273 | case *int32: 274 | *p, err = ReadInt32(r, addr, offsets...) 275 | case *int64: 276 | *p, err = ReadInt64(r, addr, offsets...) 277 | case *uint8: 278 | *p, err = ReadUint8(r, addr, offsets...) 279 | case *uint16: 280 | *p, err = ReadUint16(r, addr, offsets...) 281 | case *uint32: 282 | *p, err = ReadUint32(r, addr, offsets...) 283 | case *uint64: 284 | *p, err = ReadUint64(r, addr, offsets...) 285 | case *float32: 286 | *p, err = ReadFloat32(r, addr, offsets...) 287 | case *float64: 288 | *p, err = ReadFloat64(r, addr, offsets...) 289 | case *[]int8: 290 | *p, err = ReadInt8Array(r, addr, offsets...) 291 | case *[]int16: 292 | *p, err = ReadInt16Array(r, addr, offsets...) 293 | case *[]int32: 294 | *p, err = ReadInt32Array(r, addr, offsets...) 295 | case *[]int64: 296 | *p, err = ReadInt64Array(r, addr, offsets...) 297 | case *[]uint8: 298 | *p, err = ReadUint8Array(r, addr, offsets...) 299 | case *[]uint16: 300 | *p, err = ReadUint16Array(r, addr, offsets...) 301 | case *[]uint32: 302 | *p, err = ReadUint32Array(r, addr, offsets...) 303 | case *[]uint64: 304 | *p, err = ReadUint64Array(r, addr, offsets...) 305 | case *[]float32: 306 | *p, err = ReadFloat32Array(r, addr, offsets...) 307 | case *[]float64: 308 | *p, err = ReadFloat64Array(r, addr, offsets...) 309 | case *string: 310 | *p, err = ReadString(r, addr, offsets...) 311 | default: 312 | err = fmt.Errorf("unknown type %T", p) 313 | } 314 | return err 315 | 316 | } 317 | 318 | type mem struct { 319 | Child *mem 320 | Offset int64 321 | } 322 | 323 | func (m *mem) String() string { 324 | var b strings.Builder 325 | if m.Child != nil { 326 | fmt.Fprintf(&b, "[%s]", m.Child) 327 | } 328 | if m.Child != nil && m.Offset != 0 { 329 | b.WriteString(" + ") 330 | } 331 | if m.Offset != 0 { 332 | fmt.Fprintf(&b, "0x%x", m.Offset) 333 | } 334 | return b.String() 335 | } 336 | 337 | func (m *mem) eval(f func(p int64) (int64, error)) (int64, error) { 338 | if m.Child != nil { 339 | childAddr, err := m.Child.eval(f) 340 | if err != nil { 341 | return 0, err 342 | } 343 | 344 | dereferenced, err := f(childAddr) 345 | if err != nil { 346 | return 0, err 347 | } 348 | 349 | if m.Offset == 0 { 350 | log("[0x%x] = 0x%x\n", childAddr, dereferenced) 351 | } else { 352 | log("[0x%x] + 0x%x = 0x%x\n", childAddr, m.Offset, 353 | dereferenced+m.Offset) 354 | } 355 | 356 | return dereferenced + m.Offset, nil 357 | } else { 358 | return m.Offset, nil 359 | } 360 | } 361 | 362 | func parseMem(tag string, 363 | varFunc func(name string) (int64, error)) (*mem, error) { 364 | var s scanner.Scanner 365 | s.Init(strings.NewReader(tag)) 366 | s.Mode = scanner.ScanIdents | scanner.ScanInts 367 | return parseMemExpr(&s, varFunc, false) 368 | } 369 | 370 | func parseMemExpr(s *scanner.Scanner, 371 | varFunc func(name string) (int64, error), inBrackets bool) (*mem, error) { 372 | expr := &mem{} 373 | switch tok := s.Scan(); tok { 374 | case '[': 375 | inner, err := parseMemExpr(s, varFunc, true) 376 | if err != nil { 377 | return nil, err 378 | } 379 | expr.Child = inner 380 | case scanner.Ident: 381 | name := s.TokenText() 382 | var err error 383 | expr.Offset, err = varFunc(name) 384 | if err != nil { 385 | return nil, err 386 | } 387 | case scanner.Int: 388 | var err error 389 | expr.Offset, err = strconv.ParseInt(s.TokenText(), 0, 64) 390 | if err != nil { 391 | return nil, err 392 | } 393 | default: 394 | return nil, fmt.Errorf("unexpected token %d (%s)", 395 | tok, s.TokenText()) 396 | } 397 | 398 | switch tok := s.Scan(); tok { 399 | case '+', '-': 400 | rest, err := parseMemExpr(s, varFunc, inBrackets) 401 | if err != nil { 402 | return nil, err 403 | } 404 | switch tok { 405 | case '+': 406 | expr.Offset += rest.Offset 407 | case '-': 408 | expr.Offset -= rest.Offset 409 | } 410 | return expr, nil 411 | case scanner.EOF, ']': 412 | if tok == ']' && !inBrackets { 413 | return nil, fmt.Errorf("unexpected token %d (%s)", 414 | tok, s.TokenText()) 415 | } 416 | return expr, nil 417 | default: 418 | return nil, fmt.Errorf("unexpected token %d (%s)", 419 | tok, s.TokenText()) 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /mem/windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package mem 4 | 5 | import ( 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | "syscall" 10 | "unsafe" 11 | 12 | windows "github.com/elastic/go-windows" 13 | xsyscall "golang.org/x/sys/windows" 14 | ) 15 | 16 | var ( 17 | modkernel32 = xsyscall.NewLazySystemDLL("kernel32.dll") 18 | user32 = xsyscall.NewLazySystemDLL("user32.dll") 19 | procEnumWindows = user32.NewProc("EnumWindows") 20 | procGetWindowTextW = user32.NewProc("GetWindowTextW") 21 | getWindowThreadProcessID = user32.NewProc("GetWindowThreadProcessId") 22 | procVirtualQueryEx = modkernel32.NewProc("VirtualQueryEx") 23 | queryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW") 24 | ) 25 | 26 | func enumWindows(enumFunc uintptr, lparam uintptr) (err error) { 27 | r1, _, e1 := syscall.Syscall(procEnumWindows.Addr(), 2, uintptr(enumFunc), uintptr(lparam), 0) 28 | if r1 == 0 { 29 | if e1 != 0 { 30 | err = error(e1) 31 | } else { 32 | err = syscall.EINVAL 33 | } 34 | } 35 | return 36 | } 37 | 38 | func getWindowText(hwnd syscall.Handle, str *uint16, maxCount int32) (len int32, err error) { 39 | r0, _, e1 := syscall.Syscall(procGetWindowTextW.Addr(), 3, uintptr(hwnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount)) 40 | len = int32(r0) 41 | if len == 0 { 42 | if e1 != 0 { 43 | err = error(e1) 44 | } else { 45 | err = syscall.EINVAL 46 | } 47 | } 48 | return 49 | } 50 | 51 | func GetWindowThreadProcessID(hwnd syscall.Handle) int32 { 52 | var processID int32 53 | getWindowThreadProcessID.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&processID))) 54 | return processID 55 | } 56 | 57 | func FindWindow(title string) (syscall.Handle, error) { 58 | var hwnd syscall.Handle 59 | cb := syscall.NewCallback(func(h syscall.Handle, p uintptr) uintptr { 60 | b := make([]uint16, 200) 61 | _, err := getWindowText(h, &b[0], int32(len(b))) 62 | if err != nil { 63 | // ignore the error 64 | return 1 // continue enumeration 65 | } 66 | if strings.Contains(syscall.UTF16ToString(b), title) { 67 | // note the window 68 | hwnd = h 69 | return 0 // stop enumeration 70 | } 71 | return 1 // continue enumeration 72 | }) 73 | enumWindows(cb, 0) 74 | if hwnd == 0 { 75 | return 0, fmt.Errorf("No window with title '%s' found", title) 76 | } 77 | return hwnd, nil 78 | } 79 | 80 | func virtualQueryEx(handle syscall.Handle, off int64) (region, error) { 81 | var reg region 82 | r1, _, e1 := syscall.Syscall6( 83 | procVirtualQueryEx.Addr(), 84 | 4, 85 | uintptr(handle), 86 | uintptr(off), 87 | uintptr(unsafe.Pointer(®)), 88 | uintptr(unsafe.Sizeof(reg)), 89 | 0, 0, 90 | ) 91 | if r1 == 0 { 92 | if e1 != 0 { 93 | return region{}, e1 94 | } else { 95 | return region{}, syscall.EINVAL 96 | } 97 | } 98 | return reg, nil 99 | } 100 | 101 | func queryFullProcessImageName(hProcess syscall.Handle) (string, error) { 102 | var buf [syscall.MAX_PATH]uint16 103 | n := uint32(len(buf)) 104 | r1, _, e1 := queryFullProcessImageNameW.Call( 105 | uintptr(hProcess), 106 | uintptr(0), 107 | uintptr(unsafe.Pointer(&buf[0])), 108 | uintptr(unsafe.Pointer(&n))) 109 | if r1 == 0 { 110 | if e1 != nil { 111 | return "", e1 112 | } else { 113 | return "", syscall.EINVAL 114 | } 115 | } 116 | return syscall.UTF16ToString(buf[:n]), nil 117 | 118 | } 119 | 120 | func FindProcess(re *regexp.Regexp, blacklistedTitles ...string) ([]Process, error) { 121 | var procs []Process 122 | pids, err := windows.EnumProcesses() 123 | if err != nil { 124 | return nil, err 125 | } 126 | for _, pid := range pids { 127 | handle, err := syscall.OpenProcess( 128 | syscall.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, 129 | false, pid) 130 | if err != nil { 131 | continue 132 | } 133 | name, err := windows.GetProcessImageFileName(handle) 134 | if err != nil { 135 | syscall.CloseHandle(handle) 136 | continue 137 | } 138 | if re.MatchString(name) { 139 | var bannedHandles []syscall.Handle 140 | for _, title := range blacklistedTitles { 141 | h, _ := FindWindow(title) 142 | if h != 0 { 143 | bannedHandles = append(bannedHandles, h) 144 | } 145 | } 146 | isBanned := false 147 | for _, bHandle := range bannedHandles { 148 | if int32(pid) == GetWindowThreadProcessID(bHandle) { 149 | isBanned = true 150 | break 151 | } 152 | } 153 | if !isBanned { 154 | procs = append(procs, process{pid, handle}) 155 | } 156 | } 157 | } 158 | if len(procs) < 1 { 159 | return nil, ErrNoProcess 160 | } 161 | return procs, nil 162 | } 163 | 164 | type process struct { 165 | pid uint32 166 | h syscall.Handle 167 | } 168 | 169 | func (p process) HandleFromTitle() (string, error) { 170 | return queryFullProcessImageName(p.h) 171 | } 172 | 173 | func (p process) ExecutablePath() (string, error) { 174 | return queryFullProcessImageName(p.h) 175 | } 176 | 177 | func (p process) Close() error { 178 | return syscall.CloseHandle(p.h) 179 | } 180 | 181 | func (p process) Pid() int { 182 | return int(p.pid) 183 | } 184 | 185 | func (p process) ReadAt(b []byte, off int64) (n int, err error) { 186 | un, err := windows.ReadProcessMemory(p.h, uintptr(off), b) 187 | logRead(b, int(un), off, err) 188 | return int(un), err 189 | } 190 | 191 | func (p process) Maps() ([]Map, error) { 192 | lastAddr := int64(0) 193 | var maps []Map 194 | for { 195 | reg, err := virtualQueryEx(p.h, lastAddr) 196 | if err != nil { 197 | if lastAddr == 0 { 198 | return nil, err 199 | } 200 | break 201 | } 202 | maps = append(maps, reg) 203 | lastAddr = reg.Start() + reg.Size() 204 | } 205 | return maps, nil 206 | } 207 | 208 | type region struct { 209 | baseAddress uintptr 210 | allocationBase uintptr 211 | allocationProtect int32 212 | regionSize int 213 | state int32 214 | protect int32 215 | type_ int32 216 | } 217 | 218 | func (r region) Start() int64 { 219 | return int64(r.baseAddress) 220 | } 221 | 222 | func (r region) Size() int64 { 223 | return int64(r.regionSize) 224 | } 225 | -------------------------------------------------------------------------------- /memory/functions.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "math" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | "time" 12 | 13 | "github.com/spf13/cast" 14 | 15 | "github.com/k0kubun/pp" 16 | "github.com/l3lackShark/gosumemory/mem" 17 | ) 18 | 19 | func modsResolver(xor uint32) string { 20 | return Mods(xor).String() 21 | } 22 | 23 | // UpdateTime Intervall between value updates 24 | var UpdateTime int 25 | 26 | // UnderWine? 27 | var UnderWine bool 28 | 29 | // MemCycle test 30 | var MemCycle bool 31 | var isTournamentMode bool 32 | var tourneyProcs []mem.Process 33 | var tourneyErr error 34 | 35 | // Were we in the Result Screen? 36 | var dirtyResults bool = false 37 | 38 | // var proc, procerr = kiwi.GetProcessByFileName("osu!.exe") 39 | var leaderStart int32 40 | 41 | // SongsFolderPath is full path to osu! Songs. Gets set automatically on Windows (through memory) 42 | var SongsFolderPath string 43 | 44 | var allProcs []mem.Process 45 | var process mem.Process 46 | var procerr error 47 | var tempRetries int32 48 | 49 | // Init the whole thing and get osu! memory values to start working with it. 50 | func Init() { 51 | if UnderWine == true || runtime.GOOS != "windows" { //Arrays start at 0xC in Linux for some reason, has to be wine specific 52 | leaderStart = 0xC 53 | } else { 54 | leaderStart = 0x8 55 | } 56 | 57 | allProcs, procerr = mem.FindProcess(osuProcessRegex, "osu!lazer", "osu!framework") 58 | for { 59 | start := time.Now() 60 | if procerr != nil { 61 | DynamicAddresses.IsReady = false 62 | for procerr != nil { 63 | allProcs, procerr = mem.FindProcess(osuProcessRegex, "osu!lazer", "osu!framework") 64 | log.Println("It seems that we lost the process, retrying! ERROR:", procerr) 65 | time.Sleep(1 * time.Second) 66 | } 67 | err := initBase() 68 | for err != nil { 69 | log.Println("Failure mid getting offsets, retrying! ERROR:", err) 70 | err = initBase() 71 | time.Sleep(1 * time.Second) 72 | } 73 | } 74 | if DynamicAddresses.IsReady == false { 75 | err := initBase() 76 | for err != nil { 77 | log.Println("Failure mid getting offsets, retrying! ERROR:", err) 78 | err = initBase() 79 | time.Sleep(1 * time.Second) 80 | } 81 | } else { 82 | err := mem.Read(process, 83 | &patterns.PreSongSelectAddresses, 84 | &menuData.PreSongSelectData) 85 | if err != nil { 86 | DynamicAddresses.IsReady = false 87 | log.Println("It appears that we lost the process, retrying! ERROR:", err) 88 | continue 89 | } 90 | MenuData.OsuStatus = menuData.Status 91 | 92 | mem.Read(process, &patterns, &alwaysData) 93 | MenuData.ChatChecker = alwaysData.ChatStatus 94 | MenuData.Bm.Time.PlayTime = alwaysData.PlayTime 95 | SettingsData.Folders.Skin = alwaysData.SkinFolder 96 | 97 | SettingsData.ShowInterface = cast.ToBool(int(alwaysData.ShowInterface)) 98 | switch menuData.Status { 99 | case 0: 100 | err = bmUpdateData() 101 | if err != nil { 102 | pp.Println(err) 103 | } 104 | mem.Read(process, &patterns, &mainMenuData) 105 | MenuData.MainMenuValues.BassDensity = calculateBassDensity(mainMenuData.AudioVelocityBase, &process) 106 | case 2: 107 | if MenuData.Bm.Time.PlayTime < 150 || menuData.Path == "" { //To catch up with the F2-->Enter 108 | err := bmUpdateData() 109 | if err != nil { 110 | pp.Println(err) 111 | } 112 | } 113 | if gameplayData.Retries > tempRetries || dirtyResults { 114 | tempRetries = gameplayData.Retries 115 | GameplayData = GameplayValues{} 116 | gameplayData = gameplayD{} 117 | dirtyResults = false 118 | } 119 | getGamplayData() 120 | case 1: 121 | err = bmUpdateData() 122 | if err != nil { 123 | pp.Println(err) 124 | } 125 | case 7: 126 | err = bmUpdateData() 127 | if err != nil { 128 | pp.Println(err) 129 | } 130 | mem.Read(process, &patterns, &resultsScreenData) 131 | 132 | if resultsScreenData.ModsXor1 == 0 { //not initialized yet 133 | for i := 0; i < 10; i++ { 134 | mem.Read(process, &patterns, &resultsScreenData) 135 | if resultsScreenData.ModsXor1 != 0 { 136 | break 137 | } 138 | time.Sleep(50 * time.Millisecond) 139 | } 140 | } 141 | ResultsScreenData.H300 = resultsScreenData.Hit300 142 | ResultsScreenData.H100 = resultsScreenData.Hit100 143 | ResultsScreenData.H50 = resultsScreenData.Hit50 144 | ResultsScreenData.H0 = resultsScreenData.HitMiss 145 | ResultsScreenData.MaxCombo = resultsScreenData.MaxCombo 146 | ResultsScreenData.Name = resultsScreenData.PlayerName 147 | ResultsScreenData.Score = resultsScreenData.Score 148 | ResultsScreenData.HGeki = resultsScreenData.HitGeki 149 | ResultsScreenData.HKatu = resultsScreenData.HitKatu 150 | 151 | ResultsScreenData.Mods.AppliedMods = resultsScreenData.ModsXor1 ^ resultsScreenData.ModsXor2 152 | if ResultsScreenData.Mods.AppliedMods == 0 { 153 | ResultsScreenData.Mods.PpMods = "NM" 154 | } else { 155 | ResultsScreenData.Mods.PpMods = Mods(resultsScreenData.ModsXor1 ^ resultsScreenData.ModsXor2).String() 156 | } 157 | if !dirtyResults { 158 | dirtyResults = true 159 | } 160 | default: 161 | tempRetries = -1 162 | GameplayData = GameplayValues{} 163 | gameplayData = gameplayD{} 164 | err = bmUpdateData() 165 | if err != nil { 166 | pp.Println(err) 167 | } 168 | 169 | } 170 | } 171 | if isTournamentMode { 172 | 173 | if err := getTourneyIPC(); err != nil { 174 | DynamicAddresses.IsReady = false 175 | log.Println("It appears that we lost the precess, retrying", err) 176 | continue 177 | } 178 | } 179 | if menuData.Status != 7 { 180 | ResultsScreenData = ResultsScreenValues{} 181 | } 182 | elapsed := time.Since(start) 183 | if MemCycle { 184 | log.Printf("Cycle took %s", elapsed) 185 | } 186 | time.Sleep(time.Duration(UpdateTime-int(elapsed.Milliseconds())) * time.Millisecond) 187 | 188 | } 189 | 190 | } 191 | 192 | var tempBeatmapString string 193 | var tempGameMode int32 = 5 194 | 195 | func bmUpdateData() error { 196 | mem.Read(process, &patterns, &menuData) 197 | 198 | bmString := menuData.Path 199 | if (strings.HasSuffix(bmString, ".osu") && tempBeatmapString != bmString) || (strings.HasSuffix(bmString, ".osu") && tempGameMode != menuData.MenuGameMode) { //On map/mode change 200 | for i := 0; i < 50; i++ { 201 | if menuData.BackgroundFilename != "" { 202 | break 203 | } 204 | time.Sleep(25 * time.Millisecond) 205 | mem.Read(process, &patterns, &menuData) 206 | } 207 | tempGameMode = menuData.MenuGameMode 208 | tempBeatmapString = bmString 209 | MenuData.Bm.BeatmapID = menuData.MapID 210 | MenuData.Bm.BeatmapSetID = menuData.SetID 211 | MenuData.Bm.Stats.MemoryAR = menuData.AR 212 | MenuData.Bm.Stats.MemoryCS = menuData.CS 213 | MenuData.Bm.Stats.MemoryHP = menuData.HP 214 | MenuData.Bm.Stats.MemoryOD = menuData.OD 215 | MenuData.Bm.Stats.TotalHitObjects = menuData.ObjectCount 216 | MenuData.Bm.Metadata.Artist = menuData.Artist 217 | MenuData.Bm.Metadata.ArtistOriginal = menuData.ArtistOriginal 218 | MenuData.Bm.Metadata.Title = menuData.Title 219 | MenuData.Bm.Metadata.TitleOriginal = menuData.TitleOriginal 220 | MenuData.Bm.Metadata.Mapper = menuData.Creator 221 | MenuData.Bm.Metadata.Version = menuData.Difficulty 222 | MenuData.GameMode = menuData.MenuGameMode 223 | MenuData.Bm.RandkedStatus = menuData.RankedStatus 224 | MenuData.Bm.BeatmapMD5 = menuData.MD5 225 | MenuData.Bm.Path = path{ 226 | AudioPath: menuData.AudioFilename, 227 | BGPath: menuData.BackgroundFilename, 228 | BeatmapOsuFileString: menuData.Path, 229 | BeatmapFolderString: menuData.Folder, 230 | FullMP3Path: filepath.Join(SongsFolderPath, menuData.Folder, menuData.AudioFilename), 231 | FullDotOsu: filepath.Join(SongsFolderPath, menuData.Folder, bmString), 232 | InnerBGPath: filepath.Join(menuData.Folder, menuData.BackgroundFilename), 233 | } 234 | } 235 | if menuData.Status != 7 && menuData.Status != 14 { 236 | if alwaysData.MenuMods == 0 { 237 | MenuData.Mods.PpMods = "NM" 238 | MenuData.Mods.AppliedMods = int32(alwaysData.MenuMods) 239 | } else { 240 | MenuData.Mods.AppliedMods = int32(alwaysData.MenuMods) 241 | MenuData.Mods.PpMods = Mods(alwaysData.MenuMods).String() 242 | } 243 | } 244 | 245 | return nil 246 | } 247 | func getGamplayData() { 248 | err := mem.Read(process, &patterns, &gameplayData) 249 | if err != nil && !strings.Contains(err.Error(), "LeaderBoard") && !strings.Contains(err.Error(), "KeyOverlay") { //those could be disabled 250 | return //struct not initialized yet 251 | } 252 | //GameplayData.BitwiseKeypress = gameplayData.BitwiseKeypress 253 | GameplayData.Combo.Current = gameplayData.Combo 254 | GameplayData.Combo.Max = gameplayData.MaxCombo 255 | GameplayData.GameMode = gameplayData.Mode 256 | GameplayData.Hits.H100 = gameplayData.Hit100 257 | GameplayData.Hits.HKatu = gameplayData.HitKatu 258 | GameplayData.Hits.H300 = gameplayData.Hit300 259 | GameplayData.Hits.HGeki = gameplayData.HitGeki 260 | GameplayData.Hits.H50 = gameplayData.Hit50 261 | GameplayData.Hits.H0 = gameplayData.HitMiss 262 | if GameplayData.Combo.Temp > GameplayData.Combo.Max { 263 | GameplayData.Combo.Temp = 0 264 | } 265 | if GameplayData.Combo.Current < GameplayData.Combo.Temp && GameplayData.Hits.H0Temp == GameplayData.Hits.H0 { 266 | GameplayData.Hits.HSB++ 267 | } 268 | GameplayData.Hits.H0Temp = GameplayData.Hits.H0 269 | GameplayData.Combo.Temp = GameplayData.Combo.Current 270 | GameplayData.Accuracy = cast.ToFloat64(fmt.Sprintf("%.2f", gameplayData.Accuracy)) 271 | GameplayData.Hp.Normal = gameplayData.PlayerHP 272 | GameplayData.Hp.Smooth = gameplayData.PlayerHPSmooth 273 | GameplayData.Name = gameplayData.PlayerName 274 | MenuData.Mods.AppliedMods = gameplayData.ModsXor1 ^ gameplayData.ModsXor2 275 | if MenuData.Mods.AppliedMods == 0 { 276 | MenuData.Mods.PpMods = "NM" 277 | } else { 278 | MenuData.Mods.PpMods = Mods(gameplayData.ModsXor1 ^ gameplayData.ModsXor2).String() 279 | } 280 | // if strings.Contains(MenuData.Mods.PpMods, "V2") { 281 | GameplayData.Score = gameplayData.ScoreV2 282 | // } else { 283 | // GameplayData.Score = gameplayData.Score 284 | // } 285 | if GameplayData.Combo.Max > 0 { 286 | GameplayData.Hits.HitErrorArray = gameplayData.HitErrors 287 | baseUR, _ := calculateUR(GameplayData.Hits.HitErrorArray) 288 | if strings.Contains(MenuData.Mods.PpMods, "DT") || strings.Contains(MenuData.Mods.PpMods, "NC") { 289 | GameplayData.Hits.UnstableRate = baseUR / 1.5 290 | } else if strings.Contains(MenuData.Mods.PpMods, "HT") { 291 | GameplayData.Hits.UnstableRate = baseUR * 1.33 292 | } else { 293 | GameplayData.Hits.UnstableRate = baseUR 294 | } 295 | } 296 | getLeaderboard() 297 | getKeyOveraly() 298 | } 299 | 300 | func getLeaderboard() { 301 | var board leaderboard 302 | if gameplayData.LeaderBoard == 0 { 303 | board.DoesLeaderBoardExists = false 304 | GameplayData.Leaderboard = board 305 | return 306 | } 307 | board.DoesLeaderBoardExists = true 308 | ourPlayerStruct, _ := mem.ReadUint32(process, int64(gameplayData.LeaderBoard)+0x10, 0) 309 | board.OurPlayer, board.IsLeaderBoardVisible = readLeaderPlayerStruct(int64(ourPlayerStruct)) 310 | board.OurPlayer.Mods = MenuData.Mods.PpMods //ourplayer mods is sometimes delayed so better default to PlayContainer Here 311 | playersArray, _ := mem.ReadUint32(process, int64(gameplayData.LeaderBoard)+0x4) 312 | amOfSlots, _ := mem.ReadInt32(process, int64(playersArray+0xC)) 313 | if amOfSlots < 1 || amOfSlots > 64 { 314 | return 315 | } 316 | items, _ := mem.ReadInt32(process, int64(playersArray+0x4)) 317 | board.Slots = make([]leaderPlayer, amOfSlots) 318 | for i, j := leaderStart, 0; j < int(amOfSlots); i, j = i+0x4, j+1 { 319 | slot, _ := mem.ReadUint32(process, int64(items), int64(i)) 320 | board.Slots[j], _ = readLeaderPlayerStruct(int64(slot)) 321 | } 322 | GameplayData.Leaderboard = board 323 | } 324 | 325 | type ManiaStars struct { 326 | NoMod float64 327 | DT float64 328 | HT float64 329 | } 330 | 331 | func ReadManiaStars() (ManiaStars, error) { 332 | addresses := struct{ Base int64 }{int64(menuData.StarRatingStruct)} //Beatmap + 0x88 333 | var entries struct { 334 | Data uint32 `mem:"[Base + 0x14] + 0x8"` 335 | } 336 | err := mem.Read(process, &addresses, &entries) 337 | if err != nil || entries.Data == 0 { 338 | pp.Println("[MEMORY] Could not find star rating for this map or converts not supported yet") 339 | return ManiaStars{}, nil 340 | } 341 | starRating := struct{ Base int64 }{int64(entries.Data)} 342 | var stars struct { 343 | NoMod float64 `mem:"Base + 0x18"` 344 | DT float64 `mem:"Base + 0x30"` 345 | HT float64 `mem:"Base + 0x48"` 346 | } 347 | err = mem.Read(process, &starRating, &stars) 348 | if err != nil { 349 | return ManiaStars{}, errors.New("[MEMORY] Empty star rating (internal)") 350 | } 351 | return ManiaStars{stars.NoMod, stars.DT, stars.HT}, nil 352 | } 353 | 354 | func readLeaderPlayerStruct(base int64) (leaderPlayer, bool) { 355 | addresses := struct{ Base int64 }{base} 356 | var player struct { 357 | Name string `mem:"[Base + 0x8]"` 358 | Score int32 `mem:"Base + 0x30"` 359 | Combo int16 `mem:"[Base + 0x20] + 0x94"` 360 | MaxCombo int16 `mem:"[Base + 0x20] + 0x68"` 361 | ModsXor1 uint32 `mem:"[[Base + 0x20] + 0x1C] + 0x8"` 362 | ModsXor2 uint32 `mem:"[[Base + 0x20] + 0x1C] + 0xC"` 363 | H300 int16 `mem:"[Base + 0x20] + 0x8A"` 364 | H100 int16 `mem:"[Base + 0x20] + 0x88"` 365 | H50 int16 `mem:"[Base + 0x20] + 0x8C"` 366 | H0 int16 `mem:"[Base + 0x20] + 0x92"` 367 | Team int32 `mem:"Base + 0x40"` 368 | Position int32 `mem:"Base + 0x2C"` 369 | IsPassing int8 `mem:"Base + 0x4B"` 370 | IsLeaderboardVisible int8 `mem:"[Base + 0x24] + 0x20"` 371 | } 372 | mem.Read(process, &addresses, &player) 373 | mods := modsResolver(player.ModsXor1 ^ player.ModsXor2) 374 | if mods == "" { 375 | mods = "NM" 376 | } 377 | return leaderPlayer{ 378 | Name: player.Name, 379 | Score: player.Score, 380 | Combo: player.Combo, 381 | MaxCombo: player.MaxCombo, 382 | Mods: mods, 383 | H300: player.H300, 384 | H100: player.H100, 385 | H50: player.H50, 386 | H0: player.H0, 387 | Team: player.Team, 388 | Position: player.Position, 389 | IsPassing: player.IsPassing, 390 | }, cast.ToBool(int(player.IsLeaderboardVisible)) 391 | } 392 | 393 | func calculateUR(HitErrorArray []int32) (float64, error) { 394 | if len(HitErrorArray) < 1 { 395 | return 0, errors.New("empty hit error array") 396 | } 397 | var totalAll float32 //double 398 | for _, hit := range HitErrorArray { 399 | totalAll += float32(hit) 400 | } 401 | var average = totalAll / float32(len(HitErrorArray)) 402 | var variance float64 = 0 403 | for _, hit := range HitErrorArray { 404 | variance += math.Pow(float64(hit)-float64(average), 2) 405 | } 406 | variance = variance / float64(len(HitErrorArray)) 407 | return math.Sqrt(variance) * 10, nil 408 | 409 | } 410 | 411 | var currentAudioVelocity float64 412 | 413 | func calculateBassDensity(base uint32, proc *mem.Process) float64 { 414 | var bass float32 415 | for i, j := leaderStart, 0; j < 40; i, j = i+0x4, j+1 { 416 | value, err := mem.ReadFloat32(*proc, int64(base), int64(i)) 417 | if err != nil { 418 | return 0.5 419 | } 420 | bass += 2 * value * (40 - float32(j)) / 40 421 | } 422 | if math.IsNaN(currentAudioVelocity) || math.IsNaN(float64(bass)) { 423 | currentAudioVelocity = 0 424 | return 0.5 425 | } 426 | currentAudioVelocity = math.Max(currentAudioVelocity, math.Min(float64(bass)*1.5, 6)) 427 | currentAudioVelocity *= 0.95 428 | return (1 + currentAudioVelocity) * 0.5 429 | 430 | } 431 | 432 | func getKeyOveraly() { 433 | addresses := struct{ Base int64 }{int64(gameplayData.KeyOverlayArrayAddr)} 434 | var entries struct { 435 | K1Pressed int8 `mem:"[Base + 0x8] + 0x1C"` //Pressed usually works with <20 update rate. It's recommended to create a buffer and predict presses by count to save CPU overhead 436 | K1Count int32 `mem:"[Base + 0x8] + 0x14"` 437 | K2Pressed int8 `mem:"[Base + 0xC] + 0x1C"` 438 | K2Count int32 `mem:"[Base + 0xC] + 0x14"` 439 | M1Pressed int8 `mem:"[Base + 0x10] + 0x1C"` 440 | M1Count int32 `mem:"[Base + 0x10] + 0x14"` 441 | M2Pressed int8 `mem:"[Base + 0x14] + 0x1C"` 442 | M2Count int32 `mem:"[Base + 0x14] + 0x14"` 443 | } 444 | err := mem.Read(process, &addresses, &entries) 445 | if err != nil { 446 | return 447 | } 448 | 449 | var out keyOverlay 450 | 451 | out.K1.IsPressed = cast.ToBool(int(entries.K1Pressed)) 452 | out.K1.Count = entries.K1Count 453 | out.K2.IsPressed = cast.ToBool(int(entries.K2Pressed)) 454 | out.K2.Count = entries.K2Count 455 | out.M1.IsPressed = cast.ToBool(int(entries.M1Pressed)) 456 | out.M1.Count = entries.M1Count 457 | out.M2.IsPressed = cast.ToBool(int(entries.M2Pressed)) 458 | out.M2.Count = entries.M2Count 459 | GameplayData.KeyOverlay = out //needs complete rewrite in 1.4.0 460 | } 461 | -------------------------------------------------------------------------------- /memory/init.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "path/filepath" 8 | "regexp" 9 | "runtime" 10 | "strings" 11 | "time" 12 | 13 | "github.com/l3lackShark/gosumemory/config" 14 | "github.com/l3lackShark/gosumemory/injctr" 15 | "github.com/l3lackShark/gosumemory/mem" 16 | "github.com/spf13/cast" 17 | ) 18 | 19 | var osuProcessRegex = regexp.MustCompile(`.*osu!\.exe.*`) 20 | var patterns staticAddresses 21 | var tourneyPatterns []staticAddresses 22 | var tourneySpecificPatterns []tourneyStaticAddresses 23 | var tourneyMenuData []menuD 24 | var tourneyManagerData tourneyD 25 | var tourneyGameplayData []gameplayD 26 | var tourneyAlwaysData []allTimesD 27 | 28 | var menuData menuD 29 | var songsFolderData songsFolderD 30 | var gameplayData gameplayD 31 | var alwaysData allTimesD 32 | var mainMenuData mainMenuD 33 | var resultsScreenData resultsScreenD 34 | 35 | func resolveSongsFolder() (string, error) { 36 | var err error 37 | osuExecutablePath, err := process.ExecutablePath() 38 | if err != nil { 39 | return "", err 40 | } 41 | if !strings.Contains(osuExecutablePath, `:\`) { 42 | log.Println("Automatic executable path finder has failed. Please try again or manually specify it. (see --help) GOT: ", osuExecutablePath) 43 | time.Sleep(5 * time.Second) 44 | return "", errors.New("osu! executable was not found") 45 | } 46 | rootFolder := strings.TrimSuffix(osuExecutablePath, "osu!.exe") 47 | songsFolder := filepath.Join(rootFolder, "Songs") 48 | if songsFolderData.SongsFolder == "Songs" || songsFolderData.SongsFolder == "CompatibilityContext" { //dirty hack to fix old stable offset 49 | return songsFolder, nil 50 | } 51 | return songsFolderData.SongsFolder, nil 52 | } 53 | 54 | func initBase() error { 55 | var err error 56 | isTournamentMode = false 57 | allProcs, err = mem.FindProcess(osuProcessRegex, "osu!lazer", "osu!framework") 58 | if err != nil { 59 | return err 60 | } 61 | process = allProcs[0] 62 | 63 | err = mem.ResolvePatterns(process, &patterns.PreSongSelectAddresses) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | err = mem.Read(process, 69 | &patterns.PreSongSelectAddresses, 70 | &menuData.PreSongSelectData) 71 | if err != nil { 72 | return err 73 | } 74 | fmt.Println("[MEMORY] Got osu!status addr...") 75 | 76 | if runtime.GOOS == "windows" && SongsFolderPath == "auto" { 77 | err = mem.Read(process, 78 | &patterns.PreSongSelectAddresses, 79 | &songsFolderData) 80 | if err != nil { 81 | return err 82 | } 83 | SongsFolderPath, err = resolveSongsFolder() 84 | if err != nil { 85 | log.Fatalln(err) 86 | } 87 | } 88 | fmt.Println("[MEMORY] Songs folder:", SongsFolderPath) 89 | pepath, err := process.ExecutablePath() 90 | if err != nil { 91 | panic(err) 92 | } 93 | SettingsData.Folders.Game = filepath.Dir(pepath) 94 | 95 | if menuData.PreSongSelectData.Status == 22 || len(allProcs) > 1 { 96 | fmt.Println("[MEMORY] Operating in tournament mode!") 97 | tourneyProcs, tourneyErr = resolveTourneyClients(allProcs) 98 | if tourneyErr != nil { 99 | return err 100 | } 101 | isTournamentMode = true 102 | tourneyPatterns = make([]staticAddresses, len(tourneyProcs)) 103 | tourneySpecificPatterns = make([]tourneyStaticAddresses, len(tourneyProcs)) 104 | TourneyData.IPCClients = make([]ipcClient, len(tourneyProcs)) 105 | tourneyMenuData = make([]menuD, len(tourneyProcs)) 106 | tourneyGameplayData = make([]gameplayD, len(tourneyProcs)) 107 | tourneyAlwaysData = make([]allTimesD, len(tourneyProcs)) 108 | for i, proc := range tourneyProcs { 109 | err = mem.ResolvePatterns(proc, &tourneyPatterns[i].PreSongSelectAddresses) 110 | if err != nil { 111 | return err 112 | } 113 | err = mem.Read(proc, 114 | &tourneyPatterns[i].PreSongSelectAddresses, 115 | &tourneyMenuData[i].PreSongSelectData) 116 | if err != nil { 117 | return err 118 | } 119 | fmt.Println(fmt.Sprintf("[MEMORY] Got osu!status addr for client #%d...", i)) 120 | fmt.Println(fmt.Sprintf("[MEMORY] Resolving patterns for client #%d...", i)) 121 | err = mem.ResolvePatterns(proc, &tourneyPatterns[i]) 122 | if err != nil { 123 | return err 124 | } 125 | err = mem.ResolvePatterns(proc, &tourneySpecificPatterns[i]) 126 | if err != nil { 127 | return err 128 | } 129 | } 130 | 131 | } 132 | 133 | fmt.Println("[MEMORY] Resolving patterns...") 134 | err = mem.ResolvePatterns(process, &patterns) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | SettingsData.Folders.Songs = SongsFolderPath 140 | 141 | fmt.Println("[MEMORY] Got all patterns...") 142 | fmt.Println("WARNING: Mania pp calcualtion is experimental and only works if you choose mania gamemode in the SongSelect!") 143 | fmt.Println(fmt.Sprintf("Initialization complete, you can now visit http://%s or add it as a browser source in OBS", config.Config["serverip"])) 144 | DynamicAddresses.IsReady = true 145 | if cast.ToBool(config.Config["enabled"]) { 146 | err = injctr.Injct(process.Pid()) 147 | if err != nil { 148 | log.Printf("Failed to inject into osu's process, in game overlay will be unavailabe. %e\n", err) 149 | } 150 | } else { 151 | fmt.Println("[MEMORY] In-Game overlay is disabled, but could be enabled in config.ini!") 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /memory/mods.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Mods represents zero or more mods of an osu! score. 8 | type Mods int 9 | 10 | // Mods constants. 11 | // Names are taken from official osu! documentation. 12 | const ( 13 | ModsNoFail Mods = 1 << iota 14 | ModsEasy 15 | ModsTouchDevice 16 | ModsHidden 17 | ModsHardRock 18 | ModsSuddenDeath 19 | ModsDoubleTime 20 | ModsRelax 21 | ModsHalfTime 22 | ModsNightcore 23 | ModsFlashlight 24 | ModsAutoplay 25 | ModsSpunOut 26 | ModsRelax2 // Autopilot 27 | ModsPerfect 28 | ModsKey4 29 | ModsKey5 30 | ModsKey6 31 | ModsKey7 32 | ModsKey8 33 | ModsFadeIn 34 | ModsRandom 35 | ModsCinema 36 | ModsTargetPractice 37 | ModsKey9 38 | ModsKeyCoop 39 | ModsKey1 40 | ModsKey3 41 | ModsKey2 42 | ModsScoreV2 43 | ModsMirror 44 | 45 | ModsKeyMod Mods = ModsKey1 | ModsKey2 | ModsKey3 | ModsKey4 | ModsKey5 | ModsKey6 | ModsKey7 | ModsKey8 | ModsKey9 46 | ModsFreeModAllowed Mods = ModsNoFail | ModsEasy | ModsHidden | ModsHardRock | ModsSuddenDeath | ModsFlashlight | ModsFadeIn | ModsRelax | ModsSpunOut | ModsKeyMod 47 | ModsScoreIncrease Mods = ModsHidden | ModsHardRock | ModsDoubleTime | ModsFlashlight | ModsFadeIn 48 | 49 | // modsSpeedChanging Mods = ModsDT | ModsHT | ModsNC 50 | // modsMapChanging Mods = ModsHR | ModsEZ | modsSpeedChanging 51 | ) 52 | 53 | // Convenience aliases. 54 | const ( 55 | NF Mods = ModsNoFail 56 | EZ Mods = ModsEasy 57 | TD Mods = ModsTouchDevice 58 | HD Mods = ModsHidden 59 | HR Mods = ModsHardRock 60 | SD Mods = ModsSuddenDeath 61 | DT Mods = ModsDoubleTime 62 | RX Mods = ModsRelax 63 | HT Mods = ModsHalfTime 64 | NC Mods = ModsNightcore 65 | FL Mods = ModsFlashlight 66 | SO Mods = ModsSpunOut 67 | PF Mods = ModsPerfect 68 | 69 | MR Mods = ModsMirror 70 | V2 Mods = ModsScoreV2 71 | AT Mods = ModsAutoplay 72 | AP Mods = ModsRelax2 73 | FI Mods = ModsFadeIn 74 | RD Mods = ModsRandom 75 | CN Mods = ModsCinema 76 | TP Mods = ModsTargetPractice 77 | CO Mods = ModsKeyCoop 78 | 79 | K1 Mods = ModsKey1 80 | K2 Mods = ModsKey2 81 | K3 Mods = ModsKey3 82 | K4 Mods = ModsKey4 83 | K5 Mods = ModsKey5 84 | K6 Mods = ModsKey6 85 | K7 Mods = ModsKey7 86 | K8 Mods = ModsKey8 87 | K9 Mods = ModsKey9 88 | ) 89 | 90 | // This is a slice instead of a map because order matters 91 | var modStrings = []struct { 92 | mod Mods 93 | str string 94 | }{ 95 | {NF, "NF"}, 96 | {EZ, "EZ"}, 97 | {TD, "TD"}, 98 | {HD, "HD"}, 99 | {HR, "HR"}, 100 | {SD, "SD"}, 101 | {DT, "DT"}, 102 | {RX, "RX"}, 103 | {HT, "HT"}, 104 | {NC, "NC"}, 105 | {FL, "FL"}, 106 | {AT, "AT"}, 107 | {SO, "SO"}, 108 | {AP, "AP"}, 109 | {PF, "PF"}, 110 | {K4, "4K"}, 111 | {K5, "5K"}, 112 | {K6, "6K"}, 113 | {K7, "7K"}, 114 | {K8, "8K"}, 115 | {FI, "FI"}, 116 | {RD, "RD"}, 117 | {CN, "CN"}, 118 | {TP, "TP"}, 119 | {K9, "9K"}, 120 | {CO, "CO"}, 121 | {K1, "1K"}, 122 | {K3, "3K"}, 123 | {K2, "2K"}, 124 | {V2, "V2"}, 125 | {MR, "MR"}, 126 | } 127 | 128 | func (m Mods) String() string { 129 | if m == 0 { 130 | return "" 131 | } 132 | 133 | var s strings.Builder 134 | 135 | for _, v := range modStrings { 136 | if m&v.mod > 0 { 137 | s.WriteString(v.str) 138 | } 139 | } 140 | 141 | out := s.String() 142 | if strings.Contains(out, "NC") { // ex. DTNCATCN (4196928) 143 | out = strings.Replace(out, "DT", "", 1) 144 | } 145 | if strings.Contains(out, "CN") { 146 | out = strings.Replace(out, "AT", "", 1) 147 | } 148 | 149 | return out 150 | } 151 | -------------------------------------------------------------------------------- /memory/read.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | type PreSongSelectAddresses struct { 4 | Status int64 `sig:"48 83 F8 04 73 1E"` 5 | SettingsClass int64 `sig:"83 E0 20 85 C0 7E 2F"` 6 | } 7 | 8 | type songsFolderD struct { 9 | SongsFolder string `mem:"[[Settings + 0xB8] + 0x4]"` 10 | } 11 | 12 | type PreSongSelectData struct { 13 | Status uint32 `mem:"[Status - 0x4]"` 14 | } 15 | 16 | type staticAddresses struct { 17 | PreSongSelectAddresses 18 | Base int64 `sig:"F8 01 74 04 83 65"` 19 | MenuMods int64 `sig:"C8 FF ?? ?? ?? ?? ?? 81 0D ?? ?? ?? ?? 00 08 00 00"` 20 | PlayTime int64 `sig:"5E 5F 5D C3 A1 ?? ?? ?? ?? 89 ?? 04"` 21 | ChatChecker int64 `sig:"0A D7 23 3C 00 00 ?? 01"` 22 | SkinData int64 `sig:"75 21 8B 1D"` 23 | Rulesets int64 `sig:"7D 15 A1 ?? ?? ?? ?? 85 C0"` 24 | ChatArea int64 `sig:"33 47 9D FF 5B 7F FF FF"` 25 | } 26 | 27 | type tourneyStaticAddresses struct { 28 | UserInfo int64 `sig:"52 30 8B C8 E8 ?? ?? ?? ?? 8B C8 8D"` 29 | } 30 | 31 | type tourneyD struct { 32 | IPCState int32 `mem:"Ruleset + 0x54"` 33 | LeftStars int32 `mem:"[Ruleset + 0x1C] + 0x2C"` 34 | RightStars int32 `mem:"[Ruleset + 0x20] + 0x2C"` 35 | BO int32 `mem:"[Ruleset + 0x20] + 0x30"` 36 | StarsVisible int8 `mem:"[Ruleset + 0x20] + 0x38"` 37 | ScoreVisible int8 `mem:"[Ruleset + 0x20] + 0x39"` 38 | TeamOneName string `mem:"[[[Ruleset + 0x1C] + 0x20] + 0x144]"` 39 | TeamTwoName string `mem:"[[[Ruleset + 0x20] + 0x20] + 0x144]"` 40 | TeamOneScore int32 `mem:"[Ruleset + 0x1C] + 0x28"` 41 | TeamTwoScore int32 `mem:"[Ruleset + 0x20] + 0x28"` 42 | IPCBaseAddr uint32 `mem:"[[Ruleset + 0x34] + 0x4] + 0x4"` 43 | } 44 | 45 | type resultsScreenD struct { 46 | PlayerName string `mem:"[[Ruleset + 0x38] + 0x28]"` 47 | ModsXor1 int32 `mem:"[[Ruleset + 0x38] + 0x1C] + 0xC"` 48 | ModsXor2 int32 `mem:"[[Ruleset + 0x38] + 0x1C] + 0x8"` 49 | Mode int32 `mem:"[Ruleset + 0x38] + 0x64"` 50 | MaxCombo int16 `mem:"[Ruleset + 0x38] + 0x68"` 51 | Score int32 `mem:"[Ruleset + 0x38] + 0x78"` 52 | Hit100 int16 `mem:"[Ruleset + 0x38] + 0x88"` 53 | Hit300 int16 `mem:"[Ruleset + 0x38] + 0x8A"` 54 | Hit50 int16 `mem:"[Ruleset + 0x38] + 0x8C"` 55 | HitGeki int16 `mem:"[Ruleset + 0x38] + 0x8E"` 56 | HitKatu int16 `mem:"[Ruleset + 0x38] + 0x90"` 57 | HitMiss int16 `mem:"[Ruleset + 0x38] + 0x92"` 58 | Accuracy float64 `mem:"[Ruleset + 0x48] + 0xC"` 59 | } 60 | 61 | func (staticAddresses) Ruleset() string { 62 | return "[[Rulesets - 0xB] + 0x4]" 63 | } 64 | 65 | func (staticAddresses) Beatmap() string { 66 | return "[Base - 0xC]" 67 | } 68 | 69 | func (PreSongSelectAddresses) Settings() string { 70 | return "[SettingsClass + 0x8]" 71 | } 72 | 73 | func (staticAddresses) PlayContainer() string { 74 | return "[[[[PlayContainerBase + 0x7] + 0x4] + 0xC4] + 0x4]" 75 | } 76 | 77 | func (staticAddresses) Leaderboard() string { 78 | return "[[[LeaderboardBase+0x1] + 0x4] + 0x7C] + 0x24" 79 | } 80 | 81 | type menuD struct { 82 | PreSongSelectData 83 | //SearchText string `mem:"[Ruleset + 0xCC]"` 84 | //GroupingType int32 `mem:"Ruleset + 0x104"` 85 | MenuGameMode int32 `mem:"[Base - 0x33]"` 86 | Plays int32 `mem:"[Base - 0x33] + 0xC"` 87 | Artist string `mem:"[[Beatmap] + 0x18]"` 88 | ArtistOriginal string `mem:"[[Beatmap] + 0x1C]"` 89 | Title string `mem:"[[Beatmap] + 0x24]"` 90 | TitleOriginal string `mem:"[[Beatmap] + 0x28]"` 91 | AR float32 `mem:"[Beatmap] + 0x2C"` 92 | CS float32 `mem:"[Beatmap] + 0x30"` 93 | HP float32 `mem:"[Beatmap] + 0x34"` 94 | OD float32 `mem:"[Beatmap] + 0x38"` 95 | StarRatingStruct uint32 `mem:"[Beatmap] + 0x8C"` 96 | AudioFilename string `mem:"[[Beatmap] + 0x64]"` 97 | BackgroundFilename string `mem:"[[Beatmap] + 0x68]"` 98 | Folder string `mem:"[[Beatmap] + 0x78]"` 99 | Creator string `mem:"[[Beatmap] + 0x7C]"` 100 | Name string `mem:"[[Beatmap] + 0x80]"` 101 | Path string `mem:"[[Beatmap] + 0x90]"` 102 | Difficulty string `mem:"[[Beatmap] + 0xAC]"` 103 | MapID int32 `mem:"[Beatmap] + 0xC8"` 104 | SetID int32 `mem:"[Beatmap] + 0xCC"` 105 | RankedStatus int32 `mem:"[Beatmap] + 0x12C"` // unknown, unsubmitted, pending/wip/graveyard, unused, ranked, approved, qualified 106 | MD5 string `mem:"[[Beatmap] + 0x6C]"` 107 | ObjectCount int32 `mem:"[Beatmap] + 0xFC"` 108 | //BeatmapMode int32 `mem:"[Beatmap] + 0x118"` 109 | //Tags string `mem:"[[Beatmap] + 0x20]"` 110 | //Length int32 `mem:"[Beatmap] + 0x12C"` 111 | //AudioLeadIn int32 `mem:"[Beatmap] + 0xC0"` 112 | //DrainTime int32 `mem:"[Beatmap] + 0xE8"` 113 | //DrainTime2 int32 `mem:"[Beatmap] + 0xEC"` 114 | //ScoreMenu int32 `mem:"[Beatmap] + 0xFC"` // Local, global, mod, friend, country 115 | //PreviewTime int32 `mem:"[Beatmap] + 0x118"` 116 | } 117 | 118 | type mainMenuD struct { 119 | AudioVelocityBase uint32 `mem:"[Ruleset + 0x44] + 0x10"` 120 | } 121 | 122 | type allTimesD struct { 123 | PlayTime int32 `mem:"[PlayTime + 0x5]"` 124 | MenuMods uint32 `mem:"[MenuMods + 0x9]"` 125 | ChatStatus int8 `mem:"ChatChecker - 0x20"` 126 | SkinFolder string `mem:"[[[SkinData + 4] + 0] + 68]"` 127 | ShowInterface int8 `mem:"[Settings + 0x4] + 0xC"` 128 | } 129 | type gameplayD struct { 130 | Retries int32 `mem:"[Base - 0x33] + 0x8"` 131 | PlayerName string `mem:"[[[Ruleset + 0x68] + 0x38] + 0x28]"` 132 | ModsXor1 int32 `mem:"[[[Ruleset + 0x68] + 0x38] + 0x1C] + 0xC"` 133 | ModsXor2 int32 `mem:"[[[Ruleset + 0x68] + 0x38] + 0x1C] + 0x8"` 134 | HitErrors []int32 `mem:"[[[Ruleset + 0x68] + 0x38] + 0x38]"` 135 | Mode int32 `mem:"[[Ruleset + 0x68] + 0x38] + 0x64"` 136 | MaxCombo int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x68"` 137 | ScoreV2 int32 `mem:"Ruleset + 0x100"` 138 | Hit100 int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x88"` 139 | Hit300 int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x8A"` 140 | Hit50 int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x8C"` 141 | HitGeki int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x8E"` 142 | HitKatu int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x90"` 143 | HitMiss int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x92"` 144 | Combo int16 `mem:"[[Ruleset + 0x68] + 0x38] + 0x94"` 145 | PlayerHPSmooth float64 `mem:"[[Ruleset + 0x68] + 0x40] + 0x14"` 146 | PlayerHP float64 `mem:"[[Ruleset + 0x68] + 0x40] + 0x1C"` 147 | Accuracy float64 `mem:"[[Ruleset + 0x68] + 0x48] + 0xC"` 148 | LeaderBoard uint32 `mem:"[Ruleset + 0x7C] + 0x24"` 149 | KeyOverlayArrayAddr uint32 `mem:"[[Ruleset + 0xB0] + 0x10] + 0x4"` //has to be at the end due to mem not liking dead pointers, TODO: Fix this mem-side 150 | // Score int32 `mem:"[[Ruleset + 0x68] + 0x38] + 0x78"` 151 | } 152 | -------------------------------------------------------------------------------- /memory/tournament_linux.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package memory 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/l3lackShark/gosumemory/mem" 9 | ) 10 | 11 | func resolveTourneyClients(procs []mem.Process) ([]mem.Process, error) { 12 | return nil, errors.New("Not implemented!") 13 | } 14 | 15 | func getTourneyGameplayData(proc mem.Process, iterator int) { 16 | return 17 | } 18 | 19 | func getTourneyIPC() error { 20 | return errors.New("Not implemented!") 21 | } 22 | -------------------------------------------------------------------------------- /memory/tournament_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package memory 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/l3lackShark/gosumemory/mem" 18 | "github.com/spf13/cast" 19 | ) 20 | 21 | func resolveTourneyClients(procs []mem.Process) ([]mem.Process, error) { 22 | //read tournament.cfg to check how many clients we are expecting 23 | osuExecutablePath, err := procs[0].ExecutablePath() 24 | if err != nil { 25 | return nil, err 26 | } 27 | if !strings.Contains(osuExecutablePath, `:\`) { 28 | log.Println("Automatic executable path finder has failed. The program will now ext. GOT: ", osuExecutablePath) 29 | return nil, errors.New("osu! executable was not found") 30 | } 31 | cfgFile, err := os.Open(filepath.Join(filepath.Dir(osuExecutablePath), "tournament.cfg")) 32 | if err != nil { 33 | return nil, err 34 | } 35 | defer cfgFile.Close() 36 | 37 | var totalClients int 38 | scanner := bufio.NewScanner(cfgFile) 39 | 40 | for scanner.Scan() { 41 | if strings.Contains(scanner.Text(), "TeamSize") { 42 | teamSize, err := strconv.Atoi(scanner.Text()[len(scanner.Text())-1:]) 43 | if err != nil { 44 | return nil, err 45 | } 46 | totalClients = teamSize * 2 47 | fmt.Println("Total expected amount of tournament clients:", totalClients) 48 | break 49 | } 50 | } 51 | 52 | if totalClients == 0 { 53 | return nil, errors.New("total clients is 0") 54 | } 55 | fmt.Println("[TOURNAMENT] Awaiting all clients to load...") 56 | var tourneyClients []mem.Process 57 | for len(procs) != totalClients+1 { 58 | procs, err = mem.FindProcess(osuProcessRegex) 59 | if err != nil { 60 | return nil, err 61 | } 62 | } 63 | for i := range procs { 64 | if i > len(procs)-2 { 65 | break 66 | } 67 | client, err := mem.FindWindow(fmt.Sprintf("Tournament Client %d", i)) 68 | counter := 0 69 | for err != nil { 70 | if counter >= 30 { 71 | fmt.Println("Time's up! exiting tournament mode, failed after 30 attempts") 72 | return nil, errors.New("Tournament client timeout") 73 | } 74 | fmt.Println(fmt.Sprintf("[TOURNAMENT] %s, waiting for it...", err)) 75 | time.Sleep(1 * time.Second) 76 | counter++ 77 | client, err = mem.FindWindow(fmt.Sprintf("Tournament Client %d", i)) 78 | } 79 | for _, proc := range procs { 80 | if int32(proc.Pid()) == mem.GetWindowThreadProcessID(client) { 81 | tourneyClients = append(tourneyClients, proc) 82 | break 83 | } 84 | } 85 | } 86 | return tourneyClients, nil 87 | } 88 | 89 | func getTourneyGameplayData(proc mem.Process, iterator int) { 90 | err := mem.Read(proc, &tourneyPatterns[iterator], &tourneyGameplayData[iterator]) 91 | if err != nil && !strings.Contains(err.Error(), "LeaderBoard") && !strings.Contains(err.Error(), "KeyOverlay") { //TODO: fix this mem-side 92 | return //struct not initialized yet 93 | } 94 | TourneyData.IPCClients[iterator].Gameplay.Combo.Current = tourneyGameplayData[iterator].Combo 95 | TourneyData.IPCClients[iterator].Gameplay.Combo.Max = tourneyGameplayData[iterator].MaxCombo 96 | TourneyData.IPCClients[iterator].Gameplay.GameMode = tourneyGameplayData[iterator].Mode 97 | TourneyData.IPCClients[iterator].Gameplay.Score = tourneyGameplayData[iterator].ScoreV2 98 | TourneyData.IPCClients[iterator].Gameplay.Hits.H100 = tourneyGameplayData[iterator].Hit100 99 | TourneyData.IPCClients[iterator].Gameplay.Hits.HKatu = tourneyGameplayData[iterator].HitKatu 100 | TourneyData.IPCClients[iterator].Gameplay.Hits.H300 = tourneyGameplayData[iterator].Hit300 101 | TourneyData.IPCClients[iterator].Gameplay.Hits.HGeki = tourneyGameplayData[iterator].HitGeki 102 | TourneyData.IPCClients[iterator].Gameplay.Hits.H50 = tourneyGameplayData[iterator].Hit50 103 | TourneyData.IPCClients[iterator].Gameplay.Hits.H0 = tourneyGameplayData[iterator].HitMiss 104 | if TourneyData.IPCClients[iterator].Gameplay.Combo.Temp > TourneyData.IPCClients[iterator].Gameplay.Combo.Max { 105 | TourneyData.IPCClients[iterator].Gameplay.Combo.Temp = 0 106 | } 107 | if TourneyData.IPCClients[iterator].Gameplay.Combo.Current < TourneyData.IPCClients[iterator].Gameplay.Combo.Temp && TourneyData.IPCClients[iterator].Gameplay.Hits.H0Temp == TourneyData.IPCClients[iterator].Gameplay.Hits.H0 { 108 | TourneyData.IPCClients[iterator].Gameplay.Hits.HSB++ 109 | } 110 | TourneyData.IPCClients[iterator].Gameplay.Hits.H0Temp = TourneyData.IPCClients[iterator].Gameplay.Hits.H0 111 | TourneyData.IPCClients[iterator].Gameplay.Combo.Temp = TourneyData.IPCClients[iterator].Gameplay.Combo.Current 112 | TourneyData.IPCClients[iterator].Gameplay.Accuracy = tourneyGameplayData[iterator].Accuracy 113 | TourneyData.IPCClients[iterator].Gameplay.Hp.Normal = tourneyGameplayData[iterator].PlayerHP 114 | TourneyData.IPCClients[iterator].Gameplay.Hp.Smooth = tourneyGameplayData[iterator].PlayerHPSmooth 115 | TourneyData.IPCClients[iterator].Gameplay.Name = tourneyGameplayData[iterator].PlayerName 116 | TourneyData.IPCClients[iterator].Gameplay.Mods.AppliedMods = int32(tourneyGameplayData[iterator].ModsXor1 ^ tourneyGameplayData[iterator].ModsXor2) 117 | if TourneyData.IPCClients[iterator].Gameplay.Mods.AppliedMods == 0 { 118 | TourneyData.IPCClients[iterator].Gameplay.Mods.PpMods = "NM" 119 | } else { 120 | TourneyData.IPCClients[iterator].Gameplay.Mods.PpMods = Mods(tourneyGameplayData[iterator].ModsXor1 ^ tourneyGameplayData[iterator].ModsXor2).String() 121 | } 122 | if TourneyData.IPCClients[iterator].Gameplay.Combo.Max > 0 { 123 | TourneyData.IPCClients[iterator].Gameplay.Hits.HitErrorArray = tourneyGameplayData[iterator].HitErrors 124 | baseUR, _ := calculateUR(TourneyData.IPCClients[iterator].Gameplay.Hits.HitErrorArray) 125 | if strings.Contains(TourneyData.IPCClients[iterator].Gameplay.Mods.PpMods, "DT") || strings.Contains(TourneyData.IPCClients[iterator].Gameplay.Mods.PpMods, "NC") { 126 | TourneyData.IPCClients[iterator].Gameplay.Hits.UnstableRate = baseUR / 1.5 127 | } else if strings.Contains(TourneyData.IPCClients[iterator].Gameplay.Mods.PpMods, "HT") { 128 | TourneyData.IPCClients[iterator].Gameplay.Hits.UnstableRate = baseUR * 1.33 129 | } else { 130 | TourneyData.IPCClients[iterator].Gameplay.Hits.UnstableRate = baseUR 131 | } 132 | } 133 | } 134 | 135 | func readTourneyIPCStruct(base int64) (int32, int32) { 136 | addresses := struct { 137 | Base int64 138 | }{base} 139 | var data struct { 140 | SpectatingID int32 `mem:"Base + 0x14"` 141 | Score int32 `mem:"Base + 0x18"` 142 | } 143 | 144 | mem.Read(process, &addresses, &data) 145 | 146 | return data.SpectatingID, data.Score 147 | } 148 | 149 | func readSpectatingUser(user int64, proc *mem.Process) (ipcSpec, error) { 150 | userAddr := struct { 151 | UserInfo int64 152 | }{user} 153 | var userData struct { 154 | Accuracy float64 `mem:"[[UserInfo - 0x5]] + 0x4"` 155 | RankedScore int64 `mem:"[[UserInfo - 0x5]] + 0xC"` 156 | PlayCount int32 `mem:"[[UserInfo - 0x5]] + 0x7C"` 157 | GlobalRank int32 `mem:"[[UserInfo - 0x5]] + 0x84"` 158 | PP int32 `mem:"[[UserInfo - 0x5]] + 0x9C"` 159 | Name string `mem:"[[[UserInfo - 0x5]] + 0x30]"` 160 | Country string `mem:"[[[UserInfo - 0x5]] + 0x2C]"` 161 | UserID int32 `mem:"[[UserInfo - 0x5]] + 0x70"` 162 | } 163 | err := mem.Read(*proc, &userAddr, &userData) 164 | if err != nil { 165 | return ipcSpec{}, errors.New("[TOURNAMENT] Could not read userData") 166 | } 167 | 168 | return ipcSpec{ 169 | Accuracy: userData.Accuracy, 170 | GlobalPP: userData.PP, 171 | GlobalRank: userData.GlobalRank, 172 | Name: userData.Name, 173 | Country: userData.Country, 174 | ID: userData.UserID, 175 | PlayCount: userData.PlayCount, 176 | RankedScore: userData.RankedScore, 177 | }, nil 178 | } 179 | 180 | func getTourneyIPC() error { 181 | err := mem.Read(process, 182 | &patterns, 183 | &tourneyManagerData) 184 | if err != nil { 185 | return err 186 | } 187 | chatClass := patterns.ChatArea - 0x44 188 | TourneyData.Manager.Chat, err = readChatData(&chatClass) 189 | if err != nil { 190 | log.Println(err) 191 | DynamicAddresses.IsReady = false 192 | } 193 | TourneyData.Manager.BO = tourneyManagerData.BO 194 | TourneyData.Manager.IPCState = tourneyManagerData.IPCState 195 | TourneyData.Manager.Bools.ScoreVisible = cast.ToBool(int(tourneyManagerData.ScoreVisible)) 196 | TourneyData.Manager.Bools.StarsVisible = cast.ToBool(int(tourneyManagerData.StarsVisible)) 197 | TourneyData.Manager.Stars.Left = tourneyManagerData.LeftStars 198 | TourneyData.Manager.Stars.Right = tourneyManagerData.RightStars 199 | TourneyData.Manager.Name.Left = tourneyManagerData.TeamOneName 200 | TourneyData.Manager.Name.Right = tourneyManagerData.TeamTwoName 201 | TourneyData.Manager.Gameplay.Score.Left = tourneyManagerData.TeamOneScore 202 | TourneyData.Manager.Gameplay.Score.Right = tourneyManagerData.TeamTwoScore 203 | if TourneyData.Manager.IPCState != 3 && TourneyData.Manager.IPCState != 4 { //Playing, Ranking 204 | TourneyData.Manager.Gameplay = tmGameplay{} 205 | for i := range tourneyGameplayData { 206 | TourneyData.IPCClients[i].Gameplay = tourneyGameplay{} 207 | } 208 | } 209 | 210 | for i, j := leaderStart, 0; j < len(tourneyProcs); i, j = i+0x4, j+1 { 211 | slot, _ := mem.ReadUint32(process, int64(tourneyManagerData.IPCBaseAddr), int64(i)) 212 | 213 | TourneyData.IPCClients[j].ID, TourneyData.IPCClients[j].Gameplay.Score = readTourneyIPCStruct(int64(slot)) 214 | 215 | } 216 | 217 | for i, proc := range tourneyProcs { 218 | err := mem.Read(proc, 219 | &tourneyPatterns[i].PreSongSelectAddresses, 220 | &tourneyMenuData[i].PreSongSelectData) 221 | if err != nil { 222 | DynamicAddresses.IsReady = false 223 | log.Println("It appears that we lost the process, retrying", err) 224 | continue 225 | } 226 | if i == 0 { 227 | if tourneyMenuData[0].PreSongSelectData.Status == 0 { 228 | mem.Read(proc, &tourneyPatterns[0], &mainMenuData) 229 | MenuData.MainMenuValues.BassDensity = calculateBassDensity(mainMenuData.AudioVelocityBase, &proc) 230 | } 231 | } 232 | switch tourneyMenuData[i].PreSongSelectData.Status { 233 | case 2: 234 | getTourneyGameplayData(proc, i) 235 | } 236 | if TourneyData.IPCClients[i].ID > 0 { 237 | totalClients := len(TourneyData.IPCClients) 238 | if i < totalClients/2 { 239 | TourneyData.IPCClients[i].Team = "left" 240 | } else { 241 | TourneyData.IPCClients[i].Team = "right" 242 | } 243 | TourneyData.IPCClients[i].Spectating, _ = readSpectatingUser(tourneySpecificPatterns[i].UserInfo, &proc) 244 | } else { 245 | TourneyData.IPCClients[i] = ipcClient{} 246 | } 247 | } 248 | return nil 249 | } 250 | 251 | func readChatData(base *int64) (result []tourneyMessage, err error) { 252 | addresses := struct{ Base int64 }{*base} 253 | var data struct { 254 | Tabs uint32 `mem:"[Base + 0x1C] + 0x4"` 255 | } 256 | err = mem.Read(process, &addresses, &data) 257 | if err != nil { 258 | return nil, errors.New("[TOURNEY CHAT] Failed reading the main struct") 259 | } 260 | length, err := mem.ReadInt32(process, int64(data.Tabs), 4) 261 | for i, j := leaderStart, 0; j < int(length); i, j = i+0x4, j+1 { 262 | slot, _ := mem.ReadUint32(process, int64(data.Tabs), int64(i)) 263 | if slot == 0 { 264 | continue 265 | } 266 | addrs := struct{ Base int64 }{int64(slot)} 267 | var chatData struct { 268 | ChatTag string `mem:"[[Base + 0xC] + 0x4]"` 269 | MessagesAddr uint32 `mem:"[[Base + 0xC] + 0x10] + 0x4"` 270 | } 271 | err := mem.Read(process, &addrs, &chatData) 272 | if err != nil || chatData.ChatTag != "#multiplayer" { 273 | continue 274 | } 275 | msgLength, err := mem.ReadInt32(process, int64(chatData.MessagesAddr), 4) 276 | var messages []tourneyMessage 277 | for n, k := leaderStart, 0; k < int(msgLength); n, k = n+0x4, k+1 { 278 | msgSlot, err := mem.ReadUint32(process, int64(chatData.MessagesAddr), int64(n)) 279 | if err != nil { 280 | return nil, errors.New("[TOURNEY CHAT] Internal error") 281 | } 282 | msgAddrs := struct{ Base int64 }{int64(msgSlot)} 283 | var chatContent struct { 284 | TimeName string `mem:"[Base + 0x8]"` 285 | Content string `mem:"[Base + 0x4]"` 286 | } 287 | err = mem.Read(process, &msgAddrs, &chatContent) 288 | if chatContent.Content == "" || strings.HasPrefix(chatContent.Content, "!mp") { 289 | continue 290 | } 291 | spl := strings.SplitAfterN(chatContent.TimeName, " ", 2) 292 | if len(spl) < 2 { 293 | return nil, errors.New("[TOURNEY CHAT] Internal error, could not split") 294 | } 295 | var msg tourneyMessage 296 | msg.Time = strings.TrimSpace(spl[0]) 297 | msg.Name = strings.TrimSuffix(spl[1], ":") 298 | msg.MessageBody = chatContent.Content 299 | for _, client := range TourneyData.IPCClients { 300 | if client.Spectating.Name == msg.Name { 301 | msg.Team = client.Team 302 | } 303 | } 304 | if msg.Team == "" { 305 | if msg.Name == "BanchoBot" { 306 | msg.Team = "bot" 307 | } else { 308 | msg.Team = "unknown" 309 | } 310 | } 311 | 312 | messages = append(messages, msg) 313 | 314 | } 315 | if len(messages) > 0 { 316 | return messages, nil 317 | } 318 | return nil, nil 319 | } 320 | return nil, nil 321 | } 322 | -------------------------------------------------------------------------------- /memory/values.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | //InMenuValues inside osu!memory 4 | type InMenuValues struct { 5 | MainMenuValues MainMenuValues `json:"mainMenu"` 6 | OsuStatus uint32 `json:"state"` 7 | GameMode int32 `json:"gameMode"` 8 | ChatChecker int8 `json:"isChatEnabled"` //bool (1 byte) 9 | Bm bm `json:"bm"` 10 | Mods modsM `json:"mods"` 11 | PP ppM `json:"pp"` 12 | } 13 | 14 | type folders struct { 15 | Game string `json:"game"` 16 | Skin string `json:"skin"` 17 | Songs string `json:"songs"` 18 | } 19 | 20 | type ResultsScreenValues struct { 21 | Name string `json:"name"` 22 | Score int32 `json:"score"` 23 | MaxCombo int16 `json:"maxCombo"` 24 | Mods modsM `json:"mods"` 25 | H300 int16 `json:"300"` 26 | HGeki int16 `json:"geki"` 27 | H100 int16 `json:"100"` 28 | HKatu int16 `json:"katu"` 29 | H50 int16 `json:"50"` 30 | H0 int16 `json:"0"` 31 | } 32 | 33 | type MainMenuValues struct { 34 | BassDensity float64 `json:"bassDensity"` 35 | } 36 | 37 | //InSettingsValues are values represented inside settings class, could be dynamic 38 | type InSettingsValues struct { 39 | ShowInterface bool `json:"showInterface"` //dynamic in gameplay 40 | Folders folders `json:"folders"` 41 | } 42 | 43 | type TourneyValues struct { 44 | Manager tourneyManager `json:"manager"` 45 | IPCClients []ipcClient `json:"ipcClients"` 46 | } 47 | 48 | type tourneyManager struct { 49 | IPCState int32 `json:"ipcState"` 50 | BO int32 `json:"bestOF"` 51 | Name tName `json:"teamName"` 52 | Stars tStars `json:"stars"` 53 | Bools tBools `json:"bools"` 54 | Chat []tourneyMessage `json:"chat"` 55 | Gameplay tmGameplay `json:"gameplay"` 56 | } 57 | 58 | type tourneyMessage struct { 59 | Team string `json:"team"` 60 | Time string `json:"time"` 61 | Name string `json:"name"` 62 | MessageBody string `json:"messageBody"` 63 | } 64 | 65 | type tmGameplay struct { 66 | Score tScore `json:"score"` 67 | } 68 | 69 | type tBools struct { 70 | ScoreVisible bool `json:"scoreVisible"` 71 | StarsVisible bool `json:"starsVisible"` 72 | } 73 | 74 | type tName struct { 75 | Left string `json:"left"` 76 | Right string `json:"right"` 77 | } 78 | type tStars struct { 79 | Left int32 `json:"left"` 80 | Right int32 `json:"right"` 81 | } 82 | type tScore struct { 83 | Left int32 `json:"left"` 84 | Right int32 `json:"right"` 85 | } 86 | 87 | type ipcClient struct { 88 | ID int32 `json:"-"` 89 | Team string `json:"team"` 90 | Spectating ipcSpec `json:"spectating"` 91 | Gameplay tourneyGameplay `json:"gameplay"` 92 | } 93 | 94 | type ipcSpec struct { 95 | Name string `json:"name"` 96 | Country string `json:"country"` 97 | ID int32 `json:"userID"` 98 | Accuracy float64 `json:"accuracy"` 99 | RankedScore int64 `json:"rankedScore"` 100 | PlayCount int32 `json:"playCount"` 101 | GlobalRank int32 `json:"globalRank"` 102 | GlobalPP int32 `json:"totalPP"` 103 | } 104 | 105 | type tourneyGameplay struct { 106 | GameMode int32 `json:"gameMode"` 107 | Score int32 `json:"score"` 108 | Name string `json:"name"` 109 | Accuracy float64 `json:"accuracy"` 110 | Hits tourneyHits `json:"hits"` 111 | Combo combo `json:"combo"` 112 | Mods modsM `json:"mods"` 113 | Hp hp `json:"hp"` 114 | } 115 | 116 | type gGrade struct { 117 | Current string `json:"current"` 118 | Expected string `json:"maxThisPlay"` 119 | } 120 | 121 | //GameplayValues inside osu!memory 122 | type GameplayValues struct { 123 | GameMode int32 `json:"gameMode"` 124 | Name string `json:"name"` 125 | Score int32 `json:"score"` 126 | Accuracy float64 `json:"accuracy"` 127 | Combo combo `json:"combo"` 128 | Hp hp `json:"hp"` 129 | Hits hits `json:"hits"` 130 | PP ppG `json:"pp"` 131 | KeyOverlay keyOverlay `json:"keyOverlay"` 132 | Leaderboard leaderboard `json:"leaderboard"` 133 | } 134 | 135 | type keyOverlay struct { 136 | K1 keyOverlayButton `json:"k1"` 137 | K2 keyOverlayButton `json:"k2"` 138 | M1 keyOverlayButton `json:"m1"` 139 | M2 keyOverlayButton `json:"m2"` 140 | } 141 | 142 | type keyOverlayButton struct { 143 | IsPressed bool `json:"isPressed"` 144 | Count int32 `json:"count"` 145 | } 146 | 147 | type bm struct { 148 | Time tim `json:"time"` 149 | BeatmapID int32 `json:"id"` 150 | BeatmapSetID int32 `json:"set"` 151 | BeatmapMD5 string `json:"md5"` 152 | RandkedStatus int32 `json:"rankedStatus"` //unknown, unsubmitted, pending/wip/graveyard, unused, ranked, approved, qualified 153 | Metadata Metadata `json:"metadata"` 154 | Stats stats `json:"stats"` 155 | Path path `json:"path"` 156 | HitObjectStats string `json:"-"` 157 | BeatmapString string `json:"-"` 158 | } 159 | 160 | type tim struct { 161 | FirstObj int32 `json:"firstObj"` 162 | PlayTime int32 `json:"current"` 163 | FullTime int32 `json:"full"` 164 | Mp3Time int32 `json:"mp3"` 165 | } 166 | 167 | // Metadata Map data 168 | type Metadata struct { 169 | Artist string `json:"artist"` 170 | ArtistOriginal string `json:"artistOriginal"` 171 | Title string `json:"title"` 172 | TitleOriginal string `json:"titleOriginal"` 173 | Mapper string `json:"mapper"` 174 | Version string `json:"difficulty"` 175 | } 176 | 177 | type stats struct { 178 | BeatmapAR float32 `json:"AR"` 179 | BeatmapCS float32 `json:"CS"` 180 | BeatmapOD float32 `json:"OD"` 181 | BeatmapHP float32 `json:"HP"` 182 | BeatmapSR float32 `json:"SR"` 183 | BeatmapBPM bpm `json:"BPM"` 184 | BeatmapMaxCombo int32 `json:"maxCombo"` 185 | FullSR float32 `json:"fullSR"` 186 | MemoryAR float32 `json:"memoryAR"` 187 | MemoryCS float32 `json:"memoryCS"` 188 | MemoryOD float32 `json:"memoryOD"` 189 | MemoryHP float32 `json:"memoryHP"` 190 | TotalHitObjects int32 `json:"-"` 191 | } 192 | 193 | type bpm struct { 194 | Minimal int `json:"min"` 195 | Maximal int `json:"max"` 196 | } 197 | 198 | type path struct { 199 | InnerBGPath string `json:"full"` 200 | BeatmapFolderString string `json:"folder"` 201 | BeatmapOsuFileString string `json:"file"` 202 | BGPath string `json:"bg"` 203 | AudioPath string `json:"audio"` 204 | FullMP3Path string `json:"-"` 205 | FullDotOsu string `json:"-"` 206 | } 207 | 208 | type modsM struct { 209 | AppliedMods int32 `json:"num"` 210 | PpMods string `json:"str"` 211 | } 212 | 213 | type ppM struct { 214 | PpSS int32 `json:"100"` 215 | Pp99 int32 `json:"99"` 216 | Pp98 int32 `json:"98"` 217 | Pp97 int32 `json:"97"` 218 | Pp96 int32 `json:"96"` 219 | Pp95 int32 `json:"95"` 220 | PpStrains []float64 `json:"strains"` 221 | } 222 | 223 | type combo struct { 224 | Current int16 `json:"current"` 225 | Max int16 `json:"max"` 226 | Temp int16 `json:"-"` 227 | } 228 | 229 | type hp struct { 230 | Normal float64 `json:"normal"` 231 | Smooth float64 `json:"smooth"` 232 | } 233 | 234 | type hits struct { 235 | H300 int16 `json:"300"` 236 | HGeki int16 `json:"geki"` 237 | H100 int16 `json:"100"` 238 | HKatu int16 `json:"katu"` 239 | H50 int16 `json:"50"` 240 | H0 int16 `json:"0"` 241 | H0Temp int16 `json:"-"` 242 | HSB int16 `json:"sliderBreaks"` 243 | Grade gGrade `json:"grade"` 244 | UnstableRate float64 `json:"unstableRate"` 245 | HitErrorArray []int32 `json:"hitErrorArray"` 246 | } 247 | 248 | type tourneyHits struct { 249 | H300 int16 `json:"300"` 250 | HGeki int16 `json:"geki"` 251 | H100 int16 `json:"100"` 252 | HKatu int16 `json:"katu"` 253 | H50 int16 `json:"50"` 254 | H0 int16 `json:"0"` 255 | H0Temp int16 `json:"-"` 256 | HSB int16 `json:"sliderBreaks"` 257 | UnstableRate float64 `json:"unstableRate"` 258 | HitErrorArray []int32 `json:"-"` 259 | } 260 | 261 | type ppG struct { 262 | Pp int32 `json:"current"` 263 | PPifFC int32 `json:"fc"` 264 | PPMaxThisPlay int32 `json:"maxThisPlay"` 265 | } 266 | 267 | type dynamicAddresses struct { 268 | IsReady bool 269 | } 270 | 271 | type leaderPlayer struct { 272 | Name string `json:"name"` 273 | Score int32 `json:"score"` 274 | Combo int16 `json:"combo"` 275 | MaxCombo int16 `json:"maxCombo"` 276 | Mods string `json:"mods"` 277 | H300 int16 `json:"h300"` 278 | H100 int16 `json:"h100"` 279 | H50 int16 `json:"h50"` 280 | H0 int16 `json:"h0"` 281 | Team int32 `json:"team"` 282 | Position int32 `json:"position"` 283 | IsPassing int8 `json:"isPassing"` //bool 284 | } 285 | 286 | type leaderboard struct { 287 | DoesLeaderBoardExists bool `json:"hasLeaderboard"` 288 | IsLeaderBoardVisible bool `json:"isVisible"` 289 | OurPlayer leaderPlayer `json:"ourplayer"` 290 | Slots []leaderPlayer `json:"slots"` 291 | } 292 | 293 | //MenuData contains raw values taken from osu! memory 294 | var MenuData = InMenuValues{} 295 | 296 | //GameplayData contains raw values taken from osu! memory 297 | var GameplayData = GameplayValues{} 298 | 299 | //ResultsScreenData contains raw values taken from osu! memory 300 | var ResultsScreenData = ResultsScreenValues{} 301 | 302 | //SettingsData contains raw values taken from osu! memory 303 | var SettingsData = InSettingsValues{} 304 | 305 | //TourneyData contains raw values taken from osu! memory 306 | var TourneyData = TourneyValues{} 307 | 308 | //DynamicAddresses are in-between pointers that lead to values 309 | var DynamicAddresses = dynamicAddresses{} 310 | -------------------------------------------------------------------------------- /out.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l3lackShark/gosumemory/8b1915f594e218c3fbaa23d0dcddfadd4523c762/out.ico -------------------------------------------------------------------------------- /pp/editor.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "strings" 8 | "time" 9 | "unsafe" 10 | 11 | "github.com/l3lackShark/gosumemory/memory" 12 | "github.com/spf13/cast" 13 | ) 14 | 15 | //#cgo LDFLAGS: -lm 16 | //#cgo CPPFLAGS: -DOPPAI_STATIC_HEADER 17 | //#include 18 | //#include "oppai.c" 19 | import "C" 20 | 21 | var ezeditor C.ezpp_t 22 | 23 | func readEditorData(data *PP, ezeditor C.ezpp_t, needStrain bool) error { 24 | path := memory.MenuData.Bm.Path.FullDotOsu 25 | 26 | if strings.HasSuffix(path, ".osu") && memory.DynamicAddresses.IsReady == true { 27 | cpath := C.CString(path) 28 | 29 | defer C.free(unsafe.Pointer(cpath)) 30 | if rc := C.ezpp(ezeditor, cpath); rc < 0 { 31 | return errors.New(C.GoString(C.errstr(rc))) 32 | } 33 | C.ezpp_set_base_ar(ezeditor, C.float(memory.MenuData.Bm.Stats.BeatmapAR)) 34 | C.ezpp_set_base_od(ezeditor, C.float(memory.MenuData.Bm.Stats.BeatmapOD)) 35 | C.ezpp_set_base_cs(ezeditor, C.float(memory.MenuData.Bm.Stats.BeatmapCS)) 36 | C.ezpp_set_base_hp(ezeditor, C.float(memory.MenuData.Bm.Stats.BeatmapHP)) 37 | C.ezpp_set_accuracy_percent(ezeditor, C.float(100.0)) 38 | C.ezpp_set_nmiss(ezeditor, C.int(0)) 39 | if needStrain == true { 40 | C.ezpp_set_end_time(ezeditor, 0) 41 | C.ezpp_set_combo(ezeditor, 0) 42 | C.ezpp_set_nmiss(ezeditor, 0) 43 | strainArray = nil 44 | seek := 0 45 | var window []float64 46 | var total []float64 47 | // for seek < int(C.ezpp_time_at(ezeditor, C.ezpp_nobjects(ezeditor)-1)) { //len-1 48 | for int32(seek) < memory.MenuData.Bm.Time.Mp3Time { 49 | for obj := 0; obj <= int(C.ezpp_nobjects(ezeditor)-1); obj++ { 50 | if tempBeatmapFile != memory.MenuData.Bm.Path.BeatmapOsuFileString { 51 | return nil //Interrupt calcualtion if user has changed the map. 52 | } 53 | if int(C.ezpp_time_at(ezeditor, C.int(obj))) >= seek && int(C.ezpp_time_at(ezeditor, C.int(obj))) <= seek+3000 { 54 | window = append(window, float64(C.ezpp_strain_at(ezeditor, C.int(obj), 0))+float64(C.ezpp_strain_at(ezeditor, C.int(obj), 1))) 55 | } 56 | } 57 | sum := 0.0 58 | for _, num := range window { 59 | sum += num 60 | } 61 | total = append(total, sum/math.Max(float64(len(window)), 1)) 62 | window = nil 63 | seek += 500 64 | } 65 | strainArray = total 66 | memory.MenuData.Bm.Time.FirstObj = int32(C.ezpp_time_at(ezeditor, 0)) 67 | memory.MenuData.Bm.Time.FullTime = int32(C.ezpp_time_at(ezeditor, C.ezpp_nobjects(ezeditor)-1)) 68 | 69 | } else { 70 | C.ezpp_set_mods(ezeditor, C.int(0)) 71 | C.ezpp_set_end_time(ezeditor, C.float(memory.MenuData.Bm.Time.PlayTime)) 72 | C.ezpp_set_combo(ezeditor, C.int(-1)) 73 | } 74 | 75 | *data = PP{ 76 | Total: C.ezpp_pp(ezeditor), 77 | Strain: strainArray, 78 | AR: C.ezpp_ar(ezeditor), 79 | CS: C.ezpp_cs(ezeditor), 80 | OD: C.ezpp_od(ezeditor), 81 | HP: C.ezpp_hp(ezeditor), 82 | StarRating: C.ezpp_stars(ezeditor), 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | var tempOsuMD5 string 89 | 90 | func GetEditorData() { 91 | 92 | ezeditor = C.ezpp_new() 93 | C.ezpp_set_autocalc(ezeditor, 1) 94 | //defer C.ezpp_free(ezeditor) 95 | var data PP 96 | 97 | for { 98 | if memory.DynamicAddresses.IsReady == true { 99 | if memory.MenuData.GameMode == 0 && memory.MenuData.OsuStatus == 1 { 100 | err := readEditorData(&data, ezeditor, false) 101 | if err != nil { 102 | fmt.Println(err) 103 | } 104 | memory.GameplayData.PP.Pp = int32(data.Total) 105 | memory.MenuData.Bm.Stats.BeatmapAR = float32(data.AR) 106 | memory.MenuData.Bm.Stats.BeatmapCS = float32(data.CS) 107 | memory.MenuData.Bm.Stats.BeatmapOD = float32(data.OD) 108 | memory.MenuData.Bm.Stats.BeatmapHP = float32(data.HP) 109 | memory.MenuData.Bm.Stats.BeatmapSR = cast.ToFloat32(fmt.Sprintf("%.2f", float32(data.StarRating))) 110 | 111 | md5, err := hashFileMD5(memory.MenuData.Bm.Path.FullDotOsu) 112 | if err != nil { 113 | continue 114 | } 115 | if tempOsuMD5 != md5 { 116 | tempOsuMD5 = md5 117 | readEditorData(&data, ezeditor, true) 118 | memory.MenuData.PP.PpStrains = data.Strain 119 | } 120 | 121 | } 122 | 123 | } 124 | time.Sleep(time.Duration(memory.UpdateTime) * time.Millisecond) 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /pp/functions.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "github.com/k0kubun/pp" 12 | "github.com/l3lackShark/gosumemory/memory" 13 | "github.com/tcolgate/mp3" 14 | ) 15 | 16 | func hashFileMD5(filePath string) (string, error) { 17 | var returnMD5String string 18 | file, err := os.Open(filePath) 19 | if err != nil { 20 | return returnMD5String, err 21 | } 22 | defer file.Close() 23 | hash := md5.New() 24 | if _, err := io.Copy(hash, file); err != nil { 25 | return returnMD5String, err 26 | } 27 | hashInBytes := hash.Sum(nil)[:16] 28 | returnMD5String = hex.EncodeToString(hashInBytes) 29 | return returnMD5String, nil 30 | 31 | } 32 | func calculateMP3Time() (int32, error) { 33 | if memory.MenuData.Bm.Path.FullMP3Path == "" { 34 | return 0, nil 35 | } 36 | if !strings.HasSuffix(strings.ToLower(memory.MenuData.Bm.Path.FullMP3Path), ".mp3") { 37 | pp.Println("Expected mp3, got something else. Aborting mp3 time calculation. GOT: ", memory.MenuData.Bm.Path.FullMP3Path) 38 | return 0, nil 39 | } 40 | var t int64 41 | r, err := os.Open(memory.MenuData.Bm.Path.FullMP3Path) 42 | if err != nil { 43 | fmt.Println(err) 44 | return 0, err 45 | } 46 | 47 | d := mp3.NewDecoder(r) 48 | var f mp3.Frame 49 | skipped := 0 50 | 51 | for { 52 | 53 | if err := d.Decode(&f, &skipped); err != nil { 54 | if err != nil { 55 | break 56 | } 57 | } 58 | 59 | t = t + f.Duration().Milliseconds() 60 | } 61 | 62 | return int32(t), nil 63 | } 64 | 65 | func minMax(array []int) (int, int) { 66 | if len(array) < 1 { 67 | return 0, 0 68 | } 69 | var max int = array[0] 70 | var min int = array[0] 71 | for _, value := range array { 72 | if max < value { 73 | max = value 74 | } 75 | if min > value { 76 | min = value 77 | } 78 | } 79 | return min, max 80 | } 81 | 82 | func calculateAccuracy(h300 float32, h100 float32, h50 float32, h0 float32) float32 { 83 | return 100 * (h50*50 + h100*100 + h300*300) / (h50*300 + h100*300 + h300*300 + h0*300) 84 | } 85 | 86 | func calculateGrade(h300 float32, h100 float32, h50 float32, h0 float32) string { //https://osu.ppy.sh/help/wiki/FAQ#grades 87 | onePercent := (h300 + h100 + h50 + h0) / 100 88 | if h100 == 0 && h50 == 0 && h0 == 0 { 89 | return "SS" 90 | } 91 | if h0 == 0 && onePercent*90 < h300 && h50 < onePercent { 92 | return "S" 93 | } 94 | if h0 == 0 && onePercent*80 < h300 || onePercent*90 < h300 { 95 | return "A" 96 | } 97 | if h0 == 0 && onePercent*70 < h300 || onePercent*80 < h300 { 98 | return "B" 99 | } 100 | if onePercent*60 < h300 { 101 | return "C" 102 | } 103 | return "D" 104 | } 105 | -------------------------------------------------------------------------------- /pp/iffc.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | //TODO: I need to figure out how to use only one calc. 4 | 5 | import ( 6 | "errors" 7 | "math" 8 | "strings" 9 | "time" 10 | "unsafe" 11 | 12 | "github.com/k0kubun/pp" 13 | "github.com/l3lackShark/gosumemory/memory" 14 | "github.com/spf13/cast" 15 | ) 16 | 17 | //#cgo LDFLAGS: -lm 18 | //#cgo CPPFLAGS: -DOPPAI_STATIC_HEADER 19 | //#include 20 | //#include "oppai.c" 21 | import "C" 22 | 23 | var ezfc C.ezpp_t 24 | 25 | type PPfc struct { 26 | RestSS C.float 27 | Acc C.float 28 | GradeCurrent string 29 | GradeExpected string 30 | } 31 | 32 | func readFCData(data *PPfc, ezfc C.ezpp_t, acc C.float) error { 33 | path := memory.MenuData.Bm.Path.FullDotOsu 34 | 35 | if strings.HasSuffix(path, ".osu") && memory.DynamicAddresses.IsReady == true { 36 | cpath := C.CString(path) 37 | 38 | defer C.free(unsafe.Pointer(cpath)) 39 | if rc := C.ezpp(ezfc, cpath); rc < 0 { 40 | return errors.New(C.GoString(C.errstr(rc))) 41 | } 42 | C.ezpp_set_base_ar(ezfc, C.float(memory.MenuData.Bm.Stats.BeatmapAR)) 43 | C.ezpp_set_base_od(ezfc, C.float(memory.MenuData.Bm.Stats.BeatmapOD)) 44 | C.ezpp_set_base_cs(ezfc, C.float(memory.MenuData.Bm.Stats.BeatmapCS)) 45 | C.ezpp_set_base_hp(ezfc, C.float(memory.MenuData.Bm.Stats.BeatmapHP)) 46 | C.ezpp_set_mods(ezfc, C.int(memory.MenuData.Mods.AppliedMods)) 47 | totalObj := C.ezpp_nobjects(ezfc) 48 | 49 | if memory.GameplayData.Hits.H0+memory.GameplayData.Hits.HSB == 0 { 50 | totalCombo := C.ezpp_max_combo(ezfc) 51 | diff := currMaxCombo - C.int(memory.GameplayData.Combo.Max) 52 | C.ezpp_set_combo(ezfc, C.int(totalCombo-diff)) 53 | } else { 54 | C.ezpp_set_combo(ezfc, C.int(-1)) //since we are not freeing the counter every time we need to clear the combo TODO: Consider dropped sliderends 55 | } 56 | C.ezpp_set_nmiss(ezfc, C.int(0)) 57 | 58 | remaining := int16(totalObj) - memory.GameplayData.Hits.H300 - memory.GameplayData.Hits.H100 - memory.GameplayData.Hits.H50 - memory.GameplayData.Hits.H0 59 | ifRestSSACC := float64(calculateAccuracy(float32(memory.GameplayData.Hits.H300+remaining), float32(memory.GameplayData.Hits.H100), float32(memory.GameplayData.Hits.H50), float32(memory.GameplayData.Hits.H0))) 60 | ifRestSSACC = math.Round(ifRestSSACC*100) / 100 61 | C.ezpp_set_accuracy_percent(ezfc, C.float(ifRestSSACC)) 62 | ifRestSS := C.ezpp_pp(ezfc) 63 | C.ezpp_set_combo(ezfc, C.int(-1)) 64 | C.ezpp_set_accuracy_percent(ezfc, C.float(acc)) 65 | //C.ezpp_set_score_version(ezfc) 66 | *data = PPfc{ 67 | RestSS: ifRestSS, 68 | Acc: C.ezpp_pp(ezfc), 69 | GradeCurrent: calculateGrade(float32(memory.GameplayData.Hits.H300), float32(memory.GameplayData.Hits.H100), float32(memory.GameplayData.Hits.H50), float32(memory.GameplayData.Hits.H0)), 70 | GradeExpected: calculateGrade(float32(memory.GameplayData.Hits.H300+remaining), float32(memory.GameplayData.Hits.H100), float32(memory.GameplayData.Hits.H50), float32(memory.GameplayData.Hits.H0)), 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func GetFCData() { 78 | ezfc = C.ezpp_new() 79 | C.ezpp_set_autocalc(ezfc, 1) 80 | for { 81 | 82 | if memory.DynamicAddresses.IsReady == true { 83 | 84 | switch memory.GameplayData.GameMode { 85 | case 0, 1: 86 | 87 | if memory.MenuData.OsuStatus == 2 && memory.GameplayData.Combo.Max > 0 { 88 | var data PPfc 89 | readFCData(&data, ezfc, C.float(memory.GameplayData.Accuracy)) 90 | res, err := wiekuCalcCrutch(memory.MenuData.Bm.Path.FullDotOsu, int16(memory.MenuData.Bm.Stats.BeatmapMaxCombo), int16(C.ezpp_nobjects(ezfc)-1)-memory.GameplayData.Hits.H100-memory.GameplayData.Hits.H50, memory.GameplayData.Hits.H100, memory.GameplayData.Hits.H50, 0) 91 | if err != nil { 92 | pp.Println(err) 93 | memory.GameplayData.PP.PPifFC = cast.ToInt32(float64(data.RestSS)) 94 | } else { 95 | memory.GameplayData.PP.PPifFC = res 96 | } 97 | 98 | memory.GameplayData.Hits.Grade.Current = data.GradeCurrent 99 | memory.GameplayData.Hits.Grade.Expected = data.GradeExpected 100 | } 101 | switch memory.MenuData.OsuStatus { 102 | case 1, 4, 5, 13, 2: 103 | if memory.MenuData.OsuStatus == 2 && memory.MenuData.Bm.Time.PlayTime > 150 { //To catch up with the F2-->Enter 104 | time.Sleep(250 * time.Millisecond) 105 | continue 106 | } 107 | //TODO: figure out how to calc %% pp on the new rework 108 | // if memory.GameplayData.GameMode == 0 { 109 | // wiekuCalcCrutch(memory.MenuData.Bm.Path.FullDotOsu, int16(memory.MenuData.Bm.Stats.BeatmapMaxCombo), desired300Hits()) 110 | // } 111 | var data PPfc 112 | readFCData(&data, ezfc, 100.0) 113 | memory.MenuData.PP.PpSS = cast.ToInt32(float64(data.Acc)) 114 | readFCData(&data, ezfc, 99.0) 115 | memory.MenuData.PP.Pp99 = cast.ToInt32(float64(data.Acc)) 116 | readFCData(&data, ezfc, 98.0) 117 | memory.MenuData.PP.Pp98 = cast.ToInt32(float64(data.Acc)) 118 | readFCData(&data, ezfc, 97.0) 119 | memory.MenuData.PP.Pp97 = cast.ToInt32(float64(data.Acc)) 120 | readFCData(&data, ezfc, 96.0) 121 | memory.MenuData.PP.Pp96 = cast.ToInt32(float64(data.Acc)) 122 | readFCData(&data, ezfc, 95.0) 123 | memory.MenuData.PP.Pp95 = cast.ToInt32(float64(data.Acc)) 124 | } 125 | } 126 | 127 | } 128 | 129 | time.Sleep(250 * time.Millisecond) 130 | } 131 | } 132 | 133 | func desired300Hits(maxcombo float32, acc float32) int16 { 134 | return int16(maxcombo / 100.0 * acc) 135 | } 136 | -------------------------------------------------------------------------------- /pp/mania.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func calculateManiaPP(od float64, stars float64, noteCount float64, score float64) float64 { 8 | 9 | //var strainStep := 400 * timeScale 10 | hit300Window := 34 + 3*(math.Min(10, math.Max(0, 10-od))) 11 | strainValue := math.Pow(5*math.Max(1, stars/0.2)-4, 2.2) / 135 * (1 + 0.1*math.Min(1, noteCount/1500)) 12 | //if score <= 500000 { 13 | // strainValue = 0 14 | //} 15 | if score <= 600000 { 16 | strainValue *= (score - 500000) / 100000 * 0.3 17 | } else if score <= 700000 { 18 | strainValue *= 0.3 + (score-600000)/100000*0.25 19 | } else if score <= 800000 { 20 | strainValue *= 0.55 + (score-700000)/100000*0.20 21 | } else if score <= 900000 { 22 | strainValue *= 0.75 + (score-800000)/100000*0.15 23 | } else { 24 | strainValue *= 0.9 + (score-900000)/100000*0.1 25 | } 26 | accValue := math.Max(0, 0.2-((hit300Window-34)*0.006667)) * strainValue * math.Pow(math.Max(0, score-960000)/40000, 1.1) 27 | ppMultiplier := 0.8 28 | 29 | return math.Pow(math.Pow(strainValue, 1.1)+math.Pow(accValue, 1.1), 1/1.1) * ppMultiplier 30 | } 31 | -------------------------------------------------------------------------------- /pp/max.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | //TODO: I need to figure out how to use only one calc. 4 | 5 | //#cgo LDFLAGS: -lm 6 | //#cgo CPPFLAGS: -DOPPAI_STATIC_HEADER 7 | //#include 8 | //#include "oppai.c" 9 | import "C" 10 | import ( 11 | "errors" 12 | "math" 13 | "strings" 14 | "time" 15 | "unsafe" 16 | 17 | "github.com/l3lackShark/gosumemory/memory" 18 | "github.com/spf13/cast" 19 | ) 20 | 21 | var ezmax C.ezpp_t 22 | var tempBadJudgments int16 23 | var possibleMax float64 24 | 25 | type PPmax struct { 26 | MaxThisPlay C.float 27 | } 28 | 29 | func readMaxData(data *PPmax, ezmax C.ezpp_t) error { 30 | path := memory.MenuData.Bm.Path.FullDotOsu 31 | 32 | if strings.HasSuffix(path, ".osu") && memory.DynamicAddresses.IsReady == true { 33 | cpath := C.CString(path) 34 | 35 | defer C.free(unsafe.Pointer(cpath)) 36 | if rc := C.ezpp(ezmax, cpath); rc < 0 { 37 | return errors.New(C.GoString(C.errstr(rc))) 38 | } 39 | C.ezpp_set_base_ar(ezmax, C.float(memory.MenuData.Bm.Stats.BeatmapAR)) 40 | C.ezpp_set_base_od(ezmax, C.float(memory.MenuData.Bm.Stats.BeatmapOD)) 41 | C.ezpp_set_base_cs(ezmax, C.float(memory.MenuData.Bm.Stats.BeatmapCS)) 42 | C.ezpp_set_base_hp(ezmax, C.float(memory.MenuData.Bm.Stats.BeatmapHP)) 43 | C.ezpp_set_mods(ezmax, C.int(memory.MenuData.Mods.AppliedMods)) 44 | totalObj := C.ezpp_nobjects(ezmax) 45 | totalCombo := C.ezpp_max_combo(ezmax) 46 | remaining := int16(totalObj) - memory.GameplayData.Hits.H300 - memory.GameplayData.Hits.H100 - memory.GameplayData.Hits.H50 - memory.GameplayData.Hits.H0 47 | ifRestSSACC := float64(calculateAccuracy(float32(memory.GameplayData.Hits.H300+remaining), float32(memory.GameplayData.Hits.H100), float32(memory.GameplayData.Hits.H50), float32(memory.GameplayData.Hits.H0))) 48 | ifRestSSACC = math.Round(ifRestSSACC*100) / 100 49 | C.ezpp_set_accuracy_percent(ezmax, C.float(ifRestSSACC)) 50 | 51 | //Get Possible max combo in the current play 52 | //var lessThanMaxCombo bool 53 | 54 | combinedBadJudgments := memory.GameplayData.Hits.H0 + memory.GameplayData.Hits.HSB 55 | if combinedBadJudgments > 0 { 56 | if tempBadJudgments != combinedBadJudgments { 57 | tempBadJudgments = combinedBadJudgments 58 | possibleMax = math.Max(float64(totalCombo-currMaxCombo), float64(memory.GameplayData.Combo.Max)) 59 | } 60 | //lessThanMaxCombo = true 61 | } else { 62 | possibleMax = float64(totalCombo) 63 | //lessThanMaxCombo = false 64 | } 65 | 66 | if memory.MenuData.OsuStatus == 2 { 67 | C.ezpp_set_nmiss(ezmax, C.int(memory.GameplayData.Hits.H0)) 68 | C.ezpp_set_combo(ezmax, C.int(possibleMax)) 69 | } 70 | 71 | maxThisPlay := C.ezpp_pp(ezmax) 72 | *data = PPmax{ 73 | MaxThisPlay: maxThisPlay, 74 | } 75 | // type test struct { 76 | // expectedAccuracy float64 77 | // RemainingHitObj int16 78 | // lessThanMaxCombo bool 79 | // maxPossibleCombo int32 80 | // maxThisPlayPP int32 81 | // currentPP int32 82 | // ifFC int32 83 | // } 84 | // testing := test{ifRestSSACC, remaining, lessThanMaxCombo, int32(possibleMax), int32(maxThisPlay), memory.GameplayData.PP.Pp, memory.GameplayData.PP.PPifFC} 85 | // pp.Println(testing) 86 | 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func GetMaxData() { 93 | ezmax = C.ezpp_new() 94 | C.ezpp_set_autocalc(ezmax, 1) 95 | for { 96 | 97 | if memory.DynamicAddresses.IsReady == true { 98 | 99 | switch memory.GameplayData.GameMode { 100 | case 0, 1: 101 | 102 | if memory.MenuData.OsuStatus == 2 && memory.GameplayData.Combo.Max > 0 { 103 | var data PPmax 104 | readMaxData(&data, ezmax) 105 | memory.GameplayData.PP.PPMaxThisPlay = cast.ToInt32(float64(data.MaxThisPlay)) 106 | } 107 | } 108 | } 109 | 110 | time.Sleep(250 * time.Millisecond) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /pp/oppai_impl.c: -------------------------------------------------------------------------------- 1 | #define OPPAI_IMPLEMENTATION 2 | #include "oppai.c" -------------------------------------------------------------------------------- /pp/pp.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "os" 8 | "strings" 9 | "time" 10 | "unsafe" 11 | 12 | "github.com/Wieku/gosu-pp/beatmap" 13 | "github.com/Wieku/gosu-pp/beatmap/difficulty" 14 | "github.com/Wieku/gosu-pp/performance/osu" 15 | "github.com/k0kubun/pp" 16 | "github.com/l3lackShark/gosumemory/memory" 17 | "github.com/spf13/cast" 18 | ) 19 | 20 | //#cgo LDFLAGS: -lm 21 | //#cgo CPPFLAGS: -DOPPAI_STATIC_HEADER 22 | //#include 23 | //#include "oppai.c" 24 | import "C" 25 | 26 | var ez C.ezpp_t 27 | 28 | type PP struct { 29 | Total C.float 30 | FC C.float 31 | Strain []float64 32 | StarRating C.float 33 | AimStars C.float 34 | SpeedStars C.float 35 | AimPP C.float 36 | SpeedPP C.float 37 | Accuracy C.float 38 | N300 C.int 39 | N100 C.int 40 | N50 C.int 41 | NMiss C.int 42 | AR C.float 43 | CS C.float 44 | OD C.float 45 | HP C.float 46 | Artist string 47 | ArtistUnicode string 48 | Title string 49 | TitleUnicode string 50 | Version string 51 | Creator string 52 | NCircles C.int 53 | NSliders C.int 54 | NSpinners C.int 55 | ODMS C.float 56 | Mode C.int 57 | Combo C.int 58 | MaxCombo C.int 59 | Mods C.int 60 | ScoreVersion C.int 61 | } 62 | 63 | var strainArray []float64 64 | var tempBeatmapFile string 65 | var tempGameMode int32 = 4 66 | var currMaxCombo C.int 67 | 68 | func readData(data *PP, ez C.ezpp_t, needStrain bool, path string) error { 69 | 70 | if strings.HasSuffix(path, ".osu") { 71 | cpath := C.CString(path) 72 | 73 | defer C.free(unsafe.Pointer(cpath)) 74 | if rc := C.ezpp(ez, cpath); rc < 0 { 75 | memory.MenuData.PP.PpStrains = []float64{0} 76 | return errors.New(C.GoString(C.errstr(rc))) 77 | } 78 | C.ezpp_set_base_ar(ez, C.float(memory.MenuData.Bm.Stats.MemoryAR)) 79 | C.ezpp_set_base_od(ez, C.float(memory.MenuData.Bm.Stats.MemoryOD)) 80 | C.ezpp_set_base_cs(ez, C.float(memory.MenuData.Bm.Stats.MemoryCS)) 81 | C.ezpp_set_base_hp(ez, C.float(memory.MenuData.Bm.Stats.MemoryHP)) 82 | C.ezpp_set_accuracy_percent(ez, C.float(memory.GameplayData.Accuracy)) 83 | C.ezpp_set_mods(ez, C.int(memory.MenuData.Mods.AppliedMods)) 84 | *data = PP{ 85 | Artist: C.GoString(C.ezpp_artist(ez)), 86 | Title: C.GoString(C.ezpp_title(ez)), 87 | Version: C.GoString(C.ezpp_version(ez)), 88 | Creator: C.GoString(C.ezpp_creator(ez)), 89 | AR: C.ezpp_ar(ez), 90 | CS: C.ezpp_cs(ez), 91 | OD: C.ezpp_od(ez), 92 | HP: C.ezpp_hp(ez), 93 | StarRating: C.ezpp_stars(ez), 94 | } 95 | memory.MenuData.Bm.Stats.BeatmapSR = cast.ToFloat32(fmt.Sprintf("%.2f", float32(data.StarRating))) 96 | memory.MenuData.Bm.Stats.BeatmapAR = cast.ToFloat32(fmt.Sprintf("%.2f", float32(data.AR))) 97 | memory.MenuData.Bm.Stats.BeatmapCS = cast.ToFloat32(fmt.Sprintf("%.2f", float32(data.CS))) 98 | memory.MenuData.Bm.Stats.BeatmapOD = cast.ToFloat32(fmt.Sprintf("%.2f", float32(data.OD))) 99 | memory.MenuData.Bm.Stats.BeatmapHP = cast.ToFloat32(fmt.Sprintf("%.2f", float32(data.HP))) 100 | 101 | if needStrain == true { 102 | C.ezpp_set_end_time(ez, 0) 103 | C.ezpp_set_combo(ez, 0) 104 | C.ezpp_set_nmiss(ez, 0) 105 | memory.MenuData.Bm.Stats.BeatmapMaxCombo = int32(C.ezpp_max_combo(ez)) 106 | memory.MenuData.Bm.Stats.FullSR = cast.ToFloat32(fmt.Sprintf("%.2f", float32(C.ezpp_stars(ez)))) 107 | var bpmChanges []int 108 | var bpmMultiplier float64 = 1 109 | if strings.Contains(memory.MenuData.Mods.PpMods, "DT") || strings.Contains(memory.MenuData.Mods.PpMods, "NC") { 110 | bpmMultiplier = 1.5 111 | } else if strings.Contains(memory.MenuData.Mods.PpMods, "HT") { 112 | bpmMultiplier = 0.75 113 | } 114 | for i := 0; i < int(C.ezpp_ntiming_points(ez)); i++ { 115 | msPerBeat := float64(C.ezpp_timing_ms_per_beat(ez, C.int(i))) 116 | timingChanges := int(C.ezpp_timing_change(ez, C.int(i))) 117 | if timingChanges == 1 { 118 | bpmFormula := int(math.Round(1 / msPerBeat * 1000 * 60 * bpmMultiplier)) 119 | if bpmFormula > 0 { 120 | bpmChanges = append(bpmChanges, bpmFormula) 121 | } 122 | } 123 | } 124 | memory.MenuData.Bm.Stats.BeatmapBPM.Minimal, memory.MenuData.Bm.Stats.BeatmapBPM.Maximal = minMax(bpmChanges) 125 | strainArray = nil 126 | seek := 0 127 | var window []float64 128 | var total []float64 129 | // for seek < int(C.ezpp_time_at(ez, C.ezpp_nobjects(ez)-1)) { //len-1 130 | for int32(seek) < memory.MenuData.Bm.Time.Mp3Time { 131 | for obj := 0; obj <= int(C.ezpp_nobjects(ez)-1); obj++ { 132 | if tempBeatmapFile != memory.MenuData.Bm.Path.BeatmapOsuFileString { 133 | return nil //Interrupt calcualtion if user has changed the map. 134 | } 135 | if int(C.ezpp_time_at(ez, C.int(obj))) >= seek && int(C.ezpp_time_at(ez, C.int(obj))) <= seek+3000 { 136 | window = append(window, float64(C.ezpp_strain_at(ez, C.int(obj), 0))+float64(C.ezpp_strain_at(ez, C.int(obj), 1))) 137 | } 138 | } 139 | sum := 0.0 140 | for _, num := range window { 141 | sum += num 142 | } 143 | total = append(total, sum/math.Max(float64(len(window)), 1)) 144 | window = nil 145 | seek += 500 146 | } 147 | strainArray = total 148 | memory.MenuData.Bm.Time.FirstObj = int32(C.ezpp_time_at(ez, 0)) 149 | memory.MenuData.Bm.Time.FullTime = int32(C.ezpp_time_at(ez, C.ezpp_nobjects(ez)-1)) 150 | } else { 151 | C.ezpp_set_end_time(ez, C.float(memory.MenuData.Bm.Time.PlayTime)) 152 | currMaxCombo = C.ezpp_max_combo(ez) //for RestSS 153 | C.ezpp_set_combo(ez, C.int(memory.GameplayData.Combo.Max)) 154 | C.ezpp_set_nmiss(ez, C.int(memory.GameplayData.Hits.H0)) 155 | } 156 | 157 | *data = PP{ 158 | Total: C.ezpp_pp(ez), 159 | Strain: strainArray, 160 | 161 | AimStars: C.ezpp_aim_stars(ez), 162 | SpeedStars: C.ezpp_speed_stars(ez), 163 | AimPP: C.ezpp_aim_pp(ez), 164 | SpeedPP: C.ezpp_speed_pp(ez), 165 | Accuracy: C.ezpp_accuracy_percent(ez), 166 | N300: C.ezpp_n300(ez), 167 | N100: C.ezpp_n100(ez), 168 | N50: C.ezpp_n50(ez), 169 | NMiss: C.ezpp_nmiss(ez), 170 | //ArtistUnicode: C.GoString(C.ezpp_artist_unicode(ez)), 171 | // TitleUnicode: C.GoString(C.ezpp_title_unicode(ez)), 172 | NCircles: C.ezpp_ncircles(ez), 173 | NSliders: C.ezpp_nsliders(ez), 174 | NSpinners: C.ezpp_nspinners(ez), 175 | ODMS: C.ezpp_odms(ez), 176 | Mode: C.ezpp_mode(ez), 177 | Combo: C.ezpp_combo(ez), 178 | MaxCombo: C.ezpp_max_combo(ez), 179 | Mods: C.ezpp_mods(ez), 180 | ScoreVersion: C.ezpp_score_version(ez), 181 | } 182 | memory.MenuData.PP.PpStrains = data.Strain 183 | } 184 | return nil 185 | } 186 | 187 | var maniaSR float64 188 | var maniaHitObjects float64 189 | var tempMods string 190 | 191 | func GetData() { 192 | 193 | ez = C.ezpp_new() 194 | C.ezpp_set_autocalc(ez, 1) 195 | //defer C.ezpp_free(ez) 196 | 197 | for { 198 | 199 | if memory.DynamicAddresses.IsReady == true { 200 | switch memory.MenuData.GameMode { 201 | case 0, 1: 202 | var data PP 203 | if tempBeatmapFile != memory.MenuData.Bm.Path.BeatmapOsuFileString || memory.MenuData.Mods.PpMods != tempMods || memory.MenuData.GameMode != tempGameMode { //On map/mods change 204 | tempGameMode = memory.MenuData.GameMode // looks very ugly but will rewrite everything in 1.4.0 205 | tempBadJudgments = 0 206 | path := memory.MenuData.Bm.Path.FullDotOsu 207 | tempBeatmapFile = memory.MenuData.Bm.Path.BeatmapOsuFileString 208 | tempMods = memory.MenuData.Mods.PpMods 209 | tempGameMode = memory.MenuData.GameMode 210 | mp3Time, err := calculateMP3Time() 211 | if err == nil { 212 | memory.MenuData.Bm.Time.Mp3Time = mp3Time 213 | } 214 | //Get Strains 215 | readData(&data, ez, true, path) 216 | 217 | //pp.Println(memory.MenuData.Bm.Metadata) 218 | } 219 | 220 | switch memory.MenuData.OsuStatus { 221 | case 2: 222 | path := memory.MenuData.Bm.Path.FullDotOsu 223 | readData(&data, ez, false, path) 224 | if memory.GameplayData.Combo.Max > 1 && float64(data.Total) > 0 { 225 | //pre-Wieku rewrite crutch 226 | if memory.GameplayData.GameMode == 0 { 227 | 228 | res, err := wiekuCalcCrutch(path, memory.GameplayData.Combo.Max, memory.GameplayData.Hits.H300, memory.GameplayData.Hits.H100, memory.GameplayData.Hits.H50, memory.GameplayData.Hits.H0) 229 | if err != nil { 230 | pp.Println(err) 231 | memory.GameplayData.PP.Pp = cast.ToInt32(float64(data.Total)) 232 | } 233 | memory.GameplayData.PP.Pp = cast.ToInt32(res) 234 | 235 | } else { 236 | memory.GameplayData.PP.Pp = cast.ToInt32(float64(data.Total)) 237 | } 238 | } 239 | case 7: 240 | //idle 241 | case 5: 242 | memory.GameplayData.PP.Pp = 0 243 | } 244 | 245 | case 3: 246 | 247 | if tempBeatmapFile != memory.MenuData.Bm.Path.BeatmapOsuFileString || memory.MenuData.Mods.PpMods != tempMods || memory.MenuData.GameMode != tempGameMode { //On map/mods/mode change 248 | tempGameMode = memory.MenuData.GameMode // looks very ugly but will rewrite everything in 1.4.0 249 | tempBeatmapFile = memory.MenuData.Bm.Path.BeatmapOsuFileString 250 | tempMods = memory.MenuData.Mods.PpMods 251 | 252 | maniaSR = 0.0 253 | memory.MenuData.Bm.Time.FullTime = 0 //Not implemented for mania yet 254 | memory.MenuData.Bm.Stats.BeatmapAR = 0 //Not implemented for mania yet 255 | memory.MenuData.Bm.Stats.BeatmapCS = 0 //Not implemented for mania yet 256 | memory.MenuData.Bm.Stats.BeatmapOD = 0 //Not implemented for mania yet 257 | memory.MenuData.Bm.Stats.BeatmapHP = 0 //Not implemented for mania yet 258 | memory.MenuData.PP.PpStrains = []float64{0} //Not implemented for mania yet 259 | 260 | maniaStars, err := memory.ReadManiaStars() 261 | if err != nil { 262 | pp.Println(err) 263 | } 264 | if maniaStars.NoMod == 0 { //diff calc in progress 265 | for i := 0; i < 50; i++ { 266 | maniaStars, _ = memory.ReadManiaStars() 267 | if maniaStars.NoMod > 0 { 268 | break 269 | } 270 | time.Sleep(100 * time.Millisecond) 271 | } 272 | } 273 | 274 | maniaHitObjects = float64(memory.MenuData.Bm.Stats.TotalHitObjects) 275 | 276 | if strings.Contains(memory.MenuData.Mods.PpMods, "DT") { 277 | maniaSR = maniaStars.DT 278 | } else if strings.Contains(memory.MenuData.Mods.PpMods, "HT") { 279 | maniaSR = maniaStars.HT 280 | } else { 281 | maniaSR = maniaStars.NoMod //assuming NM 282 | } 283 | memory.MenuData.Bm.Stats.BeatmapSR = cast.ToFloat32(fmt.Sprintf("%.2f", float32(maniaSR))) 284 | memory.MenuData.Bm.Stats.FullSR = memory.MenuData.Bm.Stats.BeatmapSR 285 | memory.MenuData.PP.PpSS = int32(calculateManiaPP(float64(memory.MenuData.Bm.Stats.MemoryOD), maniaSR, maniaHitObjects, 1000000.0)) // LiveSR not implemented yet 286 | } 287 | } 288 | if memory.GameplayData.GameMode == 3 { 289 | if maniaSR > 0 { 290 | memory.GameplayData.PP.PPifFC = int32(calculateManiaPP(float64(memory.MenuData.Bm.Stats.MemoryOD), maniaSR, maniaHitObjects, 1000000.0)) //PP if SS 291 | if memory.GameplayData.Score >= 500000 { 292 | memory.GameplayData.PP.Pp = int32(calculateManiaPP(float64(memory.MenuData.Bm.Stats.MemoryOD), maniaSR, maniaHitObjects, float64(memory.GameplayData.Score))) 293 | } else { 294 | memory.GameplayData.PP.Pp = 0 295 | } 296 | } 297 | } 298 | } 299 | 300 | time.Sleep(time.Duration(memory.UpdateTime) * time.Millisecond) 301 | } 302 | } 303 | 304 | var ( 305 | tempWiekuFileName string 306 | tempWiekuMods int32 307 | beatMap *beatmap.BeatMap 308 | attribs []osu.Attributes 309 | ) 310 | 311 | func wiekuCalcCrutch(path string, combo int16, h300 int16, h100 int16, h50 int16, h0 int16) (int32, error) { 312 | if tempWiekuFileName != path || tempWiekuMods != memory.MenuData.Mods.AppliedMods { 313 | tempWiekuFileName = path 314 | tempWiekuMods = memory.MenuData.Mods.AppliedMods 315 | 316 | osuFile, err := os.Open(path) 317 | if err != nil { 318 | return 0, fmt.Errorf("Failed to calc via wieku calculator, falling back to oppai, ERROR: %w", err) 319 | } 320 | defer osuFile.Close() 321 | 322 | beatMap, err = beatmap.ParseFromReader(osuFile) 323 | if err != nil { 324 | return 0, fmt.Errorf("Failed to calc via wieku calculator, falling back to oppai, ERROR: %w", err) 325 | } 326 | 327 | beatMap.Difficulty.SetMods(difficulty.Modifier(memory.MenuData.Mods.AppliedMods)) 328 | attribs = osu.CalculateStep(beatMap.HitObjects, beatMap.Difficulty) 329 | } 330 | 331 | ppWieku := &osu.PPv2{} 332 | 333 | currAttrib := int(math.Max(0, float64(h300+h100+h50+h0-1))) 334 | 335 | if len(attribs)-1 < currAttrib { 336 | return 0, nil //rade condition hell 337 | } 338 | 339 | ppWieku.PPv2x(attribs[currAttrib], int(combo), int(h300), int(h100), int(h50), int(h0), beatMap.Difficulty) 340 | 341 | return cast.ToInt32(ppWieku.Results.Total), nil 342 | } 343 | -------------------------------------------------------------------------------- /updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/blang/semver" 11 | "github.com/rhysd/go-github-selfupdate/selfupdate" 12 | "github.com/skratchdot/open-golang/open" 13 | ) 14 | 15 | const version = "1.3.9" 16 | 17 | // DoSelfUpdate updates the application 18 | func DoSelfUpdate() { 19 | fmt.Println("Checking Updates... (can take some time if you have bad routing to GitHub)") 20 | name, err := os.Executable() 21 | if err != nil { 22 | log.Fatalln(err) 23 | } 24 | v := semver.MustParse(version) 25 | latest, err := selfupdate.UpdateSelf(v, "l3lackShark/gosumemory") 26 | if err != nil { 27 | log.Println("Binary update failed:", err) 28 | return 29 | } 30 | if latest.Version.Equals(v) { 31 | // latest version is the same as current version. It means current binary is up to date. 32 | log.Println("Current binary is the latest version", version) 33 | log.Println("Release notes:\n", latest.ReleaseNotes) 34 | full, _ := os.Executable() 35 | path, executable := filepath.Split(full) 36 | oldName := filepath.Join(path, "."+executable+".old") 37 | os.Remove(oldName) 38 | } else { 39 | log.Println("Successfully updated to version", latest.Version) 40 | log.Println("Release notes:\n", latest.ReleaseNotes) 41 | time.Sleep(3 * time.Second) 42 | open.Start(name) 43 | os.Exit(0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/structure.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/k0kubun/pp" 8 | "github.com/l3lackShark/gosumemory/memory" 9 | ) 10 | 11 | //SetupStructure sets up ws and json output 12 | func SetupStructure() { 13 | var err error 14 | type wsStruct struct { //order sets here 15 | A memory.InSettingsValues `json:"settings"` 16 | B memory.InMenuValues `json:"menu"` 17 | C memory.GameplayValues `json:"gameplay"` 18 | D memory.ResultsScreenValues `json:"resultsScreen"` 19 | E memory.TourneyValues `json:"tourney"` 20 | } 21 | for { 22 | group := wsStruct{ 23 | A: memory.SettingsData, 24 | B: memory.MenuData, 25 | C: memory.GameplayData, 26 | D: memory.ResultsScreenData, 27 | E: memory.TourneyData, 28 | } 29 | 30 | JSONByte, err = json.Marshal(group) 31 | if err != nil { 32 | pp.Println("JSON Marshall error: ", err, group) 33 | } 34 | time.Sleep(time.Duration(memory.UpdateTime) * time.Millisecond) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | "github.com/l3lackShark/gosumemory/config" 13 | "github.com/l3lackShark/gosumemory/memory" 14 | "github.com/spf13/cast" 15 | ) 16 | 17 | //JSONByte contains data that will be sent to the client 18 | var JSONByte []byte 19 | 20 | var upgrader = websocket.Upgrader{ 21 | CheckOrigin: func(r *http.Request) bool { 22 | return true 23 | }, 24 | } 25 | 26 | func reader(conn *websocket.Conn) { 27 | for { 28 | messageType, p, err := conn.ReadMessage() 29 | if err != nil { 30 | log.Println(err) 31 | return 32 | } 33 | fmt.Println(string(p)) 34 | 35 | if err := conn.WriteMessage(messageType, p); err != nil { 36 | log.Println(err) 37 | return 38 | } 39 | 40 | } 41 | } 42 | 43 | func wsEndpoint(w http.ResponseWriter, r *http.Request) { 44 | if cast.ToBool(config.Config["cors"]) { 45 | enableCors(&w) 46 | } 47 | ws, err := upgrader.Upgrade(w, r, nil) 48 | if err != nil { 49 | log.Println(err) 50 | } 51 | for { 52 | if memory.DynamicAddresses.IsReady == true { 53 | ws.WriteMessage(1, []byte(JSONByte)) //sending data to the client 54 | 55 | } 56 | time.Sleep(time.Duration(memory.UpdateTime) * time.Millisecond) 57 | } 58 | 59 | } 60 | 61 | //SetupRoutes creates websocket connection 62 | func SetupRoutes() { 63 | http.HandleFunc("/ws", wsEndpoint) 64 | } 65 | 66 | func enableCors(w *http.ResponseWriter) { 67 | (*w).Header().Set("Access-Control-Allow-Origin", "*") 68 | } 69 | 70 | //HTTPServer handles json and static files output 71 | func HTTPServer() { 72 | 73 | for memory.DynamicAddresses.IsReady != true { 74 | time.Sleep(100 * time.Millisecond) 75 | } 76 | ex, err := os.Executable() 77 | if err != nil { 78 | panic(err) 79 | } 80 | exPath := filepath.Dir(ex) 81 | fs := http.FileServer(http.Dir(filepath.Join(exPath, "static"))) 82 | 83 | http.Handle("/", fs) 84 | 85 | var songsOrigin = http.StripPrefix("/Songs/", http.FileServer(http.Dir(memory.SongsFolderPath))) 86 | var songsWrapped = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 | if cast.ToBool(config.Config["cors"]) { 88 | enableCors(&w) 89 | } 90 | songsOrigin.ServeHTTP(w, r) 91 | }) 92 | http.Handle("/Songs/", songsWrapped) 93 | 94 | http.HandleFunc("/json", handler) 95 | err = http.ListenAndServe(config.Config["serverip"], nil) 96 | if err != nil { 97 | fmt.Println(err) 98 | time.Sleep(5 * time.Second) 99 | log.Fatalln(err) 100 | } 101 | } 102 | func handler(w http.ResponseWriter, r *http.Request) { 103 | if memory.DynamicAddresses.IsReady == true { 104 | w.Header().Set("Content-Type", "application/json") 105 | fmt.Fprint(w, string(JSONByte)) 106 | 107 | } else { 108 | fmt.Fprintf(w, `{"error": "osu! is not fully loaded!"}`) 109 | } 110 | 111 | } 112 | --------------------------------------------------------------------------------