diff --git a/.github/.ci.conf b/.github/.ci.conf index 058e63fd..ed340ad4 100755 --- a/.github/.ci.conf +++ b/.github/.ci.conf @@ -1,17 +1,28 @@ # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT -PRE_TEST_HOOK=_install_gstreamer_hook -PRE_LINT_HOOK=_install_gstreamer_hook -GO_MOD_VERSION_EXPECTED=1.22 +PRE_TEST_HOOK=_install_dependencies_hook +PRE_LINT_HOOK=_install_dependencies_hook +GO_MOD_VERSION_EXPECTED=1.23 SKIP_i386_TESTS=true SKIP_API_DIFF=true function _install_gstreamer_hook(){ - set -e - - sudo apt-get update sudo apt-get purge -y libunwind-14-dev sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev sudo apt-get install -y libavcodec-dev libavutil-dev libavfilter-dev libswscale-dev libavformat-dev libavdevice-dev } + +function _install_ebiten_hook(){ + sudo apt-get install -y \ + libasound2-dev libgl1-mesa-dev libxcursor-dev libxi-dev \ + libxinerama-dev libxrandr-dev libxxf86vm-dev +} + +function _install_dependencies_hook(){ + set -e + + sudo apt-get update + _install_gstreamer_hook + _install_ebiten_hook +} diff --git a/LICENSES/CC-BY-3.0.txt b/LICENSES/CC-BY-3.0.txt new file mode 100644 index 00000000..6f7f096d --- /dev/null +++ b/LICENSES/CC-BY-3.0.txt @@ -0,0 +1,319 @@ +Creative Commons Legal Code + +Attribution 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined above) for the purposes of this + License. + c. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + d. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + e. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + f. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + g. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + h. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + i. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(b), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(b), as requested. + b. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Section 3(b), in the case of an Adaptation, + a credit identifying the use of the Work in the Adaptation (e.g., + "French translation of the Work by Original Author," or "Screenplay + based on original Work by Original Author"). The credit required by + this Section 4 (b) may be implemented in any reasonable manner; + provided, however, that in the case of a Adaptation or Collection, at + a minimum such credit will appear, if a credit for all contributing + authors of the Adaptation or Collection appears, then as part of these + credits and in a manner at least as prominent as the credits for the + other contributing authors. For the avoidance of doubt, You may only + use the credit required by this Section for the purpose of attribution + in the manner set out above and, by exercising Your rights under this + License, You may not implicitly or explicitly assert or imply any + connection with, sponsorship or endorsement by the Original Author, + Licensor and/or Attribution Parties, as appropriate, of You or Your + use of the Work, without the separate, express prior written + permission of the Original Author, Licensor and/or Attribution + Parties. + c. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of this License. + + Creative Commons may be contacted at https://creativecommons.org/. \ No newline at end of file diff --git a/ebiten-game/LICENSE.md b/ebiten-game/LICENSE.md new file mode 100644 index 00000000..ad4c1e7c --- /dev/null +++ b/ebiten-game/LICENSE.md @@ -0,0 +1,7 @@ +## game/gopher.png + +``` +The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) +The design is licensed under the Creative Commons 4.0 Attributions license. +Read this article for more details: https://blog.golang.org/gopher +``` \ No newline at end of file diff --git a/ebiten-game/README.md b/ebiten-game/README.md new file mode 100644 index 00000000..bb56a2b0 --- /dev/null +++ b/ebiten-game/README.md @@ -0,0 +1,19 @@ +# Ebitengine Game! + +This is a pretty nifty demo on how to use [ebitengine](https://ebitengine.org/) and [pion](https://github.com/pion/webrtc) to pull off a cross platform game! + +You can have a client running on the browser and one running on a desktop and they can talk to each other, provided they are connected to the same signaling server + +Requires the signaling server to be running. To do go, just go inside the folder /signaling-server and do ``go run .`` + +you can then run the game by going in /game and doing either + +``go run .`` for running the game on desktop + +(see [this tutorial for more information on how to build for WebAssembly](https://ebitengine.org/en/documents/webassembly.html)) + +Click "Host Game" to get the lobby id, and then share that with the other clients to get connected + +To play: Just move around with the arrow keys once you have connected! + +Right now this only supports two clients in the same lobby diff --git a/ebiten-game/game/.gitignore b/ebiten-game/game/.gitignore new file mode 100644 index 00000000..cf14364f --- /dev/null +++ b/ebiten-game/game/.gitignore @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2025 The Pion community +# SPDX-License-Identifier: MIT + +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.wasm + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum +wasm_exec.js diff --git a/ebiten-game/game/gopher.png b/ebiten-game/game/gopher.png new file mode 100644 index 00000000..b11c1703 Binary files /dev/null and b/ebiten-game/game/gopher.png differ diff --git a/ebiten-game/game/gopher.png.license b/ebiten-game/game/gopher.png.license new file mode 100644 index 00000000..71f804a7 --- /dev/null +++ b/ebiten-game/game/gopher.png.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2016 The Go Authors +SPDX-License-Identifier: CC-BY-3.0 \ No newline at end of file diff --git a/ebiten-game/game/index.html b/ebiten-game/game/index.html new file mode 100644 index 00000000..e6b444c4 --- /dev/null +++ b/ebiten-game/game/index.html @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/ebiten-game/game/main.go b/ebiten-game/game/main.go new file mode 100644 index 00000000..b28285af --- /dev/null +++ b/ebiten-game/game/main.go @@ -0,0 +1,533 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strconv" + + _ "image/jpeg" + _ "image/png" + "io" + "log" + "os" + "time" + + "github.com/ebitengine/debugui" + "github.com/pion/webrtc/v4" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/kelindar/binary" +) + +var img *ebiten.Image + +var ( + posX = 40.0 + posY = 40.0 + remotePosX = 40.0 + remotePosY = 40.0 +) + +var lobbyID string + +var signalingIP = "127.0.0.1" +var port = 3000 + +func getSignalingURL() string { + return "http://" + signalingIP + ":" + strconv.Itoa(port) +} + +// players registered by host. +var registeredPlayers = make(map[int]struct{}) + +// client to the HTTP signaling server. +var httpClient = &http.Client{ + Timeout: 10 * time.Second, +} + +func init() { + var err error + img, _, err = ebitenutil.NewImageFromFile("gopher.png") + if err != nil { + log.Fatal(err) + } +} + +// implements ebiten.game interface. +type game struct { + debugUI debugui.DebugUI + inputCapturingState debugui.InputCapturingState + + logBuf string + logSubmitBuf string + logUpdated bool + + lobbyID string + isHost bool + + localDebugInformation string + remoteDebugInformation string +} + +func NewGame() (*game, error) { + g := &game{} + + return g, nil +} + +// Layout implements Game. +func (g *game) Layout(outsideWidth int, outsideHeight int) (int, int) { + return outsideWidth, outsideHeight +} + +// called every tick (default 60 times a second) +// updates game logical state. +func (g *game) Update() error { + if ebiten.IsKeyPressed(ebiten.KeyUp) { + posY-- + } + + if ebiten.IsKeyPressed(ebiten.KeyDown) { + posY++ + } + + if ebiten.IsKeyPressed(ebiten.KeyLeft) { + posX-- + } + + if ebiten.IsKeyPressed(ebiten.KeyRight) { + posX++ + } + + inputCaptured, err := g.debugUI.Update(func(ctx *debugui.Context) error { + g.logWindow(ctx) + + return nil + }) + if err != nil { + return err + } + g.inputCapturingState = inputCaptured + return nil +} + +// called every frame, depends on the monitor refresh rate +// which will probably be at least 60 times per second. +func (g *game) Draw(screen *ebiten.Image) { + // prints something on the screen + debugString := fmt.Sprintf("FPS: %f", ebiten.ActualFPS()) + debugString += "\n" + g.localDebugInformation + "\n" + g.remoteDebugInformation + ebitenutil.DebugPrint(screen, debugString) + + // draw image + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(posX, posY) + screen.DrawImage(img, op) + + // draw remote + op2 := &ebiten.DrawImageOptions{} + op2.GeoM.Translate(remotePosX, remotePosY) + screen.DrawImage(img, op2) + + g.debugUI.Draw(screen) +} + +var ( + // probably move all webrtc networking stuff to a struct i can manage. + peerConnection *webrtc.PeerConnection +) + +const messageSize = 32 + +type playerData struct { + Id int +} + +func (g *game) startConnection() { + // Since this behavior diverges from the WebRTC API it has to be + // enabled using a settings engine. Mixing both detached and the + // OnMessage DataChannel API is not supported. + + // Create a SettingEngine and enable Detach. + s := webrtc.SettingEngine{} + s.DetachDataChannels() + + // Create an API object with the engine. + api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) + + // Everything below is the Pion WebRTC API! Thanks for using it ❤️. + + // Prepare the configuration. + config := webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + } + + // Create a new RTCPeerConnection using the API object. + pc, err := api.NewPeerConnection(config) + if err != nil { + panic(err) + } + + // Set the global variable to the newly created RTCPeerConnection. + peerConnection = pc + + // Set the handler for Peer connection state. + // This will notify you when the peer has connected/disconnected. + peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { + g.writeLog(fmt.Sprintf("Peer Connection State has changed: %s\n", s.String())) + + if s == webrtc.PeerConnectionStateFailed { + // Wait until PeerConnection has had no network activity for 30 seconds or another failure. + // It may be reconnected using an ICE Restart. + // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. + // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. + g.writeLog(fmt.Sprintln("Peer Connection has gone to failed exiting")) + os.Exit(0) + } + + if s == webrtc.PeerConnectionStateClosed { + // PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify + g.writeLog(fmt.Sprintln("Peer Connection has gone to closed exiting")) + os.Exit(0) + } + }) + + // Set the handler for ICE connection state + // This will notify you when the peer has connected/disconnected. + peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { + g.writeLog(fmt.Sprintf("ICE Connection State has changed: %s\n", connectionState.String())) + }) + + // the one that gives the answer is the host. + if g.isHost { //nolint:nestif + g.writeLog("Hosting a lobby") + // Host creates lobby. + lobbyResp, err := httpClient.Get(getSignalingURL() + "/lobby/host") + if err != nil { + panic(err) + } + bodyBytes, err := io.ReadAll(lobbyResp.Body) + if err != nil { + panic(err) + } + lobbyID = string(bodyBytes) + lobbyIDStr := fmt.Sprintf("Lobby ID: %s\n", lobbyID) + g.writeLog(lobbyIDStr) + + // Register data channel creation handling. + peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { + g.writeLog(fmt.Sprintf("New DataChannel %s %d\n", d.Label(), d.ID())) + + // Register channel opening handling. + d.OnOpen(func() { + s := fmt.Sprintf("Data channel '%s'-'%d' open on host side!", d.Label(), d.ID()) + g.writeLog(s) + + // Detach the data channel. + raw, dErr := d.Detach() + if dErr != nil { + panic(dErr) + } + + // Handle reading from the data channel. + go ReadLoop(g, raw) + + // Handle writing to the data channel. + go WriteLoop(g, raw) + }) + }) + + // poll for offer from signaling server for player. + pollForPlayerOffer := func(playerID int) { + ticker := time.NewTicker(1 * time.Second) + for range ticker.C { + g.writeLog(fmt.Sprintf("Polling for offer for %d\n", playerID)) + // hardcode that there is only one other player and they have player_id 1. + getUrl := getSignalingURL() + "/offer/get?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerID) + g.writeLog(fmt.Sprintln(getUrl)) + offerResp, err := httpClient.Get(getUrl) + if err != nil { + panic(err) + } + if offerResp.StatusCode != http.StatusOK { + continue + } + body := new(bytes.Buffer) + _, err = body.ReadFrom(offerResp.Body) + if err != nil { + panic(err) + } + + g.writeLog(fmt.Sprintf("Got offer %v\n", body.String())) + offer := webrtc.SessionDescription{} + err = json.NewDecoder(body).Decode(&offer) + if err != nil { + panic(err) + } + // Set the remote SessionDescription. + err = peerConnection.SetRemoteDescription(offer) + if err != nil { + panic(err) + } + // Create answer. + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } + + // Create channel that is blocked until ICE Gathering is complete. + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + + // Sets the LocalDescription, and starts our UDP listeners. + err = peerConnection.SetLocalDescription(answer) + if err != nil { + panic(err) + } + + // Block until ICE Gathering is complete, disabling trickle ICE. + // we do this because we only can exchange one signaling message + // in a production application you should exchange ICE Candidates via OnICECandidate. + <-gatherComplete + // send answer we generated to the signaling server. + answerJson, err := json.Marshal(peerConnection.LocalDescription()) + if err != nil { + panic(err) + } + postUrl := getSignalingURL() + "/answer/post?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerID) + g.writeLog(fmt.Sprintln(postUrl)) + _, err = httpClient.Post(postUrl, "application/json", bytes.NewBuffer(answerJson)) + if err != nil { + panic(err) + } + + // if we have successfully set the remote description, we can break out of the loop. + ticker.Stop() + + return + } + } + + go func() { + ticker := time.NewTicker(1 * time.Second) + for t := range ticker.C { + g.writeLog(fmt.Sprintln("Polling for lobby ID {", lobbyID, "} at", t)) + idUrl := getSignalingURL() + "/lobby/unregisteredPlayers?id=" + lobbyID + g.writeLog(fmt.Sprintln(idUrl)) + idResp, err := httpClient.Get(idUrl) + if err != nil { + panic(err) + } + if idResp.StatusCode != http.StatusOK { + continue + } + var playerIds []int + err = json.NewDecoder(idResp.Body).Decode(&playerIds) + if err != nil { + panic(err) + } + g.writeLog(fmt.Sprintf("Player IDs: %v\n", playerIds)) + // poll for all of the unregistered players. + for _, playerID := range playerIds { + // only start goroutine if playerID hasn't been registered yet. + if _, ok := registeredPlayers[playerID]; !ok { + registeredPlayers[playerID] = struct{}{} + go pollForPlayerOffer(playerID) + } + } + } + }() + } else { + g.writeLog("Joining lobby: " + lobbyID) + // the following is for the client joining the lobby. + // get lobby id from text input. + lobbyID = g.lobbyID + response, err := httpClient.Get(getSignalingURL() + "/lobby/join?id=" + lobbyID) + if err != nil { + panic(err) + } + var playerData playerData + err = json.NewDecoder(response.Body).Decode(&playerData) + if err != nil { + panic(err) + } + g.writeLog(fmt.Sprintf("Player ID: %v\n", playerData)) + // Create a datachannel with label 'data'. + dataChannel, err := peerConnection.CreateDataChannel("data", nil) + if err != nil { + panic(err) + } + + // Register channel opening handling. + dataChannel.OnOpen(func() { + s := fmt.Sprintf("Data channel '%s'-'%d' open on client side!", dataChannel.Label(), dataChannel.ID()) + g.writeLog(s) + + // Detach the data channel. + raw, dErr := dataChannel.Detach() + if dErr != nil { + panic(dErr) + } + + // Handle reading from the data channel. + go ReadLoop(g, raw) + + // Handle writing to the data channel. + go WriteLoop(g, raw) + }) + + // Create an offer to send to the browser. + offer, err := peerConnection.CreateOffer(nil) + if err != nil { + panic(err) + } + + // Sets the LocalDescription, and starts our UDP listeners. + err = peerConnection.SetLocalDescription(offer) + if err != nil { + panic(err) + } + + // print out possible offers from different ICE Candidates. + peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { + if candidate != nil { + offerJson, err := json.Marshal(peerConnection.LocalDescription()) + if err != nil { + panic(err) + } + postUrl := getSignalingURL() + "/offer/post?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerData.Id) + g.writeLog(fmt.Sprintln(postUrl)) + _, err = httpClient.Post(postUrl, "application/json", bytes.NewBuffer(offerJson)) + if err != nil { + panic(err) + } + } + }) + + answer := webrtc.SessionDescription{} + // read answer from other peer (wait till we actually get something). + ticker := time.NewTicker(1 * time.Second) + go func() { + for range ticker.C { + g.writeLog(fmt.Sprintln("Polling for answer")) + url := getSignalingURL() + "/answer/get?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerData.Id) + fmt.Println(url) + answerResp, err := httpClient.Get(url) + if err != nil { + panic(err) + } + if answerResp.StatusCode != http.StatusOK { + continue + } + body := new(bytes.Buffer) + body.ReadFrom(answerResp.Body) + g.writeLog(fmt.Sprintf("Got answer %v\n", body.String())) + err = json.NewDecoder(body).Decode(&answer) + if err != nil { + panic(err) + } + + if err := peerConnection.SetRemoteDescription(answer); err != nil { + panic(err) + } + + // if we have successfully set the remote description, we can break out of the loop. + ticker.Stop() + + return + } + }() + } +} + +func (g *game) closeConnection() { + if cErr := peerConnection.Close(); cErr != nil { + fmt.Printf("cannot close peerConnection: %v\n", cErr) + } + // this doesn't work, fix this. + if g.isHost { + // delete lobby if host. + url := getSignalingURL() + "/lobby/delete" + fmt.Println(url) + _, err := httpClient.Get(url) + if err != nil { + panic(err) + } + } +} + +// entry point of the program. +func main() { + ebiten.SetWindowSize(640, 480) + ebiten.SetWindowTitle("Hello, World!") + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + + g, err := NewGame() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := ebiten.RunGame(g); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // close the connection when the game ends. + g.closeConnection() +} + +type Packet struct { + PosX float64 + PosY float64 +} + +// ReadLoop shows how to read from the datachannel directly. +func ReadLoop(game *game, d io.Reader) { + for { + buffer := make([]byte, messageSize) + _, err := io.ReadFull(d, buffer) + if err != nil { + game.writeLog(fmt.Sprintln("Datachannel closed; Exit the readloop:", err)) + + return + } + + var packet Packet + err = binary.Unmarshal(buffer, &packet) + if err != nil { + panic(err) + } + + remotePosX = packet.PosX + remotePosY = packet.PosY + + game.remoteDebugInformation = fmt.Sprintf("Message from DataChannel: %f %f", packet.PosX, packet.PosY) + } +} + +// WriteLoop shows how to write to the datachannel directly. +func WriteLoop(g *game, d io.Writer) { + ticker := time.NewTicker(time.Millisecond * 20) + defer ticker.Stop() + for range ticker.C { + packet := &Packet{posX, posY} + g.localDebugInformation = fmt.Sprintf("Sending x:%f y:%f", packet.PosX, packet.PosY) + encoded, err := binary.Marshal(packet) + if err != nil { + panic(err) + } + + if _, err := d.Write(encoded); err != nil { + panic(err) + } + } +} diff --git a/ebiten-game/game/ui.go b/ebiten-game/game/ui.go new file mode 100644 index 00000000..ad2e94ce --- /dev/null +++ b/ebiten-game/game/ui.go @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors +// SPDX-License-Identifier: MIT + +package main + +import ( + "image" + + "github.com/ebitengine/debugui" + "github.com/hajimehoshi/ebiten/v2" +) + +func (g *game) writeLog(text string) { + if len(g.logBuf) > 0 { + g.logBuf += "\n" + } + g.logBuf += text + g.logUpdated = true +} + +func (g *game) logWindow(ctx *debugui.Context) { + ctx.Window("Log Window", image.Rect(350, 40, 650, 290), func(layout debugui.ContainerLayout) { + ctx.SetGridLayout([]int{-1}, []int{-1, 0}) + ctx.Panel(func(layout debugui.ContainerLayout) { + ctx.SetGridLayout([]int{-1}, []int{-1}) + ctx.Text(g.logBuf) + if g.logUpdated { + ctx.SetScroll(image.Pt(layout.ScrollOffset.X, layout.ContentSize.Y)) + g.logUpdated = false + } + }) + ctx.GridCell(func(bounds image.Rectangle) { + submitOpen := func() { + g.isHost = true + g.startConnection() + } + + submitJoin := func() { + g.isHost = false + if g.logSubmitBuf == "" { + return + } + g.lobbyID = g.logSubmitBuf + g.logSubmitBuf = "" + g.startConnection() + } + + ctx.SetGridLayout([]int{-1, -1, -1, -1}, nil) + ctx.Text("Lobby ID:") + ctx.TextField(&g.logSubmitBuf).On(func() { + if ebiten.IsKeyPressed(ebiten.KeyEnter) { + submitJoin() + ctx.SetTextFieldValue(g.logSubmitBuf) + } + }) + ctx.Button("Open").On(func() { + submitOpen() + }) + ctx.Button("Join").On(func() { + submitJoin() + }) + }) + }) +} diff --git a/ebiten-game/signaling-server/.gitignore b/ebiten-game/signaling-server/.gitignore new file mode 100644 index 00000000..d70c6d48 --- /dev/null +++ b/ebiten-game/signaling-server/.gitignore @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 The Pion community +# SPDX-License-Identifier: MIT + +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum diff --git a/ebiten-game/signaling-server/README.md b/ebiten-game/signaling-server/README.md new file mode 100644 index 00000000..cc8138ac --- /dev/null +++ b/ebiten-game/signaling-server/README.md @@ -0,0 +1,3 @@ +# go signaling server + +to run just do ``go run .`` diff --git a/ebiten-game/signaling-server/main.go b/ebiten-game/signaling-server/main.go new file mode 100644 index 00000000..e0df3255 --- /dev/null +++ b/ebiten-game/signaling-server/main.go @@ -0,0 +1,423 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "math/rand" + "net/http" + "strconv" + "sync" + + "github.com/pion/webrtc/v4" + "github.com/rs/cors" +) + +type clientConnection struct { + IsHost bool + Offer *webrtc.SessionDescription + Answer *webrtc.SessionDescription +} + +type lobby struct { + mutex sync.Mutex + // host is first client in lobby.Clients + Clients []clientConnection +} + +var lobbyList = map[string]*lobby{} + +type playerData struct { + // player id is index in lobby.Clients + Id int +} + +var ( + errLobbyNotFound = errors.New("lobby not found") + errPlayerNotFound = errors.New("player not found") +) + +func generateNewLobbyID() string { + letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + + // have random size for lobby id + size := 6 + buffer := make([]rune, size) + for i := range buffer { + buffer[i] = letters[rand.Intn(len(letters))] //nolint:gosec + } + id := string(buffer) + + // check if room id is already in lobby_list + _, ok := lobbyList[id] + if ok { + // if it already exists, call function again + return generateNewLobbyID() + } + + return id +} + +func makeLobby() string { + lobby := lobby{} + lobby.Clients = []clientConnection{} + // first client is always host + lobbyID := generateNewLobbyID() + lobbyList[lobbyID] = &lobby + + return lobbyID +} + +func getLobbyIDs() []string { + lobbies := make([]string, len(lobbyList)) + i := 0 + for k := range lobbyList { + lobbies[i] = k + i++ + } + + return lobbies +} + +func main() { + mux := http.NewServeMux() + mux.Handle("/", http.FileServer(http.Dir("./public"))) + mux.HandleFunc("/lobby/host", lobbyHost) + mux.HandleFunc("/lobby/join", lobbyJoin) + mux.HandleFunc("/lobby/delete", lobbyDelete) + mux.HandleFunc("/lobby/unregisteredPlayers", lobbyUnregisteredPlayers) + mux.HandleFunc("/offer/get", offerGet) + mux.HandleFunc("/offer/post", offerPost) + mux.HandleFunc("/answer/get", answerGet) + mux.HandleFunc("/answer/post", answerPost) + mux.HandleFunc("/ice", ice) + + fmt.Println("Server started on port 3000") + // cors.Default() setup the middleware with default options being + // all origins accepted with simple methods (GET, POST). See + // documentation below for more options. + handler := cors.Default().Handler(mux) + err := http.ListenAndServe(":3000", handler) //nolint:gosec + if err != nil { + fmt.Printf("Failed to start server: %s", err) + + return + } +} + +func lobbyHost(res http.ResponseWriter, _ *http.Request) { + lobbyID := makeLobby() + lobby := lobbyList[lobbyID] + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + // host is first client in lobby.Clients + lobby.Clients = append(lobby.Clients, clientConnection{IsHost: true}) + // return lobby id to host + _, err := io.Writer.Write(res, []byte(lobbyID)) + if err != nil { + fmt.Printf("Failed to write lobby_id: %s", err) + + return + } + fmt.Println("lobbyHost") + fmt.Printf("lobby added: %s\n", lobbyID) + // print all lobbies + fmt.Printf("lobby_list:%s\n", getLobbyIDs()) +} + +// call "/lobby?id={lobby_id}" to connect to lobby. +func lobbyJoin(res http.ResponseWriter, req *http.Request) { + fmt.Println("lobbyJoin") + res.Header().Set("Content-Type", "application/json") + // https://freshman.tech/snippets/go/extract-url-query-params/ + // get lobby id from query params + lobbyID := req.URL.Query().Get("id") + fmt.Printf("lobby_id: %s\n", lobbyID) + + // only continue with connection if lobby exists + lobby, ok := lobbyList[lobbyID] + // If the key doesn't exist, return error + if !ok { + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte("404 - Lobby not found")) + if err != nil { + fmt.Printf("Failed to write lobby_not_found: %s", err) + + return + } + + return + } + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + body, err := io.ReadAll(req.Body) + if err != nil { + fmt.Printf("Failed to read body: %s", err) + + return + } + + fmt.Printf("body: %s", body) + + // send player id once generated + lobby.Clients = append(lobby.Clients, clientConnection{IsHost: false}) + // player id is index in lobby.Clients + playerID := len(lobby.Clients) - 1 + fmt.Printf("player_id: %d\n", playerID) + fmt.Println(lobby.Clients) + playerData := playerData{Id: playerID} + + jsonValue, err := json.Marshal(playerData) + if err != nil { + fmt.Printf("Failed to marshal player_data: %s", err) + + return + } + + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write player_data: %s", err) + + return + } +} + +func lobbyDelete(res http.ResponseWriter, req *http.Request) { + fmt.Println("lobbyDelete") + res.Header().Set("Content-Type", "application/json") + // https://freshman.tech/snippets/go/extract-url-query-params/ + // get lobby id from query params + lobbyID := req.URL.Query().Get("id") + fmt.Printf("lobby_id: %s\n", lobbyID) + // delete lobby + delete(lobbyList, lobbyID) + fmt.Printf("lobby_list:%s\n", getLobbyIDs()) +} + +// return players who haven't been registered yet by the host. +func lobbyUnregisteredPlayers(res http.ResponseWriter, req *http.Request) { + fmt.Println("UnregisteredPlayers") + res.Header().Set("Content-Type", "application/json") + // https://freshman.tech/snippets/go/extract-url-query-params/ + // get lobby id from query params + lobbyID := req.URL.Query().Get("id") + lobby := lobbyList[lobbyID] + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + // get all players who haven't been registered yet + playerIDs := []int{} + for i, client := range lobby.Clients { + if !client.IsHost && client.Answer == nil { + playerIDs = append(playerIDs, i) + } + } + + // return lobby id to host + jsonValue, err := json.Marshal(playerIDs) + if err != nil { + fmt.Printf("Failed to marshal player_ids: %s", err) + + return + } + + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write player_ids: %s", err) + + return + } + + fmt.Printf("player_ids %v\n", playerIDs) +} + +func validatePlayer(res http.ResponseWriter, req *http.Request) (*lobby, int, error) { + fmt.Println("validatePlayer") + lobbyID := req.URL.Query().Get("lobby_id") + + // only continue with connection if lobby exists + lobby, ok := lobbyList[lobbyID] + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + // If the key doesn't exist, return error + if !ok { + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte("404 - Lobby not found")) + if err != nil { + fmt.Printf("Failed to write lobby_not_found: %s", err) + + return nil, 0, errLobbyNotFound + } + + return nil, 0, errLobbyNotFound + } + + playerIDString := req.URL.Query().Get("player_id") + playerID, err := strconv.Atoi(playerIDString) + if err != nil { + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Player not found")) + if err != nil { + fmt.Printf("Failed to write player_not_found: %s", err) + + return nil, 0, errPlayerNotFound + } + + return nil, 0, errPlayerNotFound + } + + // check if player actually exists + if playerID < 0 || playerID >= len(lobby.Clients) { + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Player not found")) + if err != nil { + fmt.Printf("Failed to write player_not_found: %s", err) + + return nil, 0, errPlayerNotFound + } + + return nil, 0, errPlayerNotFound + } + + return lobby, playerID, nil +} + +func offerGet(res http.ResponseWriter, req *http.Request) { + fmt.Println("offerGet") + res.Header().Set("Content-Type", "application/json") + + lobby, playerID, err := validatePlayer(res, req) + if err != nil { + return + } + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + offer := lobby.Clients[playerID].Offer + if offer == nil { + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Offer not found")) + if err != nil { + fmt.Printf("Failed to write offer: %s", err) + + return + } + + return + } + + jsonValue, err := json.Marshal(offer) + if err != nil { + fmt.Printf("Failed to marshal offer: %s", err) + + return + } + + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write offer: %s", err) + + return + } +} + +func offerPost(res http.ResponseWriter, req *http.Request) { + fmt.Println("offerPost") + + lobby, playerID, err := validatePlayer(res, req) + if err != nil { + return + } + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + var sdp webrtc.SessionDescription + + // Try to decode the request body into the struct. If there is an error, + // respond to the client with the error message and a 400 status code. + err = json.NewDecoder(req.Body).Decode(&sdp) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + + return + } + + lobby.Clients[playerID].Offer = &sdp + fmt.Printf("Lobby: %+v\n", lobby.Clients) +} + +func answerGet(res http.ResponseWriter, req *http.Request) { + fmt.Println("answerGet") + res.Header().Set("Content-Type", "application/json") + + lobby, playerID, err := validatePlayer(res, req) + if err != nil { + return + } + + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + answer := lobby.Clients[playerID].Answer + if answer == nil { + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Answer not found")) + if err != nil { + fmt.Printf("Failed to write answer: %s", err) + + return + } + + return + } + + jsonValue, err := json.Marshal(answer) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + + return + } + + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write answer: %s", err) + + return + } +} + +func answerPost(res http.ResponseWriter, req *http.Request) { + fmt.Println("answerPost") + res.Header().Set("Content-Type", "application/json") + + lobby, playerID, err := validatePlayer(res, req) + if err != nil { + return + } + + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + var sdp webrtc.SessionDescription + + // Try to decode the request body into the struct. If there is an error, + // respond to the client with the error message and a 400 status code. + err = json.NewDecoder(req.Body).Decode(&sdp) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + + return + } + + lobby.Clients[playerID].Answer = &sdp + fmt.Printf("Lobby: %+v\n", lobby.Clients) +} + +func ice(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") +} diff --git a/go.mod b/go.mod index b8711c42..989b85ba 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,19 @@ module github.com/pion/example-webrtc-applications/v3 -go 1.22 +go 1.23.0 toolchain go1.23.6 require ( github.com/asticode/go-astiav v0.19.0 github.com/at-wat/ebml-go v0.17.1 + github.com/ebitengine/debugui v0.1.1 github.com/emiago/sipgo v0.29.0 github.com/go-gst/go-gst v1.3.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/hajimehoshi/ebiten/v2 v2.8.8 + github.com/kelindar/binary v1.0.19 github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e github.com/pion/interceptor v0.1.37 github.com/pion/logging v0.2.3 @@ -18,6 +21,7 @@ require ( github.com/pion/rtp v1.8.15 github.com/pion/sdp/v3 v3.0.11 github.com/pion/webrtc/v4 v4.1.0 + github.com/rs/cors v1.11.1 gocv.io/x/gocv v0.40.0 golang.org/x/image v0.23.0 golang.org/x/net v0.35.0 @@ -25,14 +29,21 @@ require ( require ( github.com/asticode/go-astikit v0.42.0 // indirect + github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect + github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/purego v0.8.0 // indirect github.com/go-gst/go-glib v1.3.0 // indirect + github.com/go-text/typesetting v0.2.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.2 // indirect + github.com/hajimehoshi/bitmapfont/v3 v3.2.1 // indirect github.com/icholy/digest v0.1.22 // indirect + github.com/jezek/xgb v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-pointer v0.0.1 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect github.com/pion/ice/v4 v4.0.10 // indirect @@ -43,11 +54,14 @@ require ( github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/xid v1.5.0 // indirect github.com/rs/zerolog v1.33.0 // indirect github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index c5cb66e2..8ffbce3b 100644 --- a/go.sum +++ b/go.sum @@ -7,12 +7,24 @@ github.com/at-wat/ebml-go v0.17.1/go.mod h1:w1cJs7zmGsb5nnSvhWGKLCxvfu4FVx5ERvYD github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/debugui v0.1.1 h1:3b4cIujNvKYhozlBjag9TWuuyo67Di3DgXWAz+f7xAY= +github.com/ebitengine/debugui v0.1.1/go.mod h1:wIKIq5RvNFb3+nFfJcYqSLvpC1ioD/BglWDtuWFSDz0= +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= +github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= +github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= +github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= +github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emiago/sipgo v0.29.0 h1:dg/FwwhSl6hQTiOTIHzcqemZm3tB7jvGQgIlJmuD2Nw= github.com/emiago/sipgo v0.29.0/go.mod h1:ZQ/tl5t+3assyOjiKw/AInPkcawBJ2Or+d5buztOZsc= github.com/go-gst/go-glib v1.3.0 h1:u+mPUdLmrDFA/MskIxInJY+M0O1RSkHeZYggnJGWlPk= github.com/go-gst/go-glib v1.3.0/go.mod h1:JybIYeoHNwCkHGaBf1fHNIaM4sQTrJPkPLsi7dmPNOU= github.com/go-gst/go-gst v1.3.0 h1:z4mQ7CNJXd6ZfkibzIT9kZKwtgEFJo7jJGlX9cXFzz0= github.com/go-gst/go-gst v1.3.0/go.mod h1:2li6ghiCBz7/R6DA7itVto3gsYh0QKicwSxEefNVYqE= +github.com/go-text/typesetting v0.2.0 h1:fbzsgbmk04KiWtE+c3ZD4W2nmCRzBqrqQOvYlwAOdho= +github.com/go-text/typesetting v0.2.0/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I= +github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY= +github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -27,8 +39,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hajimehoshi/bitmapfont/v3 v3.2.1 h1:33Lw85DZolX3upouUqf6Qza8HYGIROvr7SYin7PzIZ8= +github.com/hajimehoshi/bitmapfont/v3 v3.2.1/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg= +github.com/hajimehoshi/ebiten/v2 v2.8.8 h1:xyMxOAn52T1tQ+j3vdieZ7auDBOXmvjUprSrxaIbsi8= +github.com/hajimehoshi/ebiten/v2 v2.8.8/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs= github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM= github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/kelindar/binary v1.0.19 h1:DNyQCtKjkLhBh9pnP49OWREddLB0Mho+1U/AOt/Qzxw= +github.com/kelindar/binary v1.0.19/go.mod h1:/twdz8gRLNMffx0U4UOgqm1LywPs6nd9YK2TX52MDh8= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -43,6 +63,8 @@ github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e h1:L1QWI1FyFkgLOLSP/BlbkLiyLyqUuyxCCRJyULDinx8= github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e/go.mod h1:BN/Txse3qz8tZOmCm2OfajB2wHVujWmX3o9nVdsI6gE= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= @@ -75,12 +97,17 @@ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pion/webrtc/v4 v4.1.0 h1:yq/p0G5nKGbHISf0YKNA8Yk+kmijbblBvuSLwaJ4QYg= github.com/pion/webrtc/v4 v4.1.0/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= @@ -92,27 +119,88 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gocv.io/x/gocv v0.40.0 h1:kGBu/UVj+dO6A9dhQmGOnCICSL7ke7b5YtX3R3azdXI= gocv.io/x/gocv v0.40.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=