├── .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 | 
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
--------------------------------------------------------------------------------