├── .gitignore ├── .vscode └── launch.json ├── CS2FontConfigurator.exe.manifest ├── README.md ├── build.bat ├── config.go ├── fontconfig_struct.go ├── fontconfig_write.go ├── gdi32.go ├── go.mod ├── go.sum ├── helper.go ├── icon.ico ├── main.go ├── main_test.go ├── rsrc.syso └── run.bat /.gitignore: -------------------------------------------------------------------------------- 1 | /Assets 2 | /test 3 | *.exe 4 | config.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Package", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "buildFlags": "-buildvcs=false", 10 | "program": "${workspaceFolder}" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /CS2FontConfigurator.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS2FontConfigurator 2 | 3 | [![Downloads][1]][2] [![GitHub stars][3]][4] 4 | 5 | [1]: https://img.shields.io/github/downloads/spddl/CS2FontConfigurator/total.svg 6 | [2]: https://github.com/spddl/CS2FontConfigurator/releases "Downloads" 7 | [3]: https://img.shields.io/github/stars/spddl/CS2FontConfigurator.svg 8 | [4]: https://github.com/spddl/CS2FontConfigurator/stargazers "GitHub stars" 9 | 10 | ### [Download](https://github.com/spddl/CS2FontConfigurator/releases) 11 | 12 | Like CSGO, CS2 uses [FontConfig](https://www.freedesktop.org/wiki/Software/fontconfig/) which is not widely used on Windows at all. 13 | It seems that it is not fully supported or maybe even modified. Since the configuration is simply wrong according to the [official documentation of Fontconfig](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user.html) 14 | 15 | ```conf 16 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\panorama\fonts\fonts.conf", line 39: unknown element "fontpattern" 17 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\panorama\fonts\fonts.conf", line 40: unknown element "fontpattern" 18 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\panorama\fonts\fonts.conf", line 41: unknown element "fontpattern" 19 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\panorama\fonts\fonts.conf", line 42: unknown element "fontpattern" 20 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\panorama\fonts\fonts.conf", line 43: unknown element "fontpattern" 21 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\panorama\fonts\fonts.conf", line 86: saw string, expected range 22 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\core\panorama\fonts\../../../core/panorama/fonts/conf.d/41-repl-os-win.conf", line 148: Having multiple values in isn't supported and may not work as expected 23 | Fontconfig warning: "C:\Steam\steamapps\common\Counter-Strike Global Offensive\game\core\panorama\fonts\../../../core/panorama/fonts/conf.d/41-repl-os-win.conf", line 160: Having multiple values in isn't supported and may not work as expected 24 | ``` 25 | 26 | It is hard to tell if this is planned or will change in the future. 27 | My solution is to fix the `fontconfig.conf` and apply my target font with an additional entry to all texts. 28 | No valve font will be deleted and a backup of the `fontconfig.conf` will be created first. 29 | 30 | ![screenshot](https://i.imgur.com/B2fZNWL.png) 31 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | SET GOOS=windows 4 | SET GOARCH=amd64 5 | set filename=CS2FontConfigurator 6 | 7 | :loop 8 | CLS 9 | 10 | gocritic check -enableAll -disable="#experimental,#opinionated,#commentedOutCode" ./... 11 | 12 | IF exist %filename%.exe ( 13 | FOR /F "usebackq" %%A IN ('%filename%.exe') DO SET /A beforeSize=%%~zA 14 | ) ELSE ( 15 | SET /A beforeSize=0 16 | ) 17 | 18 | rem rsrc.exe -manifest CS2FontConfigurator.exe.manifest -ico icon.ico 19 | 20 | :: Build https://golang.org/cmd/go/ 21 | go build -buildvcs=false -ldflags="-w -s -H windowsgui" -o %filename%.exe 22 | go build -buildvcs=false -o %filename%_debug.exe 23 | 24 | FOR /F "usebackq" %%A IN ('%filename%.exe') DO SET /A size=%%~zA 25 | SET /A diffSize = %size% - %beforeSize% 26 | SET /A size=(%size%/1024)+1 27 | IF %diffSize% EQU 0 ( 28 | ECHO %size% kb 29 | ) ELSE ( 30 | IF %diffSize% GTR 0 ( 31 | ECHO %size% kb [+%diffSize% b] 32 | ) ELSE ( 33 | ECHO %size% kb [%diffSize% b] 34 | ) 35 | ) 36 | 37 | :: Run 38 | IF %ERRORLEVEL% EQU 0 %filename%.exe 39 | 40 | PAUSE 41 | GOTO loop -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type Config struct { // https://mholt.github.io/json-to-go/ 12 | Path string `json:"path"` 13 | Font string `json:"font"` 14 | FontFile string `json:"fontfile"` 15 | Pixelsize float64 `json:"pixelsize"` 16 | TestCase string `json:"TestCase"` 17 | } 18 | 19 | func (c *Config) Init() { 20 | found, e := FileExists("./config.json") 21 | if e != nil { 22 | log.Printf("File error: %v\n", e) 23 | os.Exit(1) 24 | } 25 | 26 | if !found { 27 | c.newConfig() 28 | } else { 29 | c.LoadConfigFile() 30 | } 31 | } 32 | 33 | func (c *Config) LoadConfigFile() { 34 | file, e := os.ReadFile("./config.json") 35 | if e != nil { 36 | log.Printf("File error: %v\n", e) 37 | os.Exit(1) 38 | } 39 | 40 | err := json.Unmarshal(file, &c) 41 | if err != nil { 42 | log.Println(err) 43 | c.newConfig() 44 | } 45 | } 46 | 47 | func (c *Config) SaveConfigFile(apppath string) error { 48 | bytes, err := json.MarshalIndent(c, "", "\t") 49 | if err != nil { 50 | return errors.New("config.json could not be saved (JSON error)") 51 | } else { 52 | err = os.WriteFile(filepath.Join(apppath, "config.json"), bytes, 0o644) 53 | if err == nil { 54 | return nil 55 | } else { 56 | return err 57 | } 58 | } 59 | } 60 | 61 | // newConfig create a Config 62 | func (c *Config) newConfig() { 63 | jsonBlob := json.RawMessage(`{ 64 | "path": "", 65 | "font": "", 66 | "fontfile": "", 67 | "pixelsize": 1.0, 68 | "TestCase": "ABC 100 PLAY DUST II 98 in 4" 69 | }`) 70 | 71 | if err := json.Unmarshal(jsonBlob, c); err != nil { 72 | panic(err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /fontconfig_struct.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "encoding/xml" 4 | 5 | var header = []byte("\n\n") 6 | 7 | type Fontconfig struct { 8 | XMLName xml.Name `xml:"fontconfig"` 9 | Text string `xml:",chardata"` 10 | Dir []xDir `xml:"dir"` 11 | Fontpattern []string `xml:"fontpattern"` 12 | Cachedir []string `xml:"cachedir"` 13 | Selectfont *xSelectfont `xml:"selectfont"` 14 | Match []xMatch `xml:"match"` 15 | Include string `xml:"include,omitempty"` 16 | } 17 | 18 | type xSelectfont struct { 19 | Text string `xml:",chardata"` 20 | Rejectfont xRejectfont `xml:"rejectfont"` 21 | } 22 | 23 | type xRejectfont struct { 24 | Text string `xml:",chardata"` 25 | Pattern xPattern `xml:"pattern"` 26 | } 27 | 28 | type xPattern struct { 29 | Text string `xml:",chardata"` 30 | Patelt *xPatelt `xml:"patelt"` 31 | } 32 | 33 | type xPatelt struct { 34 | Text string `xml:",chardata"` 35 | Name string `xml:"name,attr"` 36 | String string `xml:"string"` 37 | } 38 | 39 | type xDir struct { 40 | Text string `xml:",chardata"` 41 | Prefix string `xml:"prefix,attr,omitempty"` 42 | } 43 | 44 | type xMatch struct { 45 | Text string `xml:",chardata"` 46 | Target string `xml:"target,attr,omitempty"` 47 | Test []xTest `xml:"test"` 48 | Edit []xEdit `xml:"edit"` 49 | } 50 | 51 | type xTest struct { 52 | Text string `xml:",chardata"` 53 | Qual string `xml:"qual,attr,omitempty"` 54 | Name string `xml:"name,attr,omitempty"` 55 | Target string `xml:"target,attr,omitempty"` 56 | Compare string `xml:"compare,attr,omitempty"` 57 | String string `xml:"string"` 58 | } 59 | 60 | type xEdit struct { 61 | Text string `xml:",chardata"` 62 | Name string `xml:"name,attr"` 63 | Mode string `xml:"mode,attr"` 64 | Binding string `xml:"binding,attr,omitempty"` 65 | Const string `xml:"const,omitempty"` 66 | String string `xml:"string,omitempty"` 67 | Int int `xml:"int,omitempty"` 68 | If *xIf `xml:"if"` 69 | Times *xTimes `xml:"times"` 70 | Minus *xMinus `xml:"minus"` 71 | Bool string `xml:"bool,omitempty"` 72 | Double string `xml:"double,omitempty"` 73 | } 74 | 75 | type xTimes struct { 76 | Text string `xml:",chardata"` 77 | Name string `xml:"name,omitempty"` 78 | Double string `xml:"double,omitempty"` 79 | } 80 | 81 | type xMinus struct { 82 | Text string `xml:",chardata"` 83 | Name string `xml:"name"` 84 | Charset xChatset `xml:"charset"` 85 | } 86 | 87 | type xChatset struct { 88 | Text string `xml:",chardata"` 89 | Int []string `xml:"int"` 90 | } 91 | 92 | type xIf struct { 93 | Text string `xml:",chardata"` 94 | Contains *xContains `xml:"contains,omitempty"` 95 | Int string `xml:"int,omitempty"` 96 | Name string `xml:"name,omitempty"` 97 | Or *xOr `xml:"or,omitempty"` 98 | Times []xTimes `xml:"times,omitempty"` 99 | } 100 | 101 | type xContains struct { 102 | Text string `xml:",chardata"` 103 | Name string `xml:"name"` 104 | String string `xml:"string"` 105 | } 106 | 107 | type xOr struct { 108 | Text string `xml:",chardata"` 109 | Contains xContains `xml:"contains"` 110 | LessEq xLessEq `xml:"less_eq"` 111 | } 112 | 113 | type xLessEq struct { 114 | Text string `xml:",chardata"` 115 | Name string `xml:"name"` 116 | Int string `xml:"int"` 117 | } 118 | -------------------------------------------------------------------------------- /fontconfig_write.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // https://www.freedesktop.org/software/fontconfig/fontconfig-user.html#AEN134 4 | 5 | import ( 6 | "encoding/xml" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func WriteFontsConf(c *Config) error { 13 | fontconfig := &Fontconfig{} 14 | 15 | fontconfig.Dir = []xDir{ 16 | {Prefix: "cwd", Text: "../../csgo/panorama/fonts"}, 17 | {Text: "WINDOWSFONTDIR"}, 18 | {Text: "~/.fonts"}, 19 | {Text: "/usr/share/fonts"}, 20 | {Text: "/usr/local/share/fonts"}, 21 | {Prefix: "xdg", Text: "fonts"}, 22 | } 23 | 24 | // fontconfig.Fontpattern = []string{ 25 | // "Arial", 26 | // ".uifont", 27 | // "notosans", 28 | // "notoserif", 29 | // "notomono-regular", 30 | // } 31 | 32 | fontconfig.Cachedir = []string{ 33 | "WINDOWSTEMPDIR_FONTCONFIG_CACHE", 34 | "~/.fontconfig", 35 | } 36 | 37 | targetMatch := xMatch{ 38 | Target: "pattern", 39 | Edit: []xEdit{ 40 | { 41 | Name: "family", 42 | Mode: "assign", 43 | Binding: "strong", 44 | String: config.Font, 45 | }, 46 | }, 47 | } 48 | 49 | if config.Pixelsize != 1.0 { 50 | targetMatch.Edit = append(targetMatch.Edit, xEdit{ 51 | Name: "pixelsize", 52 | Mode: "assign", 53 | Times: &xTimes{ 54 | Name: "pixelsize", 55 | Double: fmt.Sprintf("%g", config.Pixelsize), 56 | }, 57 | }) 58 | 59 | } 60 | 61 | fontconfig.Match = []xMatch{ 62 | { 63 | Test: []xTest{ 64 | { 65 | Name: "family", 66 | String: "Stratum2 Bold Monodigit", 67 | }, 68 | }, 69 | Edit: []xEdit{ 70 | { 71 | Name: "family", 72 | Mode: "append", 73 | Binding: "strong", 74 | String: "Stratum2", 75 | }, 76 | { 77 | Name: "style", 78 | Mode: "assign", 79 | Binding: "strong", 80 | String: "Bold", 81 | }, 82 | }, 83 | }, 84 | 85 | { 86 | Test: []xTest{ 87 | { 88 | Name: "family", 89 | String: "Stratum2 Regular Monodigit", 90 | }, 91 | }, 92 | Edit: []xEdit{ 93 | { 94 | Name: "family", 95 | Mode: "append", 96 | Binding: "strong", 97 | String: "Stratum2", 98 | }, 99 | { 100 | Name: "weight", 101 | Mode: "assign", 102 | Binding: "strong", 103 | // String: "Regular", 104 | Int: 90, 105 | }, 106 | }, 107 | }, 108 | 109 | { 110 | Test: []xTest{ 111 | { 112 | Name: "lang", 113 | String: "vi-vn", 114 | }, 115 | { 116 | Name: "family", 117 | Compare: "contains", 118 | String: "Stratum2", 119 | }, 120 | { 121 | Qual: "all", 122 | Name: "family", 123 | Compare: "not_contains", 124 | String: "TF", 125 | }, 126 | { 127 | Qual: "all", 128 | Name: "family", 129 | Compare: "not_contains", 130 | String: "Mono", 131 | }, 132 | { 133 | Qual: "all", 134 | Name: "family", 135 | Compare: "not_contains", 136 | String: "ForceStratum2", 137 | }, 138 | }, 139 | Edit: []xEdit{ 140 | { 141 | Name: "weight", 142 | Mode: "assign", 143 | If: &xIf{ 144 | Contains: &xContains{ 145 | Name: "family", 146 | String: "Stratum2 Black", 147 | }, 148 | Int: "210", 149 | Name: "weight", 150 | }, 151 | }, 152 | { 153 | Name: "slant", 154 | Mode: "assign", 155 | If: &xIf{ 156 | Contains: &xContains{ 157 | Name: "family", 158 | String: "Italic", 159 | }, 160 | Int: "100", 161 | Name: "slant", 162 | }, 163 | }, 164 | { 165 | Name: "pixelsize", 166 | Mode: "assign", 167 | If: &xIf{ 168 | Or: &xOr{ 169 | Contains: xContains{ 170 | Name: "family", 171 | String: "Condensed", 172 | }, 173 | LessEq: xLessEq{ 174 | Name: "width", 175 | Int: "75", 176 | }, 177 | }, 178 | Times: []xTimes{ 179 | { 180 | Name: "pixelsize", 181 | Double: "0.7", 182 | }, 183 | { 184 | Name: "pixelsize", 185 | Double: "0.9", 186 | }, 187 | }, 188 | }, 189 | }, 190 | { 191 | Name: "family", 192 | Mode: "assign", 193 | Binding: "same", 194 | String: "notosans", 195 | }, 196 | }, 197 | }, 198 | 199 | { 200 | Test: []xTest{ 201 | { 202 | Name: "lang", 203 | String: "vi-vn", 204 | }, 205 | 206 | { 207 | Name: "family", 208 | String: "ForceStratum2", 209 | }, 210 | }, 211 | Edit: []xEdit{ 212 | { 213 | Name: "family", 214 | Mode: "assign", 215 | Binding: "same", 216 | String: "Stratum2", 217 | }, 218 | }, 219 | }, 220 | 221 | { 222 | Target: "font", 223 | Test: []xTest{ 224 | { 225 | Name: "family", 226 | Target: "pattern", 227 | Compare: "contains", 228 | String: "Stratum2", 229 | }, 230 | { 231 | Name: "family", 232 | Target: "font", 233 | Compare: "contains", 234 | String: "Arial", 235 | }, 236 | }, 237 | Edit: []xEdit{ 238 | { 239 | Name: "pixelsize", 240 | Mode: "assign", 241 | Times: &xTimes{ 242 | Name: "pixelsize", 243 | Double: "0.9", 244 | }, 245 | }, 246 | }, 247 | }, 248 | 249 | { 250 | Target: "font", 251 | Test: []xTest{ 252 | { 253 | Name: "family", 254 | Target: "pattern", 255 | Compare: "contains", 256 | String: "Stratum2", 257 | }, 258 | { 259 | Name: "family", 260 | Target: "font", 261 | Compare: "contains", 262 | String: "Noto", 263 | }, 264 | }, 265 | Edit: []xEdit{ 266 | { 267 | Name: "pixelsize", 268 | Mode: "assign", 269 | Times: &xTimes{ 270 | Name: "pixelsize", 271 | Double: "0.9", 272 | }, 273 | }, 274 | }, 275 | }, 276 | 277 | { 278 | Target: "scan", 279 | Test: []xTest{ 280 | { 281 | Name: "family", 282 | String: "Stratum2", 283 | }, 284 | }, 285 | Edit: []xEdit{ 286 | { 287 | Name: "charset", 288 | Mode: "assign", 289 | Minus: &xMinus{ 290 | Name: "charset", 291 | Charset: xChatset{ 292 | Int: []string{ 293 | "0x0394", 294 | "0x03A9", 295 | "0x03BC", 296 | "0x03C0", 297 | "0x2202", 298 | "0x2206", 299 | "0x220F", 300 | "0x2211", 301 | "0x221A", 302 | "0x221E", 303 | "0x222B", 304 | "0x2248", 305 | "0x2260", 306 | "0x2264", 307 | "0x2265", 308 | "0x25CA", 309 | }, 310 | }, 311 | }, 312 | }, 313 | }, 314 | }, 315 | 316 | { 317 | Target: "font", 318 | Edit: []xEdit{ 319 | { 320 | Name: "embeddedbitmap", 321 | Mode: "assign", 322 | Bool: "false", 323 | }, 324 | }, 325 | }, 326 | 327 | { 328 | Target: "pattern", 329 | Edit: []xEdit{ 330 | { 331 | Name: "prefer_outline", 332 | Mode: "assign", 333 | Bool: "true", 334 | }, 335 | }, 336 | }, 337 | 338 | { 339 | Target: "pattern", 340 | Edit: []xEdit{ 341 | { 342 | Name: "do_substitutions", 343 | Mode: "assign", 344 | Bool: "true", 345 | }, 346 | }, 347 | }, 348 | 349 | { 350 | Target: "pattern", 351 | Edit: []xEdit{ 352 | { 353 | Name: "bitmap_monospace", 354 | Mode: "assign", 355 | Bool: "false", 356 | }, 357 | }, 358 | }, 359 | 360 | { 361 | Target: "font", 362 | Edit: []xEdit{ 363 | { 364 | Name: "force_autohint", 365 | Mode: "assign", 366 | Bool: "false", 367 | }, 368 | }, 369 | }, 370 | 371 | { 372 | Target: "pattern", 373 | Edit: []xEdit{ 374 | { 375 | Name: "dpi", 376 | Mode: "assign", 377 | Double: "96", 378 | }, 379 | }, 380 | }, 381 | 382 | { 383 | Target: "pattern", 384 | Edit: []xEdit{ 385 | { 386 | Name: "qt_use_subpixel_positioning", 387 | Mode: "assign", 388 | Bool: "false", 389 | }, 390 | }, 391 | }, 392 | 393 | targetMatch, 394 | } 395 | 396 | fontconfig.Selectfont = &xSelectfont{ 397 | Rejectfont: xRejectfont{ 398 | Pattern: xPattern{ 399 | Patelt: &xPatelt{ 400 | Name: "fontformat", 401 | String: "Type 1", 402 | }, 403 | }, 404 | }, 405 | } 406 | 407 | fontconfig.Include = `../../../core/panorama/fonts/conf.d` 408 | 409 | bytes, err := xml.MarshalIndent(fontconfig, "", "\t") 410 | if err != nil { 411 | return err 412 | } 413 | 414 | csgofontsDir := filepath.Join(c.Path, "game", "csgo", "panorama", "fonts") 415 | if exist, _ := FileExists(csgofontsDir); !exist { 416 | os.MkdirAll(csgofontsDir, os.ModePerm) 417 | } 418 | err = os.WriteFile(filepath.Join(csgofontsDir, "fonts.conf"), append(header, bytes...), 0o644) 419 | if err == nil { 420 | return nil 421 | } else { 422 | return err 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /gdi32.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "syscall" 6 | "unsafe" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | var ( 12 | // Library 13 | libgdi32 *windows.LazyDLL 14 | 15 | // Functions 16 | getFontResourceInfoW *windows.LazyProc 17 | ) 18 | 19 | func init() { 20 | // Library 21 | libgdi32 = windows.NewLazySystemDLL("gdi32.dll") 22 | 23 | // Functions 24 | getFontResourceInfoW = libgdi32.NewProc("GetFontResourceInfoW") 25 | } 26 | 27 | func GetFontResourceInfoW(str string, size *uint32, buffer uintptr, aType int) bool { // DWORD 28 | strStr, err := windows.UTF16FromString(str) 29 | if err != nil { 30 | log.Println(err) 31 | } 32 | 33 | ret1, _, _ := syscall.SyscallN(getFontResourceInfoW.Addr(), 34 | uintptr(unsafe.Pointer(&strStr[0])), 35 | uintptr(unsafe.Pointer(size)), 36 | buffer, 37 | uintptr(aType)) 38 | return ret1 != 0 39 | } 40 | 41 | const QFR_DESCRIPTION = 1 42 | 43 | func GetFontResourceInfo(font string) string { 44 | var size uint32 = 0 45 | if !GetFontResourceInfoW(font, &size, 0, QFR_DESCRIPTION) { 46 | return "" 47 | } 48 | 49 | buff := make([]uint16, size/2) 50 | if GetFontResourceInfoW(font, &size, uintptr(unsafe.Pointer(&buff[0])), QFR_DESCRIPTION) { 51 | return syscall.UTF16ToString(buff) 52 | } 53 | 54 | return "" 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spddl/CS2FontConfigurator 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/andygrunwald/vdf v1.1.0 7 | github.com/tailscale/walk v0.0.0-20231010163931-dff4ed649e49 8 | golang.org/x/image v0.23.0 9 | golang.org/x/sys v0.13.0 10 | ) 11 | 12 | require ( 13 | github.com/dblohm7/wingoes v0.0.0-20231010205534-fae7ee30e4f3 // indirect 14 | github.com/tailscale/win v0.0.0-20230710211752-84569fd814a9 // indirect 15 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 16 | golang.org/x/text v0.21.0 // indirect 17 | gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andygrunwald/vdf v1.1.0 h1:gmstp0R7DOepIZvWoSJY97ix7QOrsxpGPU6KusKXqvw= 2 | github.com/andygrunwald/vdf v1.1.0/go.mod h1:f31AAs7HOKvs5B167iwLHwKuqKc4bE46Vdt7xQogA0o= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/dblohm7/wingoes v0.0.0-20231010205534-fae7ee30e4f3 h1:IBsRi61P7H9oq7PbstdqWR7MYvfUNCoiqPNgpmeu1ug= 7 | github.com/dblohm7/wingoes v0.0.0-20231010205534-fae7ee30e4f3/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | github.com/tailscale/walk v0.0.0-20231010163931-dff4ed649e49 h1:7qa9LRxAibUcn+uitK+U1qNlRc0onAxG/AHU2eEVWF8= 16 | github.com/tailscale/walk v0.0.0-20231010163931-dff4ed649e49/go.mod h1:sw8avM/RSfMlVMwEsyCooMIoQdHstP8enCc/Gv+ArPQ= 17 | github.com/tailscale/win v0.0.0-20230710211752-84569fd814a9 h1:K3/RR+xb+WWRC19RSctFc+81gwA4+vlz+I9qME9asHg= 18 | github.com/tailscale/win v0.0.0-20230710211752-84569fd814a9/go.mod h1:bCmhgMXv5K6RcDeQFxOZWbZW18dKNLyihfZ5tzuJ0fk= 19 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 20 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 21 | golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= 22 | golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= 23 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 24 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 26 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 27 | gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= 28 | gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "slices" 8 | 9 | "github.com/andygrunwald/vdf" 10 | "golang.org/x/image/font/opentype" 11 | "golang.org/x/image/font/sfnt" 12 | "golang.org/x/sys/windows/registry" 13 | ) 14 | 15 | func CSPath() (string, error) { 16 | k, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Valve\Steam`, registry.READ) 17 | if err != nil { 18 | return "", err 19 | } 20 | defer k.Close() 21 | 22 | s, _, err := k.GetStringValue("SteamPath") 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | f, err := os.Open(filepath.Join(s, "SteamApps", "libraryfolders.vdf")) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | p := vdf.NewParser(f) 33 | file, err := p.Parse() 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | libraryfolders := GetVDFKey(file, "libraryfolders") 39 | for _, folders := range libraryfolders { 40 | apps := GetVDFKey(folders.(map[string]interface{}), "apps") 41 | for id := range apps { 42 | if id == "730" { 43 | if val, ok := folders.(map[string]interface{}); ok { 44 | if val, ok := val["path"]; ok { 45 | return filepath.Join(val.(string), "SteamApps", "common", "Counter-Strike Global Offensive"), nil 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | return "", nil 53 | } 54 | 55 | // https://golang.org/src/path/path.go?s=4371:4399#L158 56 | func FileExists(path string) (bool, error) { 57 | _, err := os.Stat(path) 58 | if err == nil { 59 | return true, nil 60 | } 61 | if os.IsNotExist(err) { 62 | return false, nil 63 | } 64 | return true, err 65 | } 66 | 67 | func GetVDFKey(data map[string]interface{}, key string) map[string]interface{} { 68 | if val, ok := data[key]; ok { 69 | if val, ok := val.(map[string]interface{}); ok { 70 | return val 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func (c *Config) ClearUpFontsFolder() { 77 | panorama_fonts := filepath.Join(c.Path, "game", "csgo", "panorama", "fonts") 78 | entries, err := os.ReadDir(panorama_fonts) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | valveFonts := []string{ 84 | "notomono-regular.ttf", 85 | "notosans-bold.ttf", 86 | "notosans-bolditalic.ttf", 87 | "notosans-italic.ttf", 88 | "notosans-regular.ttf", 89 | "notosansjp-bold.ttf", 90 | "notosansjp-light.ttf", 91 | "notosansjp-regular.ttf", 92 | "notosanskr-bold.ttf", 93 | "notosanskr-light.ttf", 94 | "notosanskr-regular.ttf", 95 | "notosanssc-bold.ttf", 96 | "notosanssc-light.ttf", 97 | "notosanssc-regular.ttf", 98 | "notosanssymbols-regular.ttf", 99 | "notosanstc-bold.ttf", 100 | "notosanstc-light.ttf", 101 | "notosanstc-regular.ttf", 102 | "notosansthai-bold.ttf", 103 | "notosansthai-light.ttf", 104 | "notosansthai-regular.ttf", 105 | "notoserif-bold.ttf", 106 | "notoserif-boliitalic.ttf", 107 | "notoserif-italic.ttf", 108 | "notoserif-regular.ttf", 109 | } 110 | 111 | for _, e := range entries { 112 | filename := e.Name() 113 | switch filepath.Ext(filename) { 114 | case ".ttf", ".otf": 115 | if !slices.Contains(valveFonts, filename) { 116 | if err := os.Remove(filepath.Join(panorama_fonts, filename)); err != nil { 117 | log.Println(err) 118 | } 119 | } 120 | case ".conf", ".uifont": 121 | // ignore 122 | case ".zip", ".7z", ".rar": 123 | // ignore 124 | default: 125 | if err := os.Remove(filepath.Join(panorama_fonts, filename)); err != nil { 126 | log.Println(err) 127 | } 128 | } 129 | } 130 | } 131 | 132 | func copyFile(in, out string) { 133 | data, err := os.ReadFile(in) 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | 138 | if err = os.WriteFile(out, data, 0o644); err != nil { 139 | log.Fatal(err) 140 | } 141 | } 142 | 143 | func GetFontName(fontfilename string) string { 144 | data, err := os.ReadFile(fontfilename) 145 | if err != nil { 146 | log.Fatal(err) 147 | } 148 | f, err := opentype.Parse(data) 149 | if err != nil { 150 | // Windows Fallback 151 | return GetFontResourceInfo(fontfilename) 152 | } else { 153 | fontname, err := f.Name(nil, sfnt.NameIDFamily) 154 | if err != nil { 155 | log.Fatal(err) 156 | } 157 | return fontname 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spddl/CS2FontConfigurator/cf034d4f4775e3d0686bf2c5f6bf08b7f62c55b3/icon.ico -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "slices" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/tailscale/walk" 13 | 14 | //lint:ignore ST1001 standard behavior tailscale/walk 15 | . "github.com/tailscale/walk/declarative" 16 | ) 17 | 18 | type MyMainWindow struct { 19 | *walk.Dialog 20 | cb *walk.ComboBox 21 | AppPath string 22 | } 23 | 24 | var ( 25 | config = new(Config) 26 | mw *MyMainWindow 27 | fontslist []*Fontslist // Windows 28 | ) 29 | 30 | func init() { 31 | log.SetFlags(log.LstdFlags | log.Lshortfile) 32 | config.Init() 33 | } 34 | 35 | type Fontslist struct { 36 | Id int 37 | Dir string 38 | Name string 39 | Filename string 40 | Fontname string 41 | } 42 | 43 | func main() { 44 | if config.Path == "" { 45 | CSGOPath, err := CSPath() 46 | if err != nil { 47 | walk.MsgBox(nil, "CS2 Path", err.Error(), walk.MsgBoxIconError) 48 | panic(err) 49 | } 50 | config.Path = CSGOPath 51 | } 52 | 53 | mw = new(MyMainWindow) 54 | mw.AppPath, _ = filepath.Abs("./") 55 | 56 | fontslist = []*Fontslist{} 57 | for _, dir := range []string{ 58 | ".", 59 | "C:\\Windows\\Fonts", 60 | filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local", "Microsoft", "Windows", "Fonts"), 61 | } { 62 | 63 | entries, err := os.ReadDir(dir) 64 | if err != nil { 65 | log.Println(err) 66 | continue 67 | } 68 | 69 | for _, e := range entries { 70 | filename := e.Name() 71 | 72 | if filepath.Ext(filename) != ".ttf" && filepath.Ext(filename) != ".otf" { 73 | continue 74 | } 75 | 76 | var fontname string = GetFontName(filepath.Join(dir, filename)) 77 | if fontname == "" { 78 | continue 79 | } 80 | 81 | fontslist = append(fontslist, &Fontslist{ 82 | Name: fmt.Sprintf("%s (%s)", fontname, filename), 83 | Dir: dir, 84 | Fontname: fontname, 85 | Filename: filename, 86 | }) 87 | } 88 | } 89 | 90 | sort.Slice(fontslist, func(i, j int) bool { 91 | if fontslist[i].Fontname != fontslist[j].Fontname { 92 | return fontslist[i].Fontname < fontslist[j].Fontname 93 | } 94 | return fontslist[i].Filename < fontslist[j].Filename 95 | }) 96 | 97 | // reduces fonts that are installed for the user and for the computer 98 | fontslist = slices.CompactFunc(fontslist, func(b *Fontslist, a *Fontslist) bool { 99 | return a.Name == b.Name 100 | }) 101 | 102 | // creates the index for the combo box 103 | for i := range fontslist { 104 | fontslist[i].Id = i 105 | } 106 | 107 | var GroupBoxSimpleSelection *walk.GroupBox 108 | var appIcon, _ = walk.NewIconFromResourceId(7) 109 | var testfont *walk.Label 110 | var testfontinput *walk.LineEdit 111 | var defaultButton *walk.PushButton 112 | var PixelsizeEdit *walk.NumberEdit 113 | 114 | if err := (Dialog{ 115 | AssignTo: &mw.Dialog, 116 | Title: "CS2FontConfigurator", 117 | Size: Size{Width: 1, Height: 1}, 118 | Icon: appIcon, 119 | Layout: Grid{Columns: 1}, 120 | Children: []Widget{ 121 | GroupBox{ 122 | AssignTo: &GroupBoxSimpleSelection, 123 | Layout: Grid{ 124 | Columns: 2, 125 | }, 126 | Title: "Simple font selection", 127 | Children: []Widget{ 128 | 129 | Label{ 130 | Text: "Font:", 131 | }, 132 | 133 | ComboBox{ 134 | AssignTo: &mw.cb, 135 | BindingMember: "Id", 136 | DisplayMember: "Name", 137 | Model: fontslist, 138 | OnCurrentIndexChanged: func() { 139 | font := fontslist[mw.cb.CurrentIndex()] 140 | 141 | config.Font = font.Fontname 142 | config.FontFile = font.Filename 143 | 144 | var style walk.FontStyle 145 | if strings.Contains(strings.ToLower(font.Name), "bold") { 146 | style |= walk.FontBold 147 | } 148 | if strings.Contains(strings.ToLower(font.Name), "italic") { 149 | style |= walk.FontItalic 150 | } 151 | walkfont, err := walk.NewFont(font.Fontname, 20, style) 152 | if err != nil { 153 | log.Println(err) 154 | } 155 | 156 | testfont.SetFont(walkfont) 157 | }, 158 | }, 159 | 160 | Label{ 161 | Text: "Size (1 is Default):", 162 | }, 163 | NumberEdit{ 164 | AssignTo: &PixelsizeEdit, 165 | MinValue: 0, 166 | Value: config.Pixelsize, 167 | Decimals: 2, 168 | Increment: 0.1, 169 | SpinButtonsVisible: true, 170 | OnValueChanged: func() { 171 | config.Pixelsize = PixelsizeEdit.Value() 172 | }, 173 | }, 174 | 175 | Label{ 176 | AssignTo: &testfont, 177 | Text: config.TestCase, 178 | ColumnSpan: 2, 179 | }, 180 | 181 | LineEdit{ 182 | AssignTo: &testfontinput, 183 | ColumnSpan: 2, 184 | Text: config.TestCase, 185 | OnKeyUp: func(key walk.Key) { 186 | testcase := testfontinput.Text() 187 | testfont.SetText(testcase) 188 | config.TestCase = testcase 189 | }, 190 | }, 191 | }, 192 | }, 193 | 194 | Composite{ 195 | Layout: HBox{ 196 | MarginsZero: true, 197 | }, 198 | Children: []Widget{ 199 | PushButton{ 200 | AssignTo: &defaultButton, 201 | Text: "Default", 202 | Enabled: false, 203 | OnClicked: func() { 204 | backupFile := filepath.Join(config.Path, "game", "csgo", "panorama", "fonts", "fonts_backup.conf") 205 | fontFile := filepath.Join(config.Path, "game", "csgo", "panorama", "fonts", "fonts.conf") 206 | 207 | if err := os.Remove(fontFile); err != nil { 208 | walk.MsgBox(mw, "Error", err.Error(), walk.MsgBoxOK|walk.MsgBoxIconError) 209 | panic(err) 210 | } 211 | 212 | if err := os.Rename(backupFile, fontFile); err != nil { 213 | walk.MsgBox(mw, "Error", err.Error(), walk.MsgBoxOK|walk.MsgBoxIconError) 214 | panic(err) 215 | } 216 | 217 | config.ClearUpFontsFolder() 218 | 219 | // Delete Cache 220 | os.RemoveAll(filepath.Join(os.Getenv("TEMP"), "fontconfig")) 221 | 222 | walk.MsgBox(mw, "FontConfig", "done.", walk.MsgBoxIconInformation) 223 | }, 224 | }, 225 | HSpacer{}, 226 | PushButton{ 227 | Text: "Apply", 228 | OnClicked: func() { 229 | // creates a backup of the font.conf if it does not already exist 230 | backupFile := filepath.Join(config.Path, "game", "csgo", "panorama", "fonts", "fonts_backup.conf") 231 | 232 | if exist, _ := FileExists(backupFile); !exist { 233 | fontFile := filepath.Join(config.Path, "game", "csgo", "panorama", "fonts", "fonts.conf") 234 | copyFile(fontFile, backupFile) 235 | defaultButton.SetEnabled(true) 236 | } 237 | 238 | config.ClearUpFontsFolder() 239 | 240 | font := fontslist[mw.cb.CurrentIndex()] 241 | copyFile(filepath.Join(font.Dir, font.Filename), filepath.Join(config.Path, "game", "csgo", "panorama", "fonts", font.Filename)) 242 | 243 | if err := WriteFontsConf(config); err != nil { 244 | walk.MsgBox(mw, "Error", err.Error(), walk.MsgBoxOK|walk.MsgBoxIconError) 245 | panic(err) 246 | } 247 | 248 | if err := config.SaveConfigFile(mw.AppPath); err != nil { 249 | walk.MsgBox(mw, "Error", err.Error(), walk.MsgBoxOK|walk.MsgBoxIconError) 250 | panic(err) 251 | } 252 | 253 | // Delete Cache 254 | os.RemoveAll(filepath.Join(os.Getenv("TEMP"), "fontconfig")) 255 | 256 | walk.MsgBox(mw, "FontConfig", "done.", walk.MsgBoxIconInformation) 257 | }, 258 | }, 259 | PushButton{ 260 | Text: "Exit", 261 | OnClicked: func() { 262 | os.Exit(0) 263 | }, 264 | }, 265 | }, 266 | }, 267 | }, 268 | }.Create(nil)); err != nil { 269 | panic(err) 270 | } 271 | 272 | // selects the font from the config 273 | if len(config.Font) == 0 { 274 | mw.cb.SetCurrentIndex(0) 275 | } else { 276 | for i, v := range fontslist { 277 | if v.Filename == config.FontFile { 278 | mw.cb.SetCurrentIndex(i) 279 | break 280 | } 281 | } 282 | } 283 | 284 | // Activates the default button if there is also a backup 285 | if exist, _ := FileExists(filepath.Join(config.Path, "game", "csgo", "panorama", "fonts", "fonts_backup.conf")); exist { 286 | defaultButton.SetEnabled(true) 287 | } 288 | 289 | mw.Run() 290 | } 291 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | var compareTests = []struct { 6 | have string 7 | want string 8 | }{ 9 | {"C:\\Windows\\Fonts\\OperatorMono-Bold.otf", "Operator Mono"}, 10 | {"C:\\Windows\\Fonts\\OperatorMono-BoldItalic.otf", "Operator Mono"}, 11 | {"C:\\Users\\spddl\\AppData\\Local\\Microsoft\\Windows\\Fonts\\FZCuYuan-M03.ttf", "FZCuYuan-M03"}, 12 | {"C:\\Windows\\Fonts\\DavidCLM-Medium.otf", "David CLM"}, 13 | {"C:\\Windows\\Fonts\\DejaVuSans-ExtraLight.ttf", "DejaVu Sans Light"}, 14 | {"C:\\Windows\\Fonts\\FiraCode-Bold.ttf", "Fira Code"}, 15 | {"C:\\Windows\\Fonts\\FrankRuehlCLM-Bold.ttf", "Frank Ruehl CLM"}, 16 | {"C:\\Windows\\Fonts\\HelveticaNeueBoldItalic.otf", "Helvetica Neue"}, 17 | {"C:\\Windows\\Fonts\\impact.ttf", "Impact"}, 18 | {"C:\\Windows\\Fonts\\lucon.ttf", "Lucida Console"}, 19 | {"C:\\Windows\\Fonts\\msyi.ttf", "Microsoft Yi Baiti"}, 20 | {"C:\\Windows\\Fonts\\Rubik-BoldItalic.ttf", "Rubik"}, 21 | {"C:\\Windows\\Fonts\\OperatorMonoLig-LightItalic.otf", "Operator Mono Lig"}, 22 | } 23 | 24 | func TestAbs(t *testing.T) { 25 | for _, test := range compareTests { 26 | got := GetFontName(test.have) 27 | if got != test.want { 28 | t.Errorf("%s != %s; %s", got, test.want, test.have) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rsrc.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spddl/CS2FontConfigurator/cf034d4f4775e3d0686bf2c5f6bf08b7f62c55b3/rsrc.syso -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set filename=CS2FontConfigurator 4 | 5 | :loop 6 | cls 7 | 8 | rem gocritic check -enableAll -disable="#experimental,#opinionated,#commentedOutCode" ./... 9 | go build -tags debug -buildvcs=false -o %filename%.exe 10 | 11 | IF %ERRORLEVEL% EQU 0 %filename%.exe 12 | 13 | pause 14 | goto loop --------------------------------------------------------------------------------