├── .gitattributes ├── .gitignore ├── tests ├── numbered.md ├── pawn.md ├── lists.md ├── percent.md ├── numbered.bb ├── pawn.bb ├── lists.bb ├── percent.bb ├── comment.md ├── comment.bb ├── zones.md ├── full.md ├── zones.bb └── full.bb ├── go.mod ├── .goreleaser.yml ├── go.sum ├── main.go ├── yless.json ├── main_test.go ├── README.md ├── README.bb └── markdown └── markdown.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bb linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | forumfmt 2 | /forumfmt.exe 3 | *.test 4 | -------------------------------------------------------------------------------- /tests/numbered.md: -------------------------------------------------------------------------------- 1 | # numbered 2 | 3 | 1. first 4 | 2. second 5 | 3. third 6 | -------------------------------------------------------------------------------- /tests/pawn.md: -------------------------------------------------------------------------------- 1 | # pawn 2 | 3 | ```pawn 4 | main() { 5 | print("hello"); 6 | } 7 | ``` 8 | -------------------------------------------------------------------------------- /tests/lists.md: -------------------------------------------------------------------------------- 1 | # lists 2 | 3 | * item 1 4 | * sub 1.1 5 | * sub 1.2 6 | * item 2 7 | * sub 2.1 8 | * sub 2.2 9 | -------------------------------------------------------------------------------- /tests/percent.md: -------------------------------------------------------------------------------- 1 | # percent 2 | 3 | You should use `%e` instead of `%s` in SQL calls. 4 | 5 | ```pawn 6 | format(str, 128, "%s", s); 7 | ``` 8 | -------------------------------------------------------------------------------- /tests/numbered.bb: -------------------------------------------------------------------------------- 1 | [COLOR=#FF4700][SIZE=7][B]numbered[/B][/SIZE][/COLOR] 2 | 3 | [LIST=1] 4 | [*]first 5 | [*]second 6 | [*]third 7 | [/LIST] 8 | 9 | -------------------------------------------------------------------------------- /tests/pawn.bb: -------------------------------------------------------------------------------- 1 | [COLOR=#FF4700][SIZE=7][B]pawn[/B][/SIZE][/COLOR] 2 | 3 | [CODE] 4 | main() { 5 | print([COLOR=Purple]"hello"[/COLOR]); 6 | } 7 | [/CODE] 8 | 9 | -------------------------------------------------------------------------------- /tests/lists.bb: -------------------------------------------------------------------------------- 1 | [COLOR=#FF4700][SIZE=7][B]lists[/B][/SIZE][/COLOR] 2 | 3 | [LIST] 4 | [*]item 1 5 | 6 | [LIST] 7 | [*]sub 1.1 8 | [*]sub 1.2 9 | [/LIST] 10 | [*]item 2 11 | 12 | [LIST] 13 | [*]sub 2.1 14 | [*]sub 2.2 15 | [/LIST] 16 | [/LIST] 17 | 18 | -------------------------------------------------------------------------------- /tests/percent.bb: -------------------------------------------------------------------------------- 1 | [COLOR=#FF4700][SIZE=7][B]percent[/B][/SIZE][/COLOR] 2 | 3 | You should use [FONT=courier new]%e[/FONT] instead of [FONT=courier new]%s[/FONT] in SQL calls. 4 | 5 | [CODE] 6 | format(str, [COLOR=Purple]128[/COLOR], [COLOR=Purple]"%s"[/COLOR], s); 7 | [/CODE] 8 | 9 | -------------------------------------------------------------------------------- /tests/comment.md: -------------------------------------------------------------------------------- 1 | ```pawn 2 | // this is a nice piece of code 3 | //don't you agree? 4 | if (houseid != INVALID_HOUSE_ID) { // if the player is inside a house, get the exterior location of the house 5 | else if (!GetPlayerPos(playerid, x, y, z)) { //the player isn't connected, presuming that GetPlayerHouseID returns INVALID_HOUSE_ID in that case 6 | else if {//some other case 7 | else {// otherwise 8 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Southclaws/forumfmt 2 | 3 | require ( 4 | github.com/Jeffail/gabs v1.1.1 5 | github.com/davecgh/go-spew v1.1.1 // indirect 6 | github.com/pmezard/go-difflib v1.0.0 7 | github.com/russross/blackfriday v2.0.0+incompatible 8 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect 9 | github.com/stretchr/testify v1.2.2 10 | github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945 11 | golang.org/x/net v0.0.0-20181017193950-04a2e542c03f 12 | ) 13 | -------------------------------------------------------------------------------- /tests/comment.bb: -------------------------------------------------------------------------------- 1 | [CODE] 2 | [COLOR=Green]// this is a nice piece of code[/COLOR] 3 | [COLOR=Green]//don't you agree?[/COLOR] 4 | [COLOR=Blue]if[/COLOR] (houseid != INVALID_HOUSE_ID) { [COLOR=Green]// if the player is inside a house, get the exterior location of the house[/COLOR] 5 | [COLOR=Blue]else[/COLOR] [COLOR=Blue]if[/COLOR] (!GetPlayerPos(playerid, x, y, z)) { [COLOR=Green]//the player isn't connected, presuming that GetPlayerHouseID returns INVALID_HOUSE_ID in that case [/COLOR] 6 | [COLOR=Blue]else[/COLOR] [COLOR=Blue]if[/COLOR] {[COLOR=Green]//some other case[/COLOR] 7 | [COLOR=Blue]else[/COLOR] {[COLOR=Green]// otherwise[/COLOR] 8 | [/CODE] 9 | 10 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: forumfmt 2 | release: 3 | github: 4 | owner: Southclaws 5 | name: forumfmt 6 | name_template: "{{.Tag}}" 7 | brew: 8 | commit_author: 9 | name: goreleaserbot 10 | email: goreleaser@carlosbecker.com 11 | install: bin.install "forumfmt" 12 | builds: 13 | - goos: 14 | - linux 15 | - darwin 16 | - windows 17 | goarch: 18 | - amd64 19 | - "386" 20 | goarm: 21 | - "6" 22 | main: . 23 | ldflags: 24 | -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X 25 | main.date={{.Date}} 26 | binary: forumfmt 27 | archive: 28 | format: tar.gz 29 | name_template: 30 | "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm 31 | }}{{ end }}" 32 | files: 33 | - README.md 34 | - "*.json" 35 | snapshot: 36 | name_template: SNAPSHOT-{{ .Commit }} 37 | checksum: 38 | name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" 39 | dist: dist 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E= 2 | github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= 8 | github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 9 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= 10 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 11 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 12 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 13 | github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945 h1:6Ju8pZBYFTN9FaV/JvNBiIHcsgEmP4z4laciqjfjY8E= 14 | github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945/go.mod h1:4vRFPPNYllgCacoj+0FoKOjTW68rUhEfqPLiEJaK2w8= 15 | golang.org/x/net v0.0.0-20181017193950-04a2e542c03f h1:4pRM7zYwpBjCnfA1jRmhItLxYJkaEnsmuAcRtA347DA= 16 | golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/Jeffail/gabs" 9 | 10 | "github.com/Southclaws/forumfmt/markdown" 11 | ) 12 | 13 | func main() { 14 | var ( 15 | input *os.File 16 | output *os.File 17 | outputFile string 18 | styler string 19 | err error 20 | jsonParsed *gabs.Container 21 | ) 22 | 23 | forum := flag.String("forum", "default", "-forum ") 24 | 25 | flag.Parse() 26 | switch flag.NArg() { 27 | case 0: 28 | input = os.Stdin 29 | case 1, 2: 30 | input, err = os.Open(flag.Arg(0)) 31 | if err != nil { 32 | fmt.Println("failed to open input file:", err) 33 | } 34 | outputFile = flag.Arg(1) 35 | case 3: 36 | input, err = os.Open(flag.Arg(0)) 37 | if err != nil { 38 | fmt.Println("failed to open input file:", err) 39 | } 40 | outputFile = flag.Arg(1) 41 | styler = flag.Arg(2) 42 | default: 43 | fmt.Printf("input must be from stdin or file\n") 44 | os.Exit(1) 45 | } 46 | 47 | if outputFile == "" { 48 | output = os.Stdout 49 | } else { 50 | output, err = os.Create(outputFile) 51 | if err != nil { 52 | fmt.Println("failed to open output file:", err) 53 | return 54 | } 55 | defer func() { 56 | err = output.Close() 57 | if err != nil { 58 | fmt.Println("failed to close output file:", err) 59 | } 60 | }() 61 | } 62 | 63 | var tags string 64 | if *forum == "mybb" { 65 | tags = markdown.TagsMyBB 66 | } else { 67 | tags = markdown.TagsDefault 68 | } 69 | 70 | jsonParsed, err = markdown.ParseStyles(styler, tags) 71 | if err != nil { 72 | fmt.Println("failed to process styles:", err) 73 | return 74 | } 75 | 76 | err = markdown.Process(input, output, jsonParsed) 77 | if err != nil { 78 | fmt.Println("failed to process input:", err) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /yless.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "H1": "[CENTER][COLOR=Red][SIZE=8][B][u][COLOR=Green]%s[/COLOR][/u][/B][/SIZE][/COLOR][/CENTER]", 4 | "H2": "[COLOR=Red][SIZE=6][B][u][COLOR=Green]%s[/COLOR][/u][/B][/SIZE][/COLOR]", 5 | "H3": "[COLOR=Red][SIZE=4][B][u][COLOR=Green]%s[/COLOR][/u][/B][/SIZE][/COLOR]", 6 | "H4": "[LIST][*][COLOR=Red][B][u][COLOR=Green]%s[/COLOR][/u][/B][/COLOR][/LIST]" 7 | }, 8 | "keywords": { 9 | "yield": "[COLOR=Blue]$0[/COLOR]", 10 | "return": "[COLOR=Blue]$0[/COLOR]", 11 | "foreach": "[COLOR=Blue]$0[/COLOR]", 12 | "if": "[COLOR=Blue]$0[/COLOR]", 13 | "do": "[COLOR=Blue]$0[/COLOR]", 14 | "while": "[COLOR=Blue]$0[/COLOR]", 15 | "else": "[COLOR=Blue]$0[/COLOR]", 16 | "for": "[COLOR=Blue]$0[/COLOR]", 17 | "new": "[COLOR=Blue]$0[/COLOR]", 18 | "defer": "[COLOR=Blue]$0[/COLOR]", 19 | "stop": "[COLOR=Blue]$0[/COLOR]", 20 | "repeat": "[COLOR=Blue]$0[/COLOR]", 21 | "break": "[COLOR=Blue]$0[/COLOR]", 22 | "continue": "[COLOR=Blue]$0[/COLOR]", 23 | "state": "[COLOR=Blue]$0[/COLOR]", 24 | "call": "[COLOR=Blue]$0[/COLOR]", 25 | 26 | "stock": "[COLOR=DeepSkyBlue]$0[/COLOR]", 27 | "public": "[COLOR=DeepSkyBlue]$0[/COLOR]", 28 | "forward": "[COLOR=DeepSkyBlue]$0[/COLOR]", 29 | "const": "[COLOR=DeepSkyBlue]$0[/COLOR]", 30 | "static": "[COLOR=DeepSkyBlue]$0[/COLOR]", 31 | "hook": "[COLOR=DeepSkyBlue]$0[/COLOR]", 32 | "global": "[COLOR=DeepSkyBlue]$0[/COLOR]", 33 | "foreign": "[COLOR=DeepSkyBlue]$0[/COLOR]", 34 | "timer": "[COLOR=DeepSkyBlue]$0[/COLOR]", 35 | "native": "[COLOR=DeepSkyBlue]$0[/COLOR]", 36 | "iterfunc": "[COLOR=DeepSkyBlue]$0[/COLOR]" 37 | }, 38 | "numbers": "[COLOR=Orange]$0[/COLOR]", 39 | "directives": "[COLOR=Brown]$0[/COLOR]", 40 | "operators": "[COLOR=Red]$0[/COLOR]", 41 | "strings": "[COLOR=Red]$0[/COLOR]", 42 | "comment_open": "[COLOR=Green]", 43 | "comment_close": "[/COLOR]" 44 | } 45 | 46 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/pmezard/go-difflib/difflib" 12 | 13 | "github.com/Jeffail/gabs" 14 | "github.com/stretchr/testify/assert" 15 | 16 | "github.com/Southclaws/forumfmt/markdown" 17 | ) 18 | 19 | func Test_process(t *testing.T) { 20 | files, err := ioutil.ReadDir("tests") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | for _, file := range files { 26 | if filepath.Ext(file.Name()) != ".md" { 27 | continue 28 | } 29 | 30 | mdFile := filepath.Join("tests", file.Name()) 31 | bbFile := strings.TrimSuffix(mdFile, filepath.Ext(file.Name())) + ".bb" 32 | 33 | input, err := os.Open(mdFile) 34 | if err != nil { 35 | panic(err) 36 | } 37 | output := bytes.NewBuffer(nil) 38 | wantOutput, err := ioutil.ReadFile(bbFile) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | jsonParsed, _ := gabs.ParseJSON([]byte(markdown.DefaultSyntax)) 44 | 45 | err = markdown.Process(input, output, jsonParsed) 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | 50 | if string(wantOutput) != output.String() { 51 | d, _ := difflib.GetContextDiffString(difflib.ContextDiff{ 52 | A: strings.Split(string(wantOutput), "\n"), 53 | B: strings.Split(output.String(), "\n"), 54 | }) 55 | t.Log(d) 56 | t.Fail() 57 | } 58 | } 59 | } 60 | 61 | func Test_syntax(t *testing.T) { 62 | type args struct { 63 | in string 64 | } 65 | tests := []struct { 66 | name string 67 | args args 68 | want string 69 | }{ 70 | {"percent", args{`printf("%s", str);`}, `printf([COLOR=Purple]"%s"[/COLOR], str);` + "\n"}, 71 | } 72 | jsonParsed, _ := gabs.ParseJSON([]byte(markdown.DefaultSyntax)) 73 | 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | got := markdown.Syntax(tt.args.in, jsonParsed) 77 | assert.Equal(t, tt.want, got) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # forumfmt 2 | 3 | [![https://img.shields.io/badge/star_on-GitHub-lightgrey.svg](https://img.shields.io/badge/star_on-GitHub-lightgrey.svg)](https://github.com/Southclaws/forumfmt) 4 | 5 | Maintaining documentation is already difficult, maintaining it on two different 6 | platforms in two different formats is just annoying. 7 | 8 | ## Overview 9 | 10 | This tool means you can simply have a single markdown readme file in your 11 | project's repo and when you post it to the forums or update the topic, all you 12 | need to do is simply run this tool over the markdown text to generate BBCode. 13 | 14 | For example, this: 15 | 16 | ```markdown 17 | The Swiss Army Knife of SA:MP - vital tools for any server owner or library 18 | maintainer. 19 | 20 | ## Overview 21 | 22 | Server management and configuration tools: 23 | 24 | * Manage your server settings in JSON format (compiles to server.cfg) 25 | * Run the server from `sampctl` and let it worry about automatic restarts 26 | * Automatically download Windows/Linux server binaries when you need them 27 | ``` 28 | 29 | becomes this: 30 | 31 | ```json 32 | The Swiss Army Knife of SA:MP - vital tools for any server owner or library maintainer. 33 | 34 | [COLOR=RoyalBlue][size=6][B]Overview[/B][/size][/COLOR] 35 | 36 | Server management and configuration tools: 37 | 38 | [LIST] 39 | 40 | [*]Manage your server settings in JSON format (compiles to server.cfg) 41 | 42 | [*]Run the server from [FONT=courier new]sampctl[/FONT] and let it worry about automatic restarts 43 | 44 | [*]Automatically download Windows/Linux server binaries when you need them 45 | 46 | [/LIST] 47 | ``` 48 | 49 | And, as you can probably guess by now, this topic was generated using the tool! 50 | 51 | ## Installation 52 | 53 | The app is a simple Go app so just `go get` it: 54 | 55 | ```bash 56 | go get github.com/Southclaws/forumfmt 57 | ``` 58 | 59 | If you don't have Go installed, there are precompiled binaries available 60 | [on the releases page](https://github.com/Southclaws/forumfmt/releases). 61 | 62 | ## Usage 63 | 64 | Then you can use the command, either by passing input and output files as an 65 | argument: 66 | 67 | ```bash 68 | forumfmt README.md README.bbcode 69 | ``` 70 | 71 | Or by piping to stdin and/or stdout on Unix platforms: 72 | 73 | ```bash 74 | cat README.md | forumfmt > README.bbcode 75 | ``` 76 | 77 | You can also specify a style file to use, to determine the forum look, but only 78 | when all parameters are given: 79 | 80 | ```bash 81 | forumfmt README.md README.bbcode southclaws 82 | ``` 83 | 84 | The available styles are: 85 | 86 | * `southclaws` 87 | * `yless` 88 | 89 | Feel free to PR more styles if you want, just copy the existing `.json` files. 90 | -------------------------------------------------------------------------------- /README.bb: -------------------------------------------------------------------------------- 1 | [COLOR=#FF4700][SIZE=7][B]forumfmt[/B][/SIZE][/COLOR] 2 | 3 | [URL=https://github.com/Southclaws/forumfmt][IMG]https://img.shields.io/badge/star_on-GitHub-lightgrey.svg[/IMG][/URL] 4 | 5 | Maintaining documentation is already difficult, maintaining it on two different platforms in two different formats is just annoying. 6 | 7 | [COLOR=RoyalBlue][SIZE=6][B]Overview[/B][/SIZE][/COLOR] 8 | 9 | This tool means you can simply have a single markdown readme file in your project’s repo and when you post it to the forums or update the topic, all you need to do is simply run this tool over the markdown text to generate BBCode. 10 | 11 | For example, this: 12 | 13 | [CODE] 14 | The Swiss Army Knife of SA:MP - vital tools for any server owner or library 15 | maintainer. 16 | 17 | ## Overview 18 | 19 | Server management and configuration tools: 20 | 21 | * Manage your server settings in JSON format (compiles to server.cfg) 22 | * Run the server from `sampctl` and let it worry about automatic restarts 23 | * Automatically download Windows/Linux server binaries when you need them 24 | [/CODE] 25 | 26 | becomes this: 27 | 28 | [PHP] 29 | The Swiss Army Knife of SA:MP - vital tools for any server owner or library maintainer. 30 | 31 | [COLOR=RoyalBlue][size=6][B]Overview[/B][/size][/COLOR] 32 | 33 | Server management and configuration tools: 34 | 35 | [LIST] 36 | 37 | [*]Manage your server settings in JSON format (compiles to server.cfg) 38 | 39 | [*]Run the server from [FONT=courier new]sampctl[/FONT] and let it worry about automatic restarts 40 | 41 | [*]Automatically download Windows/Linux server binaries when you need them 42 | 43 | [/LIST] 44 | [/PHP] 45 | 46 | And, as you can probably guess by now, this topic was generated using the tool! 47 | 48 | [COLOR=RoyalBlue][SIZE=6][B]Installation[/B][/SIZE][/COLOR] 49 | 50 | The app is a simple Go app so just [FONT=courier new]go get[/FONT] it: 51 | 52 | [CODE] 53 | go get github.com/Southclaws/forumfmt 54 | [/CODE] 55 | 56 | If you don’t have Go installed, there are precompiled binaries available [URL=https://github.com/Southclaws/forumfmt/releases]on the releases page[/URL]. 57 | 58 | [COLOR=RoyalBlue][SIZE=6][B]Usage[/B][/SIZE][/COLOR] 59 | 60 | Then you can use the command, either by passing input and output files as an argument: 61 | 62 | [CODE] 63 | forumfmt README.md README.bbcode 64 | [/CODE] 65 | 66 | Or by piping to stdin and/or stdout on Unix platforms: 67 | 68 | [CODE] 69 | cat README.md | forumfmt > README.bbcode 70 | [/CODE] 71 | 72 | You can also specify a style file to use, to determine the forum look, but only when all parameters are given: 73 | 74 | [CODE] 75 | forumfmt README.md README.bbcode southclaws 76 | [/CODE] 77 | 78 | The available styles are: 79 | 80 | [LIST] 81 | [*][FONT=courier new]southclaws[/FONT] 82 | [*][FONT=courier new]yless[/FONT] 83 | [/LIST] 84 | 85 | Feel free to PR more styles if you want, just copy the existing [FONT=courier new].json[/FONT] files. 86 | 87 | -------------------------------------------------------------------------------- /tests/zones.md: -------------------------------------------------------------------------------- 1 | # SA-MP Map Zones 2 | 3 | [![sampctl](https://shields.southcla.ws/badge/sampctl-samp--map--zones-2f2f2f.svg?style=for-the-badge)](https://github.com/kristoisberg/samp-map-zones) 4 | 5 | This library does not bring anything gamechanging to the table, it's created to 6 | stop a decade long era of bad practices regarding map zones. An array of ~350 7 | zones dumped (or manually converted?) from the game has been around for such a 8 | long time, but in that time I've never seen a satisfactory API for them. Let's 9 | look at an implementation from Emmet\_'s South Central Roleplay. 10 | 11 | ```pawn 12 | stock GetLocation(Float:fX, Float:fY, Float:fZ) 13 | { 14 | enum e_ZoneData 15 | { 16 | e_ZoneName[32 char], 17 | Float:e_ZoneArea[6] 18 | }; 19 | new const g_arrZoneData[][e_ZoneData] = 20 | { 21 | // ... 22 | }; 23 | new 24 | name[32] = "San Andreas"; 25 | 26 | for (new i = 0; i != sizeof(g_arrZoneData); i ++) 27 | { 28 | if ( 29 | (fX >= g_arrZoneData[i][e_ZoneArea][0] && fX <= g_arrZoneData[i][e_ZoneArea][3]) && 30 | (fY >= g_arrZoneData[i][e_ZoneArea][1] && fY <= g_arrZoneData[i][e_ZoneArea][4]) && 31 | (fZ >= g_arrZoneData[i][e_ZoneArea][2] && fZ <= g_arrZoneData[i][e_ZoneArea][5])) 32 | { 33 | strunpack(name, g_arrZoneData[i][e_ZoneName]); 34 | 35 | break; 36 | } 37 | } 38 | return name; 39 | } 40 | 41 | stock GetPlayerLocation(playerid) 42 | { 43 | new 44 | Float:fX, 45 | Float:fY, 46 | Float:fZ, 47 | string[32], 48 | id = -1; 49 | 50 | if ((id = House_Inside(playerid)) != -1) 51 | { 52 | fX = HouseData[id][housePos][0]; 53 | fY = HouseData[id][housePos][1]; 54 | fZ = HouseData[id][housePos][2]; 55 | } 56 | // ... 57 | else GetPlayerPos(playerid, fX, fY, fZ); 58 | 59 | format(string, 32, GetLocation(fX, fY, fZ)); 60 | return string; 61 | } 62 | ``` 63 | 64 | ![emmetemmet](https://i.imgur.com/cyUdlu4.png "Emmet Emmet") 65 | 66 | If you didn't get the reference, you should probably check out 67 | [this repository](https://github.com/sampctl/pawn-array-return-bug). 68 | `GetPlayerLocation` most likely uses `format` to prevent this bug from 69 | occurring, but the risk is still there and arrays should never be returned in 70 | PAWN. Let's take a look at another implementation that even I used a long time 71 | ago. 72 | 73 | ```pawn 74 | stock GetPointZone(Float:x, Float:y, Float:z, zone[] = "San Andreas", len = sizeof(zone)) 75 | { 76 | for (new i, j = sizeof(Zones); i < j; i++) 77 | { 78 | if (x >= Zones[i][zArea][0] && x <= Zones[i][zArea][3] && y >= Zones[i][zArea][1] && y <= Zones[i][zArea][4] && z >= Zones[i][zArea][2] && z <= Zones[i][zArea][5]) 79 | { 80 | strunpack(zone, Zones[i][zName], len); 81 | return 1; 82 | } 83 | } 84 | return 1; 85 | } 86 | 87 | stock GetPlayerZone(playerid, zone[], len = sizeof(zone)) 88 | { 89 | new Float:pos[3]; 90 | GetPlayerPos(playerid, pos[0], pos[1], pos[2]); 91 | 92 | for (new i, j = sizeof(Zones); i < j; i++) 93 | { 94 | if (x >= Zones[i][zArea][0] && x <= Zones[i][zArea][3] && y >= Zones[i][zArea][1] && y <= Zones[i][zArea][4] && z >= Zones[i][zArea][2] && z <= Zones[i][zArea][5]) 95 | { 96 | strunpack(zone, Zones[i][zName], len); 97 | return 1; 98 | } 99 | } 100 | return 1; 101 | } 102 | ``` 103 | 104 | First of all, what do we see? A lot of code repetition. That's easy to fix in 105 | this case, but what if we also needed either the min/max position of the zone? 106 | We'd have to loop through the zones again or take a different approach. Which 107 | approach does this library take? Functions like `GetMapZoneAtPoint` and 108 | `GetPlayerMapZone` do not return the name of the zone, they return an 109 | identificator of it. The name or positions of the zone must be fetched using 110 | another function. In addition to that, I rebuilt the array of zones myself since 111 | the one used basically everywhere seems to be faulty according to 112 | [this post](https://forum.sa-mp.com/showpost.php?p=4050745&postcount=7). 113 | 114 | ## Installation 115 | 116 | Simply install to your project: 117 | 118 | ```bash 119 | sampctl package install kristoisberg/samp-map-zones 120 | ``` 121 | 122 | Include in your code and begin using the library: 123 | 124 | ```pawn 125 | #include 126 | ``` 127 | 128 | ## Usage 129 | 130 | ### Constants 131 | 132 | - `INVALID_MAP_ZONE_ID = MapZone:-1` 133 | - The return value of several functions when no map zone was matching the 134 | criteria. 135 | - `MAX_MAP_ZONE_NAME = 27` 136 | - The length of the longest map zone name including the null character. 137 | - `MAX_MAP_ZONE_AREAS = 13` 138 | - The most areas associated with a map zone. 139 | 140 | ### Functions 141 | 142 | - `MapZone:GetMapZoneAtPoint(Float:x, Float:y, Float:z)` 143 | - Returns the ID of the map zone the point is in or `INVALID_MAP_ZONE_ID` if 144 | it isn't in any. Alias: `GetMapZoneAtPoint3D`. 145 | - `MapZone:GetPlayerMapZone(playerid)` 146 | - Returns the ID of the map zone the player is in or `INVALID_MAP_ZONE_ID` if 147 | it isn't in any. Alias: `GetPlayerMapZone3D`. 148 | - `MapZone:GetVehicleMapZone(vehicleid)` 149 | - Returns the ID of the map zone the vehicle is in or `INVALID_MAP_ZONE_ID` if 150 | it isn't in any. Alias: `GetVehicleMapZone3D`. 151 | - `MapZone:GetMapZoneAtPoint2D(Float:x, Float:y)` 152 | - Returns the ID of the map zone the point is in or `INVALID_MAP_ZONE_ID` if 153 | it isn't in any. Does not check the Z-coordinate. 154 | - `MapZone:GetPlayerMapZone2D(playerid)` 155 | - Returns the ID of the map zone the player is in or `INVALID_MAP_ZONE_ID` if 156 | it isn't in any. Does not check the Z-coordinate. 157 | - `MapZone:GetVehicleMapZone2D(vehicleid)` 158 | - Returns the ID of the map zone the vehicle is in or `INVALID_MAP_ZONE_ID` if 159 | it isn't in any. Does not check the Z-coordinate. 160 | - `bool:IsValidMapZone(MapZone:id)` 161 | - Returns `true` or `false` depending on if the map zone is valid or not. 162 | - `bool:GetMapZoneName(MapZone:id, name[], size = sizeof(name))` 163 | - Retrieves the name of the map zone. Returns `true` or `false` depending on 164 | if the map zone is valid or not. 165 | - `bool:GetMapZoneSoundID(MapZone:id, &soundid)` 166 | - Retrieves the sound ID of the map zone. Returns `true` or `false` depending 167 | on if the map zone is valid or not. 168 | - `bool:GetMapZoneAreaCount(MapZone:id, &count)` 169 | - Retrieves the count of areas associated with the map zone. Returns `true` or 170 | `false` depending on if the map zone is valid or not. 171 | - `GetMapZoneAreaPos(MapZone:id, &Float:minX = 0.0, &Float:minY = 0.0, &Float:minZ = 0.0, &Float:maxX = 0.0, &Float:maxY = 0.0, &Float:maxZ = 0.0, start = 0)` 172 | - Retrieves the coordinates of an area associated with the map zone. Returns 173 | the array index for the area or `-1` if none were found. See the usage in 174 | in the examples section. 175 | - `GetMapZoneCount()` 176 | - Returns the count of map zones in the array. Could be used for iteration 177 | purposes. 178 | 179 | ## Examples 180 | 181 | ### Retrieving the location of a player 182 | 183 | ```pawn 184 | CMD:whereami(playerid) { 185 | new MapZone:zone = GetPlayerMapZone(playerid); 186 | 187 | if (zone == INVALID_MAP_ZONE_ID) { 188 | return SendClientMessage(playerid, 0xFFFFFFFF, "probably in the ocean, mate"); 189 | } 190 | 191 | new name[MAX_MAP_ZONE_NAME], soundid; 192 | GetMapZoneName(zone, name); 193 | GetMapZoneSoundID(zone, soundid); 194 | 195 | new string[128]; 196 | format(string, sizeof(string), "you are in %s", name); 197 | 198 | SendClientMessage(playerid, 0xFFFFFFFF, string); 199 | PlayerPlaySound(playerid, soundid, 0.0, 0.0, 0.0); 200 | return 1; 201 | } 202 | ``` 203 | 204 | ### Iterating through areas associated with a map zone 205 | 206 | ```pawn 207 | new zone = ZONE_RICHMAN, index = -1, Float:minX, Float:minY, Float:minZ, Float:maxX, Float:maxY, Float:maxZ; 208 | 209 | while ((index = GetMapZoneAreaPos(zone, minX, minY, minZ, maxX, maxY, maxZ, index + 1) != -1) { 210 | printf("%f %f %f %f %f %f", minX, minY, minZ, maxX, maxY, maxZ); 211 | } 212 | ``` 213 | 214 | ### Extending 215 | 216 | ```pawn 217 | stock MapZone:GetPlayerOutsideMapZone(playerid) { 218 | new House:houseid = GetPlayerHouseID(playerid), Float:x, Float:y, Float:z; 219 | 220 | if (houseid != INVALID_HOUSE_ID) { // if the player is inside a house, get the exterior location of the house 221 | GetHouseExteriorPos(houseid, x, y, z); 222 | } else if (!GetPlayerPos(playerid, x, y, z)) { // the player isn't connected, presuming that GetPlayerHouseID returns INVALID_HOUSE_ID in that case 223 | return INVALID_MAP_ZONE_ID; 224 | } 225 | 226 | return GetMapZoneAtPoint(x, y, z); 227 | } 228 | ``` 229 | 230 | ## Testing 231 | 232 | To test, simply run the package: 233 | 234 | ```bash 235 | sampctl package run 236 | ``` 237 | -------------------------------------------------------------------------------- /markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/Jeffail/gabs" 12 | "github.com/russross/blackfriday" 13 | "github.com/yhat/scrape" 14 | "golang.org/x/net/html" 15 | "golang.org/x/net/html/atom" 16 | ) 17 | 18 | var ( 19 | TagsMyBB = `"tags": { 20 | "H1": "[COLOR=#FF4700][SIZE=21][B]%s[/B][/SIZE][/COLOR]", 21 | "H2": "[COLOR=RoyalBlue][SIZE=18][B]%s[/B][/SIZE][/COLOR]", 22 | "H3": "[COLOR=DeepSkyBlue][SIZE=15][B]%s[/B][/SIZE][/COLOR]", 23 | "H4": "[COLOR=SlateGray][SIZE=12]%s[/SIZE][/COLOR]" 24 | }` 25 | TagsDefault = `"tags": { 26 | "H1": "[COLOR=#FF4700][SIZE=7][B]%s[/B][/SIZE][/COLOR]", 27 | "H2": "[COLOR=RoyalBlue][SIZE=6][B]%s[/B][/SIZE][/COLOR]", 28 | "H3": "[COLOR=DeepSkyBlue][SIZE=5][B]%s[/B][/SIZE][/COLOR]", 29 | "H4": "[COLOR=SlateGray][SIZE=4]%s[/SIZE][/COLOR]" 30 | }` 31 | GeneralSyntax = `{ 32 | %s, 33 | "keywords": { 34 | "if": "[COLOR=Blue]$0[/COLOR]", 35 | "else": "[COLOR=Blue]$0[/COLOR]", 36 | "for": "[COLOR=Blue]$0[/COLOR]", 37 | "foreach": "[COLOR=Blue]$0[/COLOR]", 38 | "while": "[COLOR=Blue]$0[/COLOR]", 39 | "do": "[COLOR=Blue]$0[/COLOR]", 40 | "switch": "[COLOR=Blue]$0[/COLOR]", 41 | "case": "[COLOR=Blue]$0[/COLOR]", 42 | "default": "[COLOR=Blue]$0[/COLOR]", 43 | "new": "[COLOR=Blue]$0[/COLOR]", 44 | "enum": "[COLOR=Blue]$0[/COLOR]", 45 | "return": "[COLOR=Blue]$0[/COLOR]", 46 | "continue": "[COLOR=Blue]$0[/COLOR]", 47 | "break": "[COLOR=Blue]$0[/COLOR]", 48 | "goto": "[COLOR=Blue]$0[/COLOR]", 49 | "char": "[COLOR=Blue]$0[/COLOR]", 50 | 51 | "state": "[COLOR=Orange]$0[/COLOR]", 52 | 53 | "true": "[COLOR=Purple]$0[/COLOR]", 54 | "false": "[COLOR=Purple]$0[/COLOR]", 55 | 56 | "stock": "[COLOR=DeepSkyBlue]$0[/COLOR]", 57 | "public": "[COLOR=DeepSkyBlue]$0[/COLOR]", 58 | "forward": "[COLOR=DeepSkyBlue]$0[/COLOR]", 59 | "const": "[COLOR=DeepSkyBlue]$0[/COLOR]", 60 | "static": "[COLOR=DeepSkyBlue]$0[/COLOR]", 61 | "hook": "[COLOR=Blue]$0[/COLOR]" 62 | }, 63 | "numbers": "[COLOR=Purple]$0[/COLOR]", 64 | "directives": "[COLOR=Blue]$0[/COLOR]", 65 | "operators": "[COLOR=Red]$0[/COLOR]", 66 | "strings": "[COLOR=Purple]$0[/COLOR]", 67 | "comment_open": "[COLOR=Green]", 68 | "comment_close": "[/COLOR]" 69 | }` 70 | ) 71 | 72 | func ParseStyles(styler, tags string) (*gabs.Container, error) { 73 | var ( 74 | jsonParsed *gabs.Container 75 | err error 76 | ) 77 | 78 | if styler == "" { 79 | jsonParsed, err = gabs.ParseJSON([]byte(fmt.Sprintf(GeneralSyntax, tags))) 80 | } else { 81 | jsonParsed, err = gabs.ParseJSONFile("./" + styler + ".json") 82 | } 83 | if err != nil { 84 | return nil, err 85 | } 86 | return jsonParsed, nil 87 | } 88 | 89 | func Process(input io.Reader, output io.Writer, jsonParsed *gabs.Container) (err error) { 90 | contents, err := ioutil.ReadAll(input) 91 | if err != nil { 92 | return 93 | } 94 | contents = bytes.Replace(contents, []byte("\r"), []byte(""), -1) 95 | 96 | out := blackfriday.Run(contents) 97 | //fmt.Fprint(output, string(out)) 98 | reader := bytes.NewReader(out) 99 | 100 | root, err := html.Parse(reader) 101 | if err != nil { 102 | return 103 | } 104 | 105 | doc := root.FirstChild.LastChild // (this gets us here) 106 | 107 | styleH1 := jsonParsed.Path("tags.H1").Data().(string) 108 | styleH2 := jsonParsed.Path("tags.H2").Data().(string) 109 | styleH3 := jsonParsed.Path("tags.H3").Data().(string) 110 | styleH4 := jsonParsed.Path("tags.H4").Data().(string) 111 | 112 | forChildren(doc, func(node *html.Node) { 113 | if scrape.ByTag(atom.H1)(node) { 114 | fmt.Fprintf(output, styleH1, getText(node, jsonParsed)) 115 | } else if scrape.ByTag(atom.H2)(node) { 116 | fmt.Fprintf(output, styleH2, getText(node, jsonParsed)) 117 | } else if scrape.ByTag(atom.H3)(node) { 118 | fmt.Fprintf(output, styleH3, getText(node, jsonParsed)) 119 | } else if scrape.ByTag(atom.H4)(node) { 120 | fmt.Fprintf(output, styleH4, getText(node, jsonParsed)) 121 | } else if scrape.ByTag(atom.P)(node) { 122 | fmt.Fprint(output, strings.Replace(getText(node, jsonParsed), "\n", " ", -1)) 123 | } else if scrape.ByTag(atom.Ul)(node) { 124 | fmt.Fprintf(output, "[LIST]"+getText(node, jsonParsed)+"[/LIST]") 125 | } else if scrape.ByTag(atom.Ol)(node) { 126 | fmt.Fprintf(output, "[LIST=1]"+getText(node, jsonParsed)+"[/LIST]") 127 | } else if scrape.ByTag(atom.Blockquote)(node) { 128 | fmt.Fprintf(output, "[QUOTE]\n"+getText(node, jsonParsed)+"\n[/QUOTE]") 129 | } else if scrape.ByTag(atom.Pre)(node) { 130 | fmt.Fprint(output, getText(node, jsonParsed)) 131 | } else { 132 | return 133 | } 134 | fmt.Fprintf(output, "\n\n") 135 | }) 136 | 137 | return 138 | } 139 | 140 | func forChildren(node *html.Node, fn func(node *html.Node)) { 141 | for c := node.FirstChild; c != nil; c = c.NextSibling { 142 | fn(c) 143 | } 144 | } 145 | 146 | func getText(node *html.Node, jsonParsed *gabs.Container) string { 147 | buf := bytes.Buffer{} 148 | 149 | forChildren(node, func(inner *html.Node) { 150 | if inner.Type == html.TextNode { 151 | buf.WriteString(inner.Data) 152 | } else if inner.Type == html.ElementNode { 153 | begin := "" 154 | end := "" 155 | text := getText(inner, jsonParsed) 156 | 157 | if inner.Data == "code" { 158 | if hasAttr(inner, "class") { 159 | if attrIs(inner, "class", "language-json") { 160 | begin = "[PHP]\n" 161 | end = "[/PHP]" 162 | } else if attrIs(inner, "class", "language-pawn") { 163 | begin = `[QUOTE][FONT=Courier New]` + "\n" 164 | text = Syntax(strings.TrimSpace(text), jsonParsed) 165 | end = "[/FONT][/QUOTE]" 166 | } else { 167 | begin = "[CODE]\n" 168 | end = "[/CODE]" 169 | } 170 | } else { 171 | begin = `[FONT=courier new]` 172 | end = `[/FONT]` 173 | } 174 | } else if inner.Data == "em" { 175 | begin = `[i]` 176 | end = `[/i]` 177 | } else if inner.Data == "strong" { 178 | begin = `[b]` 179 | end = `[/b]` 180 | } else if inner.Data == "li" { 181 | begin = "[*]" 182 | end = "" 183 | } else if inner.Data == "a" { 184 | href := getAttr(inner, "href") 185 | if href != "" { 186 | begin = fmt.Sprintf(`[URL=%s]`, href) 187 | end = "[/URL]" 188 | } 189 | } else if inner.Data == "img" { 190 | src := getAttr(inner, "src") 191 | if src != "" { 192 | begin = "[IMG]" 193 | end = "[/IMG]" 194 | text = src 195 | } 196 | } else if inner.Data == "p" { 197 | //nolint 198 | } else if inner.Data == "ul" { 199 | begin = "[LIST]" 200 | end = "[/LIST]" 201 | } else { 202 | begin = "[UNHANDLED-TAG= + inner.Data + ]" 203 | end = "[/UNHANDLED-TAG= + inner.Data + ]" 204 | } 205 | 206 | buf.WriteString(begin) 207 | buf.WriteString(text) 208 | buf.WriteString(end) 209 | } 210 | }) 211 | return buf.String() 212 | } 213 | 214 | func hasAttr(node *html.Node, attr string) bool { 215 | for _, a := range node.Attr { 216 | if a.Key == attr { 217 | return true 218 | } 219 | } 220 | return false 221 | } 222 | 223 | func attrIs(node *html.Node, attr, val string) bool { 224 | for _, a := range node.Attr { 225 | if a.Key == attr && a.Val == val { 226 | return true 227 | } 228 | } 229 | return false 230 | } 231 | 232 | func getAttr(node *html.Node, attr string) string { 233 | for _, a := range node.Attr { 234 | if a.Key == attr { 235 | return a.Val 236 | } 237 | } 238 | return "" 239 | } 240 | 241 | func Syntax(in string, jsonParsed *gabs.Container) string { 242 | stringLiteral := regexp.MustCompile(`"[\s\S]*"`) 243 | comment := regexp.MustCompile(`//.*`) 244 | blockCommentOpen := regexp.MustCompile(`\/\*.*`) 245 | blockCommentClose := regexp.MustCompile(`.*\*\/`) 246 | directive := regexp.MustCompile(`#.*`) 247 | 248 | styleCommentOpen := jsonParsed.Path("comment_open").Data().(string) 249 | styleCommentClose := jsonParsed.Path("comment_close").Data().(string) 250 | styleDirectives := jsonParsed.Path("directives").Data().(string) 251 | styleNumbers := jsonParsed.Path("numbers").Data().(string) 252 | styleStrings := jsonParsed.Path("strings").Data().(string) 253 | //styleOperators := jsonParsed.Path("operators").Data().(string) 254 | 255 | replacements := [][2]string{ 256 | {`0x(\d|[a-f]|[A-F])+`, styleNumbers}, 257 | {`0b([0-1])+`, styleNumbers}, 258 | {`(\+|-)?\d+`, styleNumbers}, 259 | } 260 | 261 | children, err := jsonParsed.Path("keywords").ChildrenMap() 262 | if err != nil { 263 | fmt.Println("Failed to read `keywords` from JSON for syntax:", err) 264 | return in 265 | } 266 | for key, child := range children { 267 | replacements = append(replacements, [2]string{`\b` + key + `\b`, child.Data().(string)}) 268 | } 269 | 270 | processSpecial := true 271 | processCommon := true 272 | inBlockComment := false 273 | buf := bytes.Buffer{} 274 | var pos []int 275 | var firstPart, secondPart string 276 | 277 | for _, line := range strings.Split(in, "\n") { 278 | line = stringLiteral.ReplaceAllString(line, styleStrings) 279 | 280 | if !inBlockComment && blockCommentOpen.MatchString(line) { 281 | line = blockCommentOpen.ReplaceAllString(line, styleCommentOpen+`$0`) 282 | inBlockComment = true 283 | } 284 | if inBlockComment { 285 | processSpecial = false 286 | processCommon = false 287 | if blockCommentClose.MatchString(line) { 288 | line = blockCommentClose.ReplaceAllString(line, `$0`+styleCommentClose) 289 | inBlockComment = false 290 | processSpecial = true 291 | processCommon = true 292 | } 293 | } 294 | 295 | if processSpecial { 296 | pos = comment.FindStringIndex(line) 297 | 298 | if pos != nil { 299 | line = comment.ReplaceAllString(line, styleCommentOpen+`$0`+styleCommentClose) 300 | processCommon = true 301 | } else if directive.MatchString(line) { 302 | line = directive.ReplaceAllString(line, styleDirectives) 303 | processCommon = false 304 | } else { 305 | processCommon = true 306 | } 307 | } 308 | 309 | if processCommon { 310 | if pos != nil { 311 | firstPart = line[:pos[0]] 312 | secondPart = line[pos[0]:] 313 | } else { 314 | firstPart = line 315 | secondPart = "" 316 | } 317 | 318 | for _, set := range replacements { 319 | firstPart = regexp.MustCompile(set[0]). 320 | ReplaceAllString(firstPart, set[1]) 321 | } 322 | 323 | line = firstPart + secondPart 324 | } 325 | 326 | tmp := 0 327 | for _, ch := range line { 328 | tmp++ 329 | if ch == '\t' { 330 | buf.WriteRune(' ') 331 | for tmp%4 != 0 { 332 | buf.WriteRune(' ') 333 | tmp++ 334 | } 335 | } else { 336 | buf.WriteRune(ch) 337 | } 338 | } 339 | buf.WriteRune('\n') 340 | } 341 | 342 | return buf.String() 343 | } 344 | -------------------------------------------------------------------------------- /tests/full.md: -------------------------------------------------------------------------------- 1 | # sampctl 2 | 3 | [![Build Status](https://travis-ci.org/Southclaws/sampctl.svg?branch=master)](https://travis-ci.org/Southclaws/sampctl) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/Southclaws/sampctl)](https://goreportcard.com/report/github.com/Southclaws/sampctl) 5 | [![https://img.shields.io/badge/Ko--Fi-Buy%20Me%20a%20Coffee-brown.svg](https://img.shields.io/badge/Ko--Fi-Buy%20Me%20a%20Coffee-brown.svg)](https://ko-fi.com/southclaws) 6 | 7 | ![sampctl.png](sampctl.png) 8 | 9 | The Swiss Army Knife of SA:MP - vital tools for any server owner or library 10 | maintainer. 11 | 12 | ## Overview 13 | 14 | Server management and configuration tools: 15 | 16 | * Manage your server settings in JSON format (compiles to server.cfg) 17 | * Run the server from `sampctl` and let it worry about automatic restarts 18 | * Automatically download Windows/Linux server binaries when you need them 19 | 20 | Package management and dependency tools: 21 | 22 | * Always have the libraries you need at the versions to specify 23 | * No more copies of the Pawn compiler or includes, let `sampctl` handle it 24 | * Easily write and run tests for libraries or quickly run arbitrary code 25 | 26 | ## Installation 27 | 28 | Installation is simple and fast on all platforms. If you're not into it, 29 | uninstallation is also simple and fast. 30 | 31 | * [Linux (Debian/Ubuntu)](https://github.com/Southclaws/sampctl/wiki/Linux) 32 | * [Windows](https://github.com/Southclaws/sampctl/wiki/Windows) 33 | * [Mac](https://github.com/Southclaws/sampctl/wiki/Mac) 34 | 35 | ## Usage 36 | 37 | Scroll to the end of this document for an overview of the commands. 38 | 39 | Or visit the [wiki](https://github.com/Southclaws/sampctl/wiki) for all the 40 | information you need. 41 | 42 | --- 43 | 44 | ## Features 45 | 46 | sampctl is designed for both development of gamemodes/libraries and management 47 | of live servers. 48 | 49 | ### Package Management and Build Tool 50 | 51 | If you've used platforms like NodeJS, Python, Go, Ruby, etc you know how useful 52 | tools like npm, pip, gem are. 53 | 54 | It's about time Pawn had the same tool. 55 | 56 | sampctl provides a simple and intuitive way to _declare_ what includes your 57 | project depends on while taking care of all the hard work such as downloading 58 | those includes to the correct directory, ensuring they are at the correct 59 | version and making sure the compiler has all the information it needs. 60 | 61 | If you're a Pawn library maintainer, you know it's awkward to set up unit tests 62 | for libraries. Even if you just want to quickly test some code, you know that 63 | you can't just write code and test it instantly. You need to set up a server, 64 | compile the include into a gamemode, configure the server and run it. 65 | 66 | Forget all that. Just make a `pawn.json` in your project directory: 67 | 68 | ```json 69 | { 70 | "entry": "test.pwn", 71 | "output": "test.amx", 72 | "dependencies": ["Southclaws/samp-stdlib", "Southclaws/formatex"] 73 | } 74 | ``` 75 | 76 | Write your quick test code: 77 | 78 | ```pawn 79 | #include 80 | #include 81 | 82 | main() { 83 | new str[128]; 84 | formatex(str, sizeof str, "My favourite vehicle is: '%v'!", 400); // should print "Landstalker" 85 | print(str); 86 | } 87 | ``` 88 | 89 | And run it! 90 | 91 | ```bash 92 | sampctl package run 93 | Using cached package for 0.3.7 94 | building /: with 3.10.4 95 | Compiling source: '/tmp/test.pwn' with compiler 3.10.4... 96 | Using cached package pawnc-3.10.4-darwin.zip 97 | Starting server... 98 | 99 | Server Plugins 100 | -------------- 101 | Loaded 0 plugins. 102 | 103 | 104 | Started server on port: 7777, with maxplayers: 50 lanmode is OFF. 105 | 106 | 107 | Filterscripts 108 | --------------- 109 | Loaded 0 filterscripts. 110 | 111 | My favourite vehicle is: 'Landstalker'! 112 | ``` 113 | 114 | You get the compiler output and the server output without ever needing to: 115 | 116 | * visit sa-mp.com/download.php 117 | * unzip a server package 118 | * worry about Windows or Linux 119 | * set up the Pawn compiler 120 | * make sure the Pawn compiler is reading the correct includes 121 | * download the formatex include 122 | 123 | [See documentation for more info.](https://github.com/Southclaws/sampctl/wiki/Package-Definition-Reference) 124 | 125 | ### Server Configuration and Automatic Plugin Download 126 | 127 | Use JSON or YAML to write your server config: 128 | 129 | ```json 130 | { 131 | "gamemodes": ["rivershell"], 132 | "plugins": ["maddinat0r/sscanf"], 133 | "rcon_password": "test", 134 | "port": 8080 135 | } 136 | ``` 137 | 138 | It compiles to this: 139 | 140 | ```conf 141 | gamemode0 rivershell 142 | plugins filemanager.so 143 | rcon_password test 144 | port 8080 145 | (... and the rest of the settings which have default values) 146 | ``` 147 | 148 | What also happens here is `maddinat0r/sscanf` tells sampctl to automatically get 149 | the latest sscanf plugin and place the `.so` or `.dll` file into the `plugins/` 150 | directory. 151 | 152 | [See documentation for more info.](https://github.com/Southclaws/sampctl/wiki/Runtime-Configuration-Reference) 153 | 154 | --- 155 | 156 | # `sampctl` 157 | 158 | 1.5.9 - Southclaws 159 | 160 | Compiles server configuration JSON to server.cfg format. Executes the server and 161 | monitors it for crashes, restarting if necessary. Provides a way to quickly 162 | download server binaries of a specified version. Provides dependency management 163 | and package build tools for library maintainers and gamemode writers alike. 164 | 165 | ## Commands (5) 166 | 167 | ### `sampctl server` 168 | 169 | Usage: `sampctl server ` 170 | 171 | For managing servers and runtime configurations. 172 | 173 | #### Subcommands (4) 174 | 175 | ### `sampctl server init` 176 | 177 | Usage: `sampctl server init` 178 | 179 | Bootstrap a new SA:MP server and generates a `samp.json`/`samp.yaml` 180 | configuration based on user input. If `gamemodes`, `filterscripts` or `plugins` 181 | directories are present, you will be prompted to select relevant files. 182 | 183 | #### Flags 184 | 185 | * `--version value`: the SA:MP server version to use (default: "0.3.7") 186 | * `--dir value`: working directory for the server - by default, uses the current 187 | directory (default: ".") 188 | * `--endpoint value`: endpoint to download packages from (default: 189 | "http://files.sa-mp.com") 190 | 191 | ### `sampctl server download` 192 | 193 | Usage: `sampctl server download` 194 | 195 | Downloads the files necessary to run a SA:MP server to the current directory 196 | (unless `--dir` specified). Will download the latest stable (non RC) server 197 | version unless `--version` is specified. 198 | 199 | #### Flags 200 | 201 | * `--version value`: the SA:MP server version to use (default: "0.3.7") 202 | * `--dir value`: working directory for the server - by default, uses the current 203 | directory (default: ".") 204 | * `--endpoint value`: endpoint to download packages from (default: 205 | "http://files.sa-mp.com") 206 | 207 | ### `sampctl server ensure` 208 | 209 | Usage: `sampctl server ensure` 210 | 211 | Ensures the server environment is representative of the configuration specified 212 | in `samp.json`/`samp.yaml` - downloads server binaries and plugin files if 213 | necessary and generates a `server.cfg` file. 214 | 215 | #### Flags 216 | 217 | * `--dir value`: working directory for the server - by default, uses the current 218 | directory (default: ".") 219 | * `--noCache --forceEnsure`: forces download of plugins if --forceEnsure is set 220 | 221 | ### `sampctl server run` 222 | 223 | Usage: `sampctl server run` 224 | 225 | Generates a `server.cfg` file based on the configuration inside 226 | `samp.json`/`samp.yaml` then executes the server process and automatically 227 | restarts it on crashes. 228 | 229 | #### Flags 230 | 231 | * `--dir value`: working directory for the server - by default, uses the current 232 | directory (default: ".") 233 | * `--container`: starts the server as a Linux container instead of running it in 234 | the current directory 235 | * `--mountCache --container`: if --container is set, mounts the local cache 236 | directory inside the container 237 | * `--forceEnsure`: forces plugin and binaries ensure before run 238 | * `--noCache --forceEnsure`: forces download of plugins if --forceEnsure is set 239 | 240 | --- 241 | 242 | ### `sampctl package` 243 | 244 | Usage: `sampctl package ` 245 | 246 | For managing Pawn packages such as gamemodes and libraries. 247 | 248 | #### Subcommands (5) 249 | 250 | ### `sampctl package init` 251 | 252 | Usage: `sampctl package init` 253 | 254 | Helper tool to bootstrap a new package or turn an existing project into a 255 | package. 256 | 257 | #### Flags 258 | 259 | * `--dir value`: working directory for the project - by default, uses the 260 | current directory (default: ".") 261 | 262 | ### `sampctl package ensure` 263 | 264 | Usage: `sampctl package ensure` 265 | 266 | Ensures dependencies are up to date based on the `dependencies` field in 267 | `pawn.json`/`pawn.yaml`. 268 | 269 | #### Flags 270 | 271 | * `--dir value`: working directory for the project - by default, uses the 272 | current directory (default: ".") 273 | 274 | ### `sampctl package install` 275 | 276 | Usage: `sampctl package install [package definition]` 277 | 278 | Installs a new package by adding it to the `dependencies` field in 279 | `pawn.json`/`pawn.yaml` downloads the contents. 280 | 281 | #### Flags 282 | 283 | * `--dir value`: working directory for the project - by default, uses the 284 | current directory (default: ".") 285 | 286 | ### `sampctl package build` 287 | 288 | Usage: `sampctl package build` 289 | 290 | Builds a package defined by a `pawn.json`/`pawn.yaml` file. 291 | 292 | #### Flags 293 | 294 | * `--dir value`: working directory for the project - by default, uses the 295 | current directory (default: ".") 296 | * `--build --forceBuild`: build configuration to use if --forceBuild is set 297 | * `--forceEnsure --forceBuild`: forces dependency ensure before build if 298 | --forceBuild is set 299 | 300 | ### `sampctl package run` 301 | 302 | Usage: `sampctl package run` 303 | 304 | Compiles and runs a package defined by a `pawn.json`/`pawn.yaml` file. 305 | 306 | #### Flags 307 | 308 | * `--version value`: the SA:MP server version to use (default: "0.3.7") 309 | * `--dir value`: working directory for the server - by default, uses the current 310 | directory (default: ".") 311 | * `--endpoint value`: endpoint to download packages from (default: 312 | "http://files.sa-mp.com") 313 | * `--container`: starts the server as a Linux container instead of running it in 314 | the current directory 315 | * `--mountCache --container`: if --container is set, mounts the local cache 316 | directory inside the container 317 | * `--build --forceBuild`: build configuration to use if --forceBuild is set 318 | * `--forceBuild`: forces a build to run before executing the server 319 | * `--forceEnsure --forceBuild`: forces dependency ensure before build if 320 | --forceBuild is set 321 | * `--noCache --forceEnsure`: forces download of plugins if --forceEnsure is set 322 | 323 | --- 324 | 325 | ### `sampctl version` 326 | 327 | Show version number - this is also the version of the container image that will 328 | be used for `--container` runtimes. 329 | 330 | --- 331 | 332 | ### `sampctl docs` 333 | 334 | Usage: `sampctl docs > documentation.md` 335 | 336 | Generate documentation in markdown format and print to standard out. 337 | 338 | --- 339 | 340 | ### `sampctl help` 341 | 342 | Usage: `Shows a list of commands or help for one command` 343 | 344 | --- 345 | 346 | ## Global Flags 347 | 348 | * `--help, -h`: show help 349 | * `--appVersion, -V`: sampctl version 350 | -------------------------------------------------------------------------------- /tests/zones.bb: -------------------------------------------------------------------------------- 1 | [COLOR=#FF4700][SIZE=7][B]SA-MP Map Zones[/B][/SIZE][/COLOR] 2 | 3 | [URL=https://github.com/kristoisberg/samp-map-zones][IMG]https://shields.southcla.ws/badge/sampctl-samp--map--zones-2f2f2f.svg?style=for-the-badge[/IMG][/URL] 4 | 5 | This library does not bring anything gamechanging to the table, it’s created to stop a decade long era of bad practices regarding map zones. An array of ~350 zones dumped (or manually converted?) from the game has been around for such a long time, but in that time I’ve never seen a satisfactory API for them. Let’s look at an implementation from Emmet_’s South Central Roleplay. 6 | 7 | [CODE] 8 | [COLOR=DeepSkyBlue]stock[/COLOR] GetLocation(Float:fX, Float:fY, Float:fZ) 9 | { 10 | [COLOR=Blue]enum[/COLOR] e_ZoneData 11 | { 12 | e_ZoneName[[COLOR=Purple]32[/COLOR] [COLOR=Blue]char[/COLOR]], 13 | Float:e_ZoneArea[[COLOR=Purple]6[/COLOR]] 14 | }; 15 | [COLOR=Blue]new[/COLOR] [COLOR=DeepSkyBlue]const[/COLOR] g_arrZoneData[][e_ZoneData] = 16 | { 17 | [COLOR=Green]// ...[/COLOR] 18 | }; 19 | [COLOR=Blue]new[/COLOR] 20 | name[[COLOR=Purple]32[/COLOR]] = [COLOR=Purple]"San Andreas"[/COLOR]; 21 | 22 | [COLOR=Blue]for[/COLOR] ([COLOR=Blue]new[/COLOR] i = [COLOR=Purple]0[/COLOR]; i != sizeof(g_arrZoneData); i ++) 23 | { 24 | [COLOR=Blue]if[/COLOR] ( 25 | (fX >= g_arrZoneData[i][e_ZoneArea][[COLOR=Purple]0[/COLOR]] && fX <= g_arrZoneData[i][e_ZoneArea][[COLOR=Purple]3[/COLOR]]) && 26 | (fY >= g_arrZoneData[i][e_ZoneArea][[COLOR=Purple]1[/COLOR]] && fY <= g_arrZoneData[i][e_ZoneArea][[COLOR=Purple]4[/COLOR]]) && 27 | (fZ >= g_arrZoneData[i][e_ZoneArea][[COLOR=Purple]2[/COLOR]] && fZ <= g_arrZoneData[i][e_ZoneArea][[COLOR=Purple]5[/COLOR]])) 28 | { 29 | strunpack(name, g_arrZoneData[i][e_ZoneName]); 30 | 31 | [COLOR=Blue]break[/COLOR]; 32 | } 33 | } 34 | [COLOR=Blue]return[/COLOR] name; 35 | } 36 | 37 | [COLOR=DeepSkyBlue]stock[/COLOR] GetPlayerLocation(playerid) 38 | { 39 | [COLOR=Blue]new[/COLOR] 40 | Float:fX, 41 | Float:fY, 42 | Float:fZ, 43 | string[[COLOR=Purple]32[/COLOR]], 44 | id = [COLOR=Purple]-1[/COLOR]; 45 | 46 | [COLOR=Blue]if[/COLOR] ((id = House_Inside(playerid)) != [COLOR=Purple]-1[/COLOR]) 47 | { 48 | fX = HouseData[id][housePos][[COLOR=Purple]0[/COLOR]]; 49 | fY = HouseData[id][housePos][[COLOR=Purple]1[/COLOR]]; 50 | fZ = HouseData[id][housePos][[COLOR=Purple]2[/COLOR]]; 51 | } 52 | [COLOR=Green]// ...[/COLOR] 53 | [COLOR=Blue]else[/COLOR] GetPlayerPos(playerid, fX, fY, fZ); 54 | 55 | format(string, [COLOR=Purple]32[/COLOR], GetLocation(fX, fY, fZ)); 56 | [COLOR=Blue]return[/COLOR] string; 57 | } 58 | [/CODE] 59 | 60 | [IMG]https://i.imgur.com/cyUdlu4.png[/IMG] 61 | 62 | If you didn’t get the reference, you should probably check out [URL=https://github.com/sampctl/pawn-array-return-bug]this repository[/URL]. [FONT=courier new]GetPlayerLocation[/FONT] most likely uses [FONT=courier new]format[/FONT] to prevent this bug from occurring, but the risk is still there and arrays should never be returned in PAWN. Let’s take a look at another implementation that even I used a long time ago. 63 | 64 | [CODE] 65 | [COLOR=DeepSkyBlue]stock[/COLOR] GetPointZone(Float:x, Float:y, Float:z, zone[] = [COLOR=Purple]"San Andreas"[/COLOR], len = sizeof(zone)) 66 | { 67 | [COLOR=Blue]for[/COLOR] ([COLOR=Blue]new[/COLOR] i, j = sizeof(Zones); i < j; i++) 68 | { 69 | [COLOR=Blue]if[/COLOR] (x >= Zones[i][zArea][[COLOR=Purple]0[/COLOR]] && x <= Zones[i][zArea][[COLOR=Purple]3[/COLOR]] && y >= Zones[i][zArea][[COLOR=Purple]1[/COLOR]] && y <= Zones[i][zArea][[COLOR=Purple]4[/COLOR]] && z >= Zones[i][zArea][[COLOR=Purple]2[/COLOR]] && z <= Zones[i][zArea][[COLOR=Purple]5[/COLOR]]) 70 | { 71 | strunpack(zone, Zones[i][zName], len); 72 | [COLOR=Blue]return[/COLOR] [COLOR=Purple]1[/COLOR]; 73 | } 74 | } 75 | [COLOR=Blue]return[/COLOR] [COLOR=Purple]1[/COLOR]; 76 | } 77 | 78 | [COLOR=DeepSkyBlue]stock[/COLOR] GetPlayerZone(playerid, zone[], len = sizeof(zone)) 79 | { 80 | [COLOR=Blue]new[/COLOR] Float:pos[[COLOR=Purple]3[/COLOR]]; 81 | GetPlayerPos(playerid, pos[[COLOR=Purple]0[/COLOR]], pos[[COLOR=Purple]1[/COLOR]], pos[[COLOR=Purple]2[/COLOR]]); 82 | 83 | [COLOR=Blue]for[/COLOR] ([COLOR=Blue]new[/COLOR] i, j = sizeof(Zones); i < j; i++) 84 | { 85 | [COLOR=Blue]if[/COLOR] (x >= Zones[i][zArea][[COLOR=Purple]0[/COLOR]] && x <= Zones[i][zArea][[COLOR=Purple]3[/COLOR]] && y >= Zones[i][zArea][[COLOR=Purple]1[/COLOR]] && y <= Zones[i][zArea][[COLOR=Purple]4[/COLOR]] && z >= Zones[i][zArea][[COLOR=Purple]2[/COLOR]] && z <= Zones[i][zArea][[COLOR=Purple]5[/COLOR]]) 86 | { 87 | strunpack(zone, Zones[i][zName], len); 88 | [COLOR=Blue]return[/COLOR] [COLOR=Purple]1[/COLOR]; 89 | } 90 | } 91 | [COLOR=Blue]return[/COLOR] [COLOR=Purple]1[/COLOR]; 92 | } 93 | [/CODE] 94 | 95 | First of all, what do we see? A lot of code repetition. That’s easy to fix in this case, but what if we also needed either the min/max position of the zone? We’d have to loop through the zones again or take a different approach. Which approach does this library take? Functions like [FONT=courier new]GetMapZoneAtPoint[/FONT] and [FONT=courier new]GetPlayerMapZone[/FONT] do not return the name of the zone, they return an identificator of it. The name or positions of the zone must be fetched using another function. In addition to that, I rebuilt the array of zones myself since the one used basically everywhere seems to be faulty according to [URL=https://forum.sa-mp.com/showpost.php?p=4050745&postcount=7]this post[/URL]. 96 | 97 | [COLOR=RoyalBlue][SIZE=6][B]Installation[/B][/SIZE][/COLOR] 98 | 99 | Simply install to your project: 100 | 101 | [CODE] 102 | sampctl package install kristoisberg/samp-map-zones 103 | [/CODE] 104 | 105 | Include in your code and begin using the library: 106 | 107 | [CODE] 108 | [COLOR=Blue]#include [/COLOR] 109 | [/CODE] 110 | 111 | [COLOR=RoyalBlue][SIZE=6][B]Usage[/B][/SIZE][/COLOR] 112 | 113 | [COLOR=DeepSkyBlue][SIZE=5][B]Constants[/B][/SIZE][/COLOR] 114 | 115 | [LIST] 116 | [*][FONT=courier new]INVALID_MAP_ZONE_ID = MapZone:-1[/FONT] 117 | 118 | [LIST] 119 | [*]The return value of several functions when no map zone was matching the 120 | criteria. 121 | [/LIST] 122 | [*][FONT=courier new]MAX_MAP_ZONE_NAME = 27[/FONT] 123 | 124 | [LIST] 125 | [*]The length of the longest map zone name including the null character. 126 | [/LIST] 127 | [*][FONT=courier new]MAX_MAP_ZONE_AREAS = 13[/FONT] 128 | 129 | [LIST] 130 | [*]The most areas associated with a map zone. 131 | [/LIST] 132 | [/LIST] 133 | 134 | [COLOR=DeepSkyBlue][SIZE=5][B]Functions[/B][/SIZE][/COLOR] 135 | 136 | [LIST] 137 | [*][FONT=courier new]MapZone:GetMapZoneAtPoint(Float:x, Float:y, Float:z)[/FONT] 138 | 139 | [LIST] 140 | [*]Returns the ID of the map zone the point is in or [FONT=courier new]INVALID_MAP_ZONE_ID[/FONT] if 141 | it isn’t in any. Alias: [FONT=courier new]GetMapZoneAtPoint3D[/FONT]. 142 | [/LIST] 143 | [*][FONT=courier new]MapZone:GetPlayerMapZone(playerid)[/FONT] 144 | 145 | [LIST] 146 | [*]Returns the ID of the map zone the player is in or [FONT=courier new]INVALID_MAP_ZONE_ID[/FONT] if 147 | it isn’t in any. Alias: [FONT=courier new]GetPlayerMapZone3D[/FONT]. 148 | [/LIST] 149 | [*][FONT=courier new]MapZone:GetVehicleMapZone(vehicleid)[/FONT] 150 | 151 | [LIST] 152 | [*]Returns the ID of the map zone the vehicle is in or [FONT=courier new]INVALID_MAP_ZONE_ID[/FONT] if 153 | it isn’t in any. Alias: [FONT=courier new]GetVehicleMapZone3D[/FONT]. 154 | [/LIST] 155 | [*][FONT=courier new]MapZone:GetMapZoneAtPoint2D(Float:x, Float:y)[/FONT] 156 | 157 | [LIST] 158 | [*]Returns the ID of the map zone the point is in or [FONT=courier new]INVALID_MAP_ZONE_ID[/FONT] if 159 | it isn’t in any. Does not check the Z-coordinate. 160 | [/LIST] 161 | [*][FONT=courier new]MapZone:GetPlayerMapZone2D(playerid)[/FONT] 162 | 163 | [LIST] 164 | [*]Returns the ID of the map zone the player is in or [FONT=courier new]INVALID_MAP_ZONE_ID[/FONT] if 165 | it isn’t in any. Does not check the Z-coordinate. 166 | [/LIST] 167 | [*][FONT=courier new]MapZone:GetVehicleMapZone2D(vehicleid)[/FONT] 168 | 169 | [LIST] 170 | [*]Returns the ID of the map zone the vehicle is in or [FONT=courier new]INVALID_MAP_ZONE_ID[/FONT] if 171 | it isn’t in any. Does not check the Z-coordinate. 172 | [/LIST] 173 | [*][FONT=courier new]bool:IsValidMapZone(MapZone:id)[/FONT] 174 | 175 | [LIST] 176 | [*]Returns [FONT=courier new]true[/FONT] or [FONT=courier new]false[/FONT] depending on if the map zone is valid or not. 177 | [/LIST] 178 | [*][FONT=courier new]bool:GetMapZoneName(MapZone:id, name[], size = sizeof(name))[/FONT] 179 | 180 | [LIST] 181 | [*]Retrieves the name of the map zone. Returns [FONT=courier new]true[/FONT] or [FONT=courier new]false[/FONT] depending on 182 | if the map zone is valid or not. 183 | [/LIST] 184 | [*][FONT=courier new]bool:GetMapZoneSoundID(MapZone:id, &soundid)[/FONT] 185 | 186 | [LIST] 187 | [*]Retrieves the sound ID of the map zone. Returns [FONT=courier new]true[/FONT] or [FONT=courier new]false[/FONT] depending 188 | on if the map zone is valid or not. 189 | [/LIST] 190 | [*][FONT=courier new]bool:GetMapZoneAreaCount(MapZone:id, &count)[/FONT] 191 | 192 | [LIST] 193 | [*]Retrieves the count of areas associated with the map zone. Returns [FONT=courier new]true[/FONT] or 194 | [FONT=courier new]false[/FONT] depending on if the map zone is valid or not. 195 | [/LIST] 196 | [*][FONT=courier new]GetMapZoneAreaPos(MapZone:id, &Float:minX = 0.0, &Float:minY = 0.0, &Float:minZ = 0.0, &Float:maxX = 0.0, &Float:maxY = 0.0, &Float:maxZ = 0.0, start = 0)[/FONT] 197 | 198 | [LIST] 199 | [*]Retrieves the coordinates of an area associated with the map zone. Returns 200 | the array index for the area or [FONT=courier new]-1[/FONT] if none were found. See the usage in 201 | in the examples section. 202 | [/LIST] 203 | [*][FONT=courier new]GetMapZoneCount()[/FONT] 204 | 205 | [LIST] 206 | [*]Returns the count of map zones in the array. Could be used for iteration 207 | purposes. 208 | [/LIST] 209 | [/LIST] 210 | 211 | [COLOR=RoyalBlue][SIZE=6][B]Examples[/B][/SIZE][/COLOR] 212 | 213 | [COLOR=DeepSkyBlue][SIZE=5][B]Retrieving the location of a player[/B][/SIZE][/COLOR] 214 | 215 | [CODE] 216 | CMD:whereami(playerid) { 217 | [COLOR=Blue]new[/COLOR] MapZone:zone = GetPlayerMapZone(playerid); 218 | 219 | [COLOR=Blue]if[/COLOR] (zone == INVALID_MAP_ZONE_ID) { 220 | [COLOR=Blue]return[/COLOR] SendClientMessage(playerid, [COLOR=Purple][COLOR=Purple]0[/COLOR]xFFFFFFFF[/COLOR], [COLOR=Purple]"probably in the ocean, mate"[/COLOR]); 221 | } 222 | 223 | [COLOR=Blue]new[/COLOR] name[MAX_MAP_ZONE_NAME], soundid; 224 | GetMapZoneName(zone, name); 225 | GetMapZoneSoundID(zone, soundid); 226 | 227 | [COLOR=Blue]new[/COLOR] string[[COLOR=Purple]128[/COLOR]]; 228 | format(string, sizeof(string), [COLOR=Purple]"you are in %s"[/COLOR], name); 229 | 230 | SendClientMessage(playerid, [COLOR=Purple][COLOR=Purple]0[/COLOR]xFFFFFFFF[/COLOR], string); 231 | PlayerPlaySound(playerid, soundid, [COLOR=Purple]0[/COLOR].[COLOR=Purple]0[/COLOR], [COLOR=Purple]0[/COLOR].[COLOR=Purple]0[/COLOR], [COLOR=Purple]0[/COLOR].[COLOR=Purple]0[/COLOR]); 232 | [COLOR=Blue]return[/COLOR] [COLOR=Purple]1[/COLOR]; 233 | } 234 | [/CODE] 235 | 236 | [COLOR=DeepSkyBlue][SIZE=5][B]Iterating through areas associated with a map zone[/B][/SIZE][/COLOR] 237 | 238 | [CODE] 239 | [COLOR=Blue]new[/COLOR] zone = ZONE_RICHMAN, index = [COLOR=Purple]-1[/COLOR], Float:minX, Float:minY, Float:minZ, Float:maxX, Float:maxY, Float:maxZ; 240 | 241 | [COLOR=Blue]while[/COLOR] ((index = GetMapZoneAreaPos(zone, minX, minY, minZ, maxX, maxY, maxZ, index + [COLOR=Purple]1[/COLOR]) != [COLOR=Purple]-1[/COLOR]) { 242 | printf([COLOR=Purple]"%f %f %f %f %f %f"[/COLOR], minX, minY, minZ, maxX, maxY, maxZ); 243 | } 244 | [/CODE] 245 | 246 | [COLOR=DeepSkyBlue][SIZE=5][B]Extending[/B][/SIZE][/COLOR] 247 | 248 | [CODE] 249 | [COLOR=DeepSkyBlue]stock[/COLOR] MapZone:GetPlayerOutsideMapZone(playerid) { 250 | [COLOR=Blue]new[/COLOR] House:houseid = GetPlayerHouseID(playerid), Float:x, Float:y, Float:z; 251 | 252 | [COLOR=Blue]if[/COLOR] (houseid != INVALID_HOUSE_ID) { [COLOR=Green]// if the player is inside a house, get the exterior location of the house[/COLOR] 253 | GetHouseExteriorPos(houseid, x, y, z); 254 | } [COLOR=Blue]else[/COLOR] [COLOR=Blue]if[/COLOR] (!GetPlayerPos(playerid, x, y, z)) { [COLOR=Green]// the player isn't connected, presuming that GetPlayerHouseID returns INVALID_HOUSE_ID in that case [/COLOR] 255 | [COLOR=Blue]return[/COLOR] INVALID_MAP_ZONE_ID; 256 | } 257 | 258 | [COLOR=Blue]return[/COLOR] GetMapZoneAtPoint(x, y, z); 259 | } 260 | [/CODE] 261 | 262 | [COLOR=RoyalBlue][SIZE=6][B]Testing[/B][/SIZE][/COLOR] 263 | 264 | To test, simply run the package: 265 | 266 | [CODE] 267 | sampctl package run 268 | [/CODE] 269 | 270 | -------------------------------------------------------------------------------- /tests/full.bb: -------------------------------------------------------------------------------- 1 | [COLOR=#FF4700][SIZE=7][B]sampctl[/B][/SIZE][/COLOR] 2 | 3 | [URL=https://travis-ci.org/Southclaws/sampctl][IMG]https://travis-ci.org/Southclaws/sampctl.svg?branch=master[/IMG][/URL] [URL=https://goreportcard.com/report/github.com/Southclaws/sampctl][IMG]https://goreportcard.com/badge/github.com/Southclaws/sampctl[/IMG][/URL] [URL=https://ko-fi.com/southclaws][IMG]https://img.shields.io/badge/Ko--Fi-Buy%20Me%20a%20Coffee-brown.svg[/IMG][/URL] 4 | 5 | [IMG]sampctl.png[/IMG] 6 | 7 | The Swiss Army Knife of SA:MP - vital tools for any server owner or library maintainer. 8 | 9 | [COLOR=RoyalBlue][SIZE=6][B]Overview[/B][/SIZE][/COLOR] 10 | 11 | Server management and configuration tools: 12 | 13 | [LIST] 14 | [*]Manage your server settings in JSON format (compiles to server.cfg) 15 | [*]Run the server from [FONT=courier new]sampctl[/FONT] and let it worry about automatic restarts 16 | [*]Automatically download Windows/Linux server binaries when you need them 17 | [/LIST] 18 | 19 | Package management and dependency tools: 20 | 21 | [LIST] 22 | [*]Always have the libraries you need at the versions to specify 23 | [*]No more copies of the Pawn compiler or includes, let [FONT=courier new]sampctl[/FONT] handle it 24 | [*]Easily write and run tests for libraries or quickly run arbitrary code 25 | [/LIST] 26 | 27 | [COLOR=RoyalBlue][SIZE=6][B]Installation[/B][/SIZE][/COLOR] 28 | 29 | Installation is simple and fast on all platforms. If you’re not into it, uninstallation is also simple and fast. 30 | 31 | [LIST] 32 | [*][URL=https://github.com/Southclaws/sampctl/wiki/Linux]Linux (Debian/Ubuntu)[/URL] 33 | [*][URL=https://github.com/Southclaws/sampctl/wiki/Windows]Windows[/URL] 34 | [*][URL=https://github.com/Southclaws/sampctl/wiki/Mac]Mac[/URL] 35 | [/LIST] 36 | 37 | [COLOR=RoyalBlue][SIZE=6][B]Usage[/B][/SIZE][/COLOR] 38 | 39 | Scroll to the end of this document for an overview of the commands. 40 | 41 | Or visit the [URL=https://github.com/Southclaws/sampctl/wiki]wiki[/URL] for all the information you need. 42 | 43 | [COLOR=RoyalBlue][SIZE=6][B]Features[/B][/SIZE][/COLOR] 44 | 45 | sampctl is designed for both development of gamemodes/libraries and management of live servers. 46 | 47 | [COLOR=DeepSkyBlue][SIZE=5][B]Package Management and Build Tool[/B][/SIZE][/COLOR] 48 | 49 | If you’ve used platforms like NodeJS, Python, Go, Ruby, etc you know how useful tools like npm, pip, gem are. 50 | 51 | It’s about time Pawn had the same tool. 52 | 53 | sampctl provides a simple and intuitive way to [i]declare[/i] what includes your project depends on while taking care of all the hard work such as downloading those includes to the correct directory, ensuring they are at the correct version and making sure the compiler has all the information it needs. 54 | 55 | If you’re a Pawn library maintainer, you know it’s awkward to set up unit tests for libraries. Even if you just want to quickly test some code, you know that you can’t just write code and test it instantly. You need to set up a server, compile the include into a gamemode, configure the server and run it. 56 | 57 | Forget all that. Just make a [FONT=courier new]pawn.json[/FONT] in your project directory: 58 | 59 | [PHP] 60 | { 61 | "entry": "test.pwn", 62 | "output": "test.amx", 63 | "dependencies": ["Southclaws/samp-stdlib", "Southclaws/formatex"] 64 | } 65 | [/PHP] 66 | 67 | Write your quick test code: 68 | 69 | [CODE] 70 | [COLOR=Blue]#include [/COLOR] 71 | [COLOR=Blue]#include [/COLOR] 72 | 73 | main() { 74 | [COLOR=Blue]new[/COLOR] str[[COLOR=Purple]128[/COLOR]]; 75 | formatex(str, sizeof str, [COLOR=Purple]"My favourite vehicle is: '%v'!", 400); [COLOR=Green]// should print "Landstalker"[/COLOR][/COLOR] 76 | print(str); 77 | } 78 | [/CODE] 79 | 80 | And run it! 81 | 82 | [CODE] 83 | sampctl package run 84 | Using cached package for 0.3.7 85 | building /: with 3.10.4 86 | Compiling source: '/tmp/test.pwn' with compiler 3.10.4... 87 | Using cached package pawnc-3.10.4-darwin.zip 88 | Starting server... 89 | 90 | Server Plugins 91 | -------------- 92 | Loaded 0 plugins. 93 | 94 | 95 | Started server on port: 7777, with maxplayers: 50 lanmode is OFF. 96 | 97 | 98 | Filterscripts 99 | --------------- 100 | Loaded 0 filterscripts. 101 | 102 | My favourite vehicle is: 'Landstalker'! 103 | [/CODE] 104 | 105 | You get the compiler output and the server output without ever needing to: 106 | 107 | [LIST] 108 | [*]visit sa-mp.com/download.php 109 | [*]unzip a server package 110 | [*]worry about Windows or Linux 111 | [*]set up the Pawn compiler 112 | [*]make sure the Pawn compiler is reading the correct includes 113 | [*]download the formatex include 114 | [/LIST] 115 | 116 | [URL=https://github.com/Southclaws/sampctl/wiki/Package-Definition-Reference]See documentation for more info.[/URL] 117 | 118 | [COLOR=DeepSkyBlue][SIZE=5][B]Server Configuration and Automatic Plugin Download[/B][/SIZE][/COLOR] 119 | 120 | Use JSON or YAML to write your server config: 121 | 122 | [PHP] 123 | { 124 | "gamemodes": ["rivershell"], 125 | "plugins": ["maddinat0r/sscanf"], 126 | "rcon_password": "test", 127 | "port": 8080 128 | } 129 | [/PHP] 130 | 131 | It compiles to this: 132 | 133 | [CODE] 134 | gamemode0 rivershell 135 | plugins filemanager.so 136 | rcon_password test 137 | port 8080 138 | (... and the rest of the settings which have default values) 139 | [/CODE] 140 | 141 | What also happens here is [FONT=courier new]maddinat0r/sscanf[/FONT] tells sampctl to automatically get the latest sscanf plugin and place the [FONT=courier new].so[/FONT] or [FONT=courier new].dll[/FONT] file into the [FONT=courier new]plugins/[/FONT] directory. 142 | 143 | [URL=https://github.com/Southclaws/sampctl/wiki/Runtime-Configuration-Reference]See documentation for more info.[/URL] 144 | 145 | [COLOR=#FF4700][SIZE=7][B][FONT=courier new]sampctl[/FONT][/B][/SIZE][/COLOR] 146 | 147 | 1.5.9 - Southclaws [URL=mailto:southclaws@gmail.com]southclaws@gmail.com[/URL] 148 | 149 | Compiles server configuration JSON to server.cfg format. Executes the server and monitors it for crashes, restarting if necessary. Provides a way to quickly download server binaries of a specified version. Provides dependency management and package build tools for library maintainers and gamemode writers alike. 150 | 151 | [COLOR=RoyalBlue][SIZE=6][B]Commands (5)[/B][/SIZE][/COLOR] 152 | 153 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl server[/FONT][/B][/SIZE][/COLOR] 154 | 155 | Usage: [FONT=courier new]sampctl server [/FONT] 156 | 157 | For managing servers and runtime configurations. 158 | 159 | [COLOR=SlateGray][SIZE=4]Subcommands (4)[/SIZE][/COLOR] 160 | 161 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl server init[/FONT][/B][/SIZE][/COLOR] 162 | 163 | Usage: [FONT=courier new]sampctl server init[/FONT] 164 | 165 | Bootstrap a new SA:MP server and generates a [FONT=courier new]samp.json[/FONT]/[FONT=courier new]samp.yaml[/FONT] configuration based on user input. If [FONT=courier new]gamemodes[/FONT], [FONT=courier new]filterscripts[/FONT] or [FONT=courier new]plugins[/FONT] directories are present, you will be prompted to select relevant files. 166 | 167 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 168 | 169 | [LIST] 170 | [*][FONT=courier new]--version value[/FONT]: the SA:MP server version to use (default: “0.3.7”) 171 | [*][FONT=courier new]--dir value[/FONT]: working directory for the server - by default, uses the current 172 | directory (default: “.”) 173 | [*][FONT=courier new]--endpoint value[/FONT]: endpoint to download packages from (default: 174 | “[URL=http://files.sa-mp.com")]http://files.sa-mp.com”)[/URL] 175 | [/LIST] 176 | 177 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl server download[/FONT][/B][/SIZE][/COLOR] 178 | 179 | Usage: [FONT=courier new]sampctl server download[/FONT] 180 | 181 | Downloads the files necessary to run a SA:MP server to the current directory (unless [FONT=courier new]--dir[/FONT] specified). Will download the latest stable (non RC) server version unless [FONT=courier new]--version[/FONT] is specified. 182 | 183 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 184 | 185 | [LIST] 186 | [*][FONT=courier new]--version value[/FONT]: the SA:MP server version to use (default: “0.3.7”) 187 | [*][FONT=courier new]--dir value[/FONT]: working directory for the server - by default, uses the current 188 | directory (default: “.”) 189 | [*][FONT=courier new]--endpoint value[/FONT]: endpoint to download packages from (default: 190 | “[URL=http://files.sa-mp.com")]http://files.sa-mp.com”)[/URL] 191 | [/LIST] 192 | 193 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl server ensure[/FONT][/B][/SIZE][/COLOR] 194 | 195 | Usage: [FONT=courier new]sampctl server ensure[/FONT] 196 | 197 | Ensures the server environment is representative of the configuration specified in [FONT=courier new]samp.json[/FONT]/[FONT=courier new]samp.yaml[/FONT] - downloads server binaries and plugin files if necessary and generates a [FONT=courier new]server.cfg[/FONT] file. 198 | 199 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 200 | 201 | [LIST] 202 | [*][FONT=courier new]--dir value[/FONT]: working directory for the server - by default, uses the current 203 | directory (default: “.”) 204 | [*][FONT=courier new]--noCache --forceEnsure[/FONT]: forces download of plugins if –forceEnsure is set 205 | [/LIST] 206 | 207 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl server run[/FONT][/B][/SIZE][/COLOR] 208 | 209 | Usage: [FONT=courier new]sampctl server run[/FONT] 210 | 211 | Generates a [FONT=courier new]server.cfg[/FONT] file based on the configuration inside [FONT=courier new]samp.json[/FONT]/[FONT=courier new]samp.yaml[/FONT] then executes the server process and automatically restarts it on crashes. 212 | 213 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 214 | 215 | [LIST] 216 | [*][FONT=courier new]--dir value[/FONT]: working directory for the server - by default, uses the current 217 | directory (default: “.”) 218 | [*][FONT=courier new]--container[/FONT]: starts the server as a Linux container instead of running it in 219 | the current directory 220 | [*][FONT=courier new]--mountCache --container[/FONT]: if –container is set, mounts the local cache 221 | directory inside the container 222 | [*][FONT=courier new]--forceEnsure[/FONT]: forces plugin and binaries ensure before run 223 | [*][FONT=courier new]--noCache --forceEnsure[/FONT]: forces download of plugins if –forceEnsure is set 224 | [/LIST] 225 | 226 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl package[/FONT][/B][/SIZE][/COLOR] 227 | 228 | Usage: [FONT=courier new]sampctl package [/FONT] 229 | 230 | For managing Pawn packages such as gamemodes and libraries. 231 | 232 | [COLOR=SlateGray][SIZE=4]Subcommands (5)[/SIZE][/COLOR] 233 | 234 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl package init[/FONT][/B][/SIZE][/COLOR] 235 | 236 | Usage: [FONT=courier new]sampctl package init[/FONT] 237 | 238 | Helper tool to bootstrap a new package or turn an existing project into a package. 239 | 240 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 241 | 242 | [LIST] 243 | [*][FONT=courier new]--dir value[/FONT]: working directory for the project - by default, uses the 244 | current directory (default: “.”) 245 | [/LIST] 246 | 247 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl package ensure[/FONT][/B][/SIZE][/COLOR] 248 | 249 | Usage: [FONT=courier new]sampctl package ensure[/FONT] 250 | 251 | Ensures dependencies are up to date based on the [FONT=courier new]dependencies[/FONT] field in [FONT=courier new]pawn.json[/FONT]/[FONT=courier new]pawn.yaml[/FONT]. 252 | 253 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 254 | 255 | [LIST] 256 | [*][FONT=courier new]--dir value[/FONT]: working directory for the project - by default, uses the 257 | current directory (default: “.”) 258 | [/LIST] 259 | 260 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl package install[/FONT][/B][/SIZE][/COLOR] 261 | 262 | Usage: [FONT=courier new]sampctl package install [package definition][/FONT] 263 | 264 | Installs a new package by adding it to the [FONT=courier new]dependencies[/FONT] field in [FONT=courier new]pawn.json[/FONT]/[FONT=courier new]pawn.yaml[/FONT] downloads the contents. 265 | 266 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 267 | 268 | [LIST] 269 | [*][FONT=courier new]--dir value[/FONT]: working directory for the project - by default, uses the 270 | current directory (default: “.”) 271 | [/LIST] 272 | 273 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl package build[/FONT][/B][/SIZE][/COLOR] 274 | 275 | Usage: [FONT=courier new]sampctl package build[/FONT] 276 | 277 | Builds a package defined by a [FONT=courier new]pawn.json[/FONT]/[FONT=courier new]pawn.yaml[/FONT] file. 278 | 279 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 280 | 281 | [LIST] 282 | [*][FONT=courier new]--dir value[/FONT]: working directory for the project - by default, uses the 283 | current directory (default: “.”) 284 | [*][FONT=courier new]--build --forceBuild[/FONT]: build configuration to use if –forceBuild is set 285 | [*][FONT=courier new]--forceEnsure --forceBuild[/FONT]: forces dependency ensure before build if 286 | –forceBuild is set 287 | [/LIST] 288 | 289 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl package run[/FONT][/B][/SIZE][/COLOR] 290 | 291 | Usage: [FONT=courier new]sampctl package run[/FONT] 292 | 293 | Compiles and runs a package defined by a [FONT=courier new]pawn.json[/FONT]/[FONT=courier new]pawn.yaml[/FONT] file. 294 | 295 | [COLOR=SlateGray][SIZE=4]Flags[/SIZE][/COLOR] 296 | 297 | [LIST] 298 | [*][FONT=courier new]--version value[/FONT]: the SA:MP server version to use (default: “0.3.7”) 299 | [*][FONT=courier new]--dir value[/FONT]: working directory for the server - by default, uses the current 300 | directory (default: “.”) 301 | [*][FONT=courier new]--endpoint value[/FONT]: endpoint to download packages from (default: 302 | “[URL=http://files.sa-mp.com")]http://files.sa-mp.com”)[/URL] 303 | [*][FONT=courier new]--container[/FONT]: starts the server as a Linux container instead of running it in 304 | the current directory 305 | [*][FONT=courier new]--mountCache --container[/FONT]: if –container is set, mounts the local cache 306 | directory inside the container 307 | [*][FONT=courier new]--build --forceBuild[/FONT]: build configuration to use if –forceBuild is set 308 | [*][FONT=courier new]--forceBuild[/FONT]: forces a build to run before executing the server 309 | [*][FONT=courier new]--forceEnsure --forceBuild[/FONT]: forces dependency ensure before build if 310 | –forceBuild is set 311 | [*][FONT=courier new]--noCache --forceEnsure[/FONT]: forces download of plugins if –forceEnsure is set 312 | [/LIST] 313 | 314 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl version[/FONT][/B][/SIZE][/COLOR] 315 | 316 | Show version number - this is also the version of the container image that will be used for [FONT=courier new]--container[/FONT] runtimes. 317 | 318 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl docs[/FONT][/B][/SIZE][/COLOR] 319 | 320 | Usage: [FONT=courier new]sampctl docs > documentation.md[/FONT] 321 | 322 | Generate documentation in markdown format and print to standard out. 323 | 324 | [COLOR=DeepSkyBlue][SIZE=5][B][FONT=courier new]sampctl help[/FONT][/B][/SIZE][/COLOR] 325 | 326 | Usage: [FONT=courier new]Shows a list of commands or help for one command[/FONT] 327 | 328 | [COLOR=RoyalBlue][SIZE=6][B]Global Flags[/B][/SIZE][/COLOR] 329 | 330 | [LIST] 331 | [*][FONT=courier new]--help, -h[/FONT]: show help 332 | [*][FONT=courier new]--appVersion, -V[/FONT]: sampctl version 333 | [/LIST] 334 | 335 | --------------------------------------------------------------------------------