123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- // This deals with calls coming from Second Life or OpenSimulator.
- // it's essentially a RESTful thingy
- package main
- import (
- _ "github.com/go-sql-driver/mysql"
- "crypto/md5"
- "database/sql"
- "encoding/hex"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- )
- // GetMD5Hash takes a string which is to be encoded using MD5 and returns a string with the hex-encoded MD5 sum.
- // Got this from https://gist.github.com/sergiotapia/8263278
- func GetMD5Hash(text string) string {
- hasher := md5.New()
- hasher.Write([]byte(text))
- return hex.EncodeToString(hasher.Sum(nil))
- }
- // updateInventory updates the inventory of the object (object key will come in the headers).
- func updateInventory(w http.ResponseWriter, r *http.Request) {
- // get all parameters in array
- err := r.ParseForm()
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)
- if r.Form.Get("signature") != "" && r.Header.Get("X-Secondlife-Object-Key") != "" {
- signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" + LSLSignaturePIN)
- if signature != r.Form.Get("signature") {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature does not match - hack attempt?")
- return
- }
- // open database connection and see if we can update the inventory for this object
- db, err := sql.Open(PDO_Prefix, GoBotDSN)
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed: %s\n", err)
- defer db.Close()
- stmt, err := db.Prepare("REPLACE INTO Inventory (`UUID`, `Name`, `Type`, `Permissions`, `LastUpdate`) VALUES (?,?,?,?,?)");
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace prepare failed:", err)
- defer stmt.Close()
- _, err = stmt.Exec(
- r.Header.Get("X-Secondlife-Object-Key"),
- r.Form.Get("name"),
- r.Form.Get("itemType"),
- r.Form.Get("permissions"),
- r.Form.Get("timestamp"),
- )
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)
- //_, err := res.RowsAffected()
- //checkErr(err)
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-type", "text/plain; charset=utf-8")
- fmt.Fprintf(w, "%s successfully updated!", r.Header.Get("X-Secondlife-Object-Key"))
- return
- } else {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
- return
- }
- /*
- fmt.Fprintf(w, "Root URL is: %s\n", updateInventory()) // send data to client side
- r.ParseForm() // parse arguments, you have to call this by yourself
- Log.Debug(r.Form) // print form information in server side
- Log.Debug("header connection: ", r.Header.Get("Connection"))
- Log.Debug("all headers:")
- for k, v := range r.Header {
- Log.Debug("key:", k)
- Log.Debug("val:", strings.Join(v, ""))
- }
- Log.Debug("path", r.URL.Path)
- Log.Debug("scheme", r.URL.Scheme)
- lLog.Debugr.Form["url-long"])
- for k, v := range r.Form {
- Log.Debug("key:", k)
- Log.Debug("val:", strings.Join(v, ""))
- }
- if r.Form["signature"] != nil {
- fmt.Fprintf(w, "Signature is %s\n", r.Form.Get("signature"))
- }
- */
- }
- // updateSensor updates the Obstacles database with an additional object found by the sensors.
- func updateSensor(w http.ResponseWriter, r *http.Request) {
- if r.Header.Get("X-Secondlife-Object-Key") != "" {
- err := r.ParseForm()
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)
- // open database connection and see if we can update the inventory for this object
- db, err := sql.Open(PDO_Prefix, GoBotDSN)
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)
- defer db.Close()
- stmt, err := db.Prepare("REPLACE INTO Obstacles (`UUID`, `Name`, `BotKey`, `BotName`, `Type`, `Origin`, `Position`, `Rotation`, `Velocity`, `Phantom`, `Prims`, `BBHi`, `BBLo`, `LastUpdate`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace prepare failed:", err)
- defer stmt.Close()
- _, err = stmt.Exec(
- r.Form.Get("key"),
- r.Form.Get("name"),
- r.Header.Get("X-Secondlife-Object-Key"),
- r.Header.Get("X-Secondlife-Object-Name"),
- r.Form.Get("type"),
- r.Form.Get("origin"),
- strings.Trim(r.Form.Get("pos"), "<>()"),
- strings.Trim(r.Form.Get("rot"), "<>()"),
- strings.Trim(r.Form.Get("vel"), "<>()"),
- r.Form.Get("phantom"),
- r.Form.Get("prims"),
- strings.Trim(r.Form.Get("bbhi"), "<>()"),
- strings.Trim(r.Form.Get("bblo"), "<>()"),
- r.Form.Get("timestamp"),
- )
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)
- //_, err := res.RowsAffected()
- //checkErr(err)
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-type", "text/plain; charset=utf-8")
- reply := r.Header.Get("X-Secondlife-Owner-Name") + " sent us:\n" +
- "Key: " + r.Form.Get("key") + " Name: " + r.Form.Get("name")+ "\n" +
- "Position: " + r.Form.Get("pos") + " Rotation: " + r.Form.Get("rot") + "\n" +
- "Type: " + r.Form.Get("type") + "\n" + "Origin: " + r.Form.Get("origin") + "\n" +
- "Velocity: " + r.Form.Get("vel") +
- " Phantom: " + r.Form.Get("phantom") +
- " Prims: " + r.Form.Get("prims") + "\n" +
- "BB high: " + r.Form.Get("bbhi") +
- " BB low: " + r.Form.Get("bblo") + "\n" +
- "Timestamp: " + r.Form.Get("timestamp")
- fmt.Fprint(w, reply)
- return
- } else {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Not called from within the virtual world.")
- return
- }
- }
- // registerPosition saves a HTTP URL for a single object, making it persistent.
- // POST parameters:
- // permURL: a permanent URL from llHTTPServer
- // signature: to make spoofing harder
- // timestamp: in-world timestamp retrieved with llGetTimestamp()
- func registerPosition(w http.ResponseWriter, r *http.Request) {
- // get all parameters in array
- err := r.ParseForm()
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)
- // Log.Debug("Received: ", r) // we know this works well (20170725)
- if r.Header.Get("X-Secondlife-Object-Key") == "" {
- // Log.Debugf("Got '%s'\n", r.Header["X-Secondlife-Object-Key"])
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Not called from within the virtual world.")
- return
- }
- if r.Form.Get("signature") != "" {
- // if we don't have the permURL to store, registering this object is pointless
- if r.Form["permURL"] == nil {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": No PermURL specified")
- return
- }
- signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" + LSLSignaturePIN)
- /* Log.Debugf("%s: Calculating signature and comparing with what we got: Object Key: '%s' Timestamp: '%s' PIN: '%s' LSL signature: '%s' Our signature: %s\n",
- funcName(),
- r.Header.Get("X-Secondlife-Object-Key"),
- r.Form.Get("timestamp"),
- LSLSignaturePIN,
- r.Form.Get("signature"),
- signature) */
- if signature != r.Form.Get("signature") {
- logErrHTTP(w, http.StatusServiceUnavailable, funcName() + ": Signature does not match - hack attempt?")
- return
- }
- // open database connection and see if we can update the inventory for this object
- db, err := sql.Open(PDO_Prefix, GoBotDSN)
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)
- defer db.Close()
- stmt, err := db.Prepare("REPLACE INTO Positions (`UUID`, `Name`, `PermURL`, `Location`, `Position`, `Rotation`, `Velocity`, `OwnerKey`, `OwnerName`, `ObjectType`, `ObjectClass`, `RateEnergy`, `RateMoney`, `RateHappiness`, `LastUpdate`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace prepare failed: %s\n", err)
- defer stmt.Close()
- _, err = stmt.Exec(
- r.Header.Get("X-Secondlife-Object-Key"),
- r.Header.Get("X-Secondlife-Object-Name"),
- r.Form.Get("permURL"),
- r.Header.Get("X-Secondlife-Region"),
- strings.Trim(r.Header.Get("X-Secondlife-Local-Position"), "<>()"),
- strings.Trim(r.Header.Get("X-Secondlife-Local-Rotation"), "<>()"),
- strings.Trim(r.Header.Get("X-Secondlife-Local-Velocity"), "<>()"),
- r.Header.Get("X-Secondlife-Owner-Key"),
- r.Header.Get("X-Secondlife-Owner-Name"),
- r.Form.Get("objecttype"),
- r.Form.Get("objectclass"),
- r.Form.Get("rateenergy"),
- r.Form.Get("ratemoney"),
- r.Form.Get("ratehappiness"),
- r.Form.Get("timestamp"),
- )
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)
- //_, err := res.RowsAffected()
- //checkErr(err)
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-type", "text/plain; charset=utf-8")
- fmt.Fprintf(w, "'%s' successfully updated!", r.Header.Get("X-Secondlife-Object-Name"))
- // Log.Debugf("These are the headers I got: %v\nAnd these are the parameters %v\n", r.Header, r.Form)
- return
- } else {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
- return
- }
- }
- // registerAgent saves a HTTP URL for a single agent, making it persistent.
- // POST parameters:
- // permURL: a permanent URL from llHTTPServer
- // signature: to make spoofing harder
- // timestamp: in-world timestamp retrieved with llGetTimestamp()
- // request: currently only delete (to remove entry from database when the bot dies)
- func registerAgent(w http.ResponseWriter, r *http.Request) {
- // get all parameters in array
- err := r.ParseForm()
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)
- if r.Header.Get("X-Secondlife-Object-Key") == "" {
- // Log.Debugf("Got '%s'\n", r.Header["X-Secondlife-Object-Key"])
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Only in-world requests allowed.")
- return
- }
- if r.Form.Get("signature") == "" {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
- return
- }
- signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" + LSLSignaturePIN)
- if signature != r.Form.Get("signature") {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature does not match - hack attempt?")
- return
- }
- // open database connection and see if we can update the inventory for this object
- db, err := sql.Open(PDO_Prefix, GoBotDSN)
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)
- defer db.Close()
- if r.Form.Get("permURL") != "" { // bot registration
- stmt, err := db.Prepare("REPLACE INTO Agents (`UUID`, `Name`, `OwnerKey`, `OwnerName`, `PermURL`, `Location`, `Position`, `Rotation`, `Velocity`, `Energy`, `Money`, `Happiness`, `Class`, `SubType`, `LastUpdate`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, "Replace prepare failed: %s\n", err)
- defer stmt.Close()
- _, err = stmt.Exec(
- r.Header.Get("X-Secondlife-Object-Key"),
- r.Header.Get("X-Secondlife-Object-Name"),
- r.Header.Get("X-Secondlife-Owner-Key"),
- r.Header.Get("X-Secondlife-Owner-Name"),
- r.Form.Get("permURL"),
- r.Header.Get("X-Secondlife-Region"),
- strings.Trim(r.Header.Get("X-Secondlife-Local-Position"), "<>()"),
- strings.Trim(r.Header.Get("X-Secondlife-Local-Rotation"), "<>()"),
- strings.Trim(r.Header.Get("X-Secondlife-Local-Velocity"), "<>()"),
- r.Form.Get("energy"),
- r.Form.Get("money"),
- r.Form.Get("happiness"),
- r.Form.Get("class"),
- r.Form.Get("subtype"),
- r.Form.Get("timestamp"),
- )
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-type", "text/plain; charset=utf-8")
- replyText := "'" + r.Header.Get("X-Secondlife-Object-Name") +
- "' successfully updated object for NPC '" +
- r.Header.Get("X-Secondlife-Owner-Name") + "' (" +
- r.Header.Get("X-Secondlife-Owner-Key") + "), energy=" +
- r.Form.Get("energy") + ", money=" +
- r.Form.Get("money") + ", happiness=" +
- r.Form.Get("happiness") + ", class=" +
- r.Form.Get("class") + ", subtype=" +
- r.Form.Get("subtype") + "."
- fmt.Fprint(w, replyText)
- // log.Printf(replyText) // debug
- } else if r.Form.Get("request") == "delete" { // other requests, currently only deletion
- stmt, err := db.Prepare("DELETE FROM Agents WHERE UUID=?")
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Delete agent prepare failed:", err)
- defer stmt.Close()
- _, err = stmt.Exec(r.Form.Get("npc"))
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Delete agent exec failed:", err)
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-type", "text/plain; charset=utf-8")
- fmt.Fprintf(w, "'%s' successfully deleted.", r.Form.Get("npc"))
- return
- }
- }
- // configureCube Support scripts for remote startup configuration for the cubes.
- // This basically gives the lists of options (e.g. energy, happiness; classes of NPCs, etc.) so
- // that we don't need to hardcode them
- func configureCube(w http.ResponseWriter, r *http.Request) {
- logErrHTTP(w, http.StatusNotImplemented, funcName() + ": configureCube not implemented")
- // return
- }
- // processCube is called when an agent sits on the cube; it will update the agent's money/energy/happiness.
- // Once everything is updated, the agent will get kicked out of the cube after a certain time has elapsed. This happens in-world.
- // We might also animate the avatar depending on its class, subclass, etc.
- func processCube(w http.ResponseWriter, r *http.Request) {
- err := r.ParseForm()
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Extracting parameters failed:", err)
- if r.Header.Get("X-Secondlife-Object-Key") == "" {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Only in-world requests allowed.")
- return
- }
- if r.Form.Get("signature") == "" {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature not found")
- return
- }
- signature := GetMD5Hash(r.Header.Get("X-Secondlife-Object-Key") + r.Form.Get("timestamp") + ":" + LSLSignaturePIN)
- if signature != r.Form.Get("signature") {
- logErrHTTP(w, http.StatusForbidden, funcName() + ": Signature does not match - hack attempt?")
- return
- }
- if r.Form.Get("avatar") == NullUUID { // happens when standing up, we return without giving errors
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-type", "text/plain; charset=utf-8")
- fmt.Fprintf(w, "Got an avatar/NPC standing up")
- sendMessageToBrowser("status", "success", "Got an avatar/NPC standing up", "")
- return
- }
- // all checks fine, now let's get the agent data from the database:
- // allegedly, we get avatar=[UUID] — all the rest ought to be on the database (20170807)
- db, err := sql.Open(PDO_Prefix, GoBotDSN)
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Connect failed:", err)
- defer db.Close()
- Log.Debug("processCube called, avatar UUID is", r.Form.Get("avatar"), "cube UUID is", r.Header.Get("X-Secondlife-Object-Key"))
- var agent AgentType
- err = db.QueryRow("SELECT * FROM Agents WHERE OwnerKey=?", r.Form.Get("avatar")).Scan(
- &agent.UUID,
- &agent.Name,
- &agent.OwnerName,
- &agent.OwnerKey,
- &agent.Location,
- &agent.Position,
- &agent.Rotation,
- &agent.Velocity,
- &agent.Energy,
- &agent.Money,
- &agent.Happiness,
- &agent.Class,
- &agent.SubType,
- &agent.PermURL,
- &agent.LastUpdate,
- &agent.BestPath,
- &agent.SecondBestPath,
- &agent.CurrentTarget,
- )
- if err != nil || !agent.UUID.Valid {
- // this can happen when an avatar sits on the object, or a NPC that never got a chance to register, or even because of bugs
- // or cubes which did not have llSitTarget() initialised (20170811)
- checkErrHTTP(w, http.StatusNotFound, funcName() + " Agent UUID not found in database or invalid:", err)
- return
- }
- // get information on this cube
- var cube PositionType
- err = db.QueryRow("SELECT * FROM Positions WHERE UUID=?", r.Header.Get("X-Secondlife-Object-Key")).Scan(
- &cube.PermURL,
- &cube.UUID,
- &cube.Name,
- &cube.OwnerName,
- &cube.Location,
- &cube.Position,
- &cube.Rotation,
- &cube.Velocity,
- &cube.LastUpdate,
- &cube.OwnerKey,
- &cube.ObjectType,
- &cube.ObjectClass,
- &cube.RateEnergy,
- &cube.RateMoney,
- &cube.RateHappiness,
- )
- if err != nil || !cube.UUID.Valid {
- checkErrPanicHTTP(w, http.StatusNotFound, funcName() + " This cube's UUID was not found in database or is invalid! We should reset it in-world. Error was:", err)
- }
- // we update the avatar's money, happiness etc. by the rate of the object
- energyAgent, err := strconv.ParseFloat(*agent.Energy.Ptr(), 64) // get this as a float or else all hell will break loose!
- checkErr(err)
- energyCube, err := strconv.ParseFloat(*cube.RateEnergy.Ptr(), 64)
- checkErr(err)
- energyAgent += energyCube
- // in theory, this should now go to the engine, to see if it's enough or not; we'll see what happens (20170808)
- // We now call the agent and tell him about the new values:
- rsBody, err := callURL(*agent.PermURL.Ptr(), fmt.Sprintf("command=setEnergy&float=%f", energyAgent))
- if (err != nil) {
- sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in-world with new energy settings; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
- } else {
- Log.Debug("Result from updating energy of ", *agent.Name.Ptr(), ":", rsBody)
- }
- // now do it for money!
- moneyAgent, err := strconv.ParseFloat(*agent.Money.Ptr(), 64)
- checkErr(err)
- moneyCube, err := strconv.ParseFloat(*cube.RateMoney.Ptr(), 64)
- checkErr(err)
- moneyAgent += moneyCube
- rsBody, err = callURL(*agent.PermURL.Ptr(), fmt.Sprintf("command=setMoney&float=%f", moneyAgent))
- if (err != nil) {
- sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in-world with new money settings; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
- } else {
- Log.Debug("Result from updating money of ", *agent.Name.Ptr(), ":", rsBody)
- }
- // last but not least, make the agent more happy!
- happinessAgent, err := strconv.ParseFloat(*agent.Happiness.Ptr(), 64)
- checkErr(err)
- happinessCube, err := strconv.ParseFloat(*cube.RateHappiness.Ptr(), 64)
- checkErr(err)
- happinessAgent += happinessCube
- rsBody, err = callURL(*agent.PermURL.Ptr(), fmt.Sprintf("command=setHappiness&float=%f", happinessAgent))
- if (err != nil) {
- sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in-world with new happiness settings; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
- } else {
- Log.Debug("Result from updating happiness of ", *agent.Name.Ptr(), ":", rsBody)
- }
- // stand up the bot, he's done his work
- rsBody, err = callURL(*agent.PermURL.Ptr(), "command=osNpcStand")
- if (err != nil) {
- sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be made to stand-up; in-world reply was: '%s'", *agent.Name.Ptr(), rsBody), "")
- } else {
- Log.Debug("Result from making", *agent.Name.Ptr(), "stand up:", rsBody)
- }
- // the next step is to update the database with the new values
- stmt, err := db.Prepare("UPDATE Agents SET `Energy`=?, `Money`=?, `Happiness`=? WHERE UUID=?")
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Update prepare failed:", err)
- if (err != nil) {
- sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in database with new energy/money/happiness settings; database reply was: '%v'", *agent.Name.Ptr(), err), "")
- }
- defer stmt.Close()
- _, err = stmt.Exec(energyAgent, moneyAgent, happinessAgent, *agent.UUID.Ptr())
- checkErrPanicHTTP(w, http.StatusServiceUnavailable, funcName() + ": Replace exec failed:", err)
- if (err != nil) {
- sendMessageToBrowser("status", "error", fmt.Sprintf("Agent '%s' could not be updated in database with new energy/money/happiness settings; database reply was: '%v'", *agent.Name.Ptr(), err), "")
- }
- Log.Debug("Agent", *agent.Name.Ptr(), "updated database with new energy:", energyAgent, "money:", moneyAgent, "happiness:", happinessAgent)
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-type", "text/plain; charset=utf-8")
- fmt.Fprintf(w, "'%s' successfully updated.", *agent.Name.Ptr())
- sendMessageToBrowser("status", "success", fmt.Sprintf("'%s' successfully updated.", *agent.Name.Ptr()), "")
- // return
- }
|